diff --git a/src/SixLabors.Fonts/CaretMovement.cs b/src/SixLabors.Fonts/CaretMovement.cs new file mode 100644 index 00000000..b780c864 --- /dev/null +++ b/src/SixLabors.Fonts/CaretMovement.cs @@ -0,0 +1,60 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Specifies a caret movement operation within laid-out text. +/// +public enum CaretMovement +{ + /// + /// Move to the previous grapheme insertion position. + /// + Previous, + + /// + /// Move to the next grapheme insertion position. + /// + Next, + + /// + /// Move to the previous Unicode word boundary. + /// + PreviousWord, + + /// + /// Move to the next Unicode word boundary. + /// + NextWord, + + /// + /// Move to the start of the current line. + /// + LineStart, + + /// + /// Move to the end of the current line. + /// + LineEnd, + + /// + /// Move to the start of the laid-out text. + /// + TextStart, + + /// + /// Move to the end of the laid-out text. + /// + TextEnd, + + /// + /// Move to the previous visual line. + /// + LineUp, + + /// + /// Move to the next visual line. + /// + LineDown +} diff --git a/src/SixLabors.Fonts/CaretPlacement.cs b/src/SixLabors.Fonts/CaretPlacement.cs new file mode 100644 index 00000000..d73d5b23 --- /dev/null +++ b/src/SixLabors.Fonts/CaretPlacement.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Specifies an absolute caret placement within a laid-out text scope. +/// +public enum CaretPlacement +{ + /// + /// Place the caret at the start of the laid-out text scope. + /// + Start, + + /// + /// Place the caret at the end of the laid-out text scope. + /// + End +} diff --git a/src/SixLabors.Fonts/CaretPosition.cs b/src/SixLabors.Fonts/CaretPosition.cs new file mode 100644 index 00000000..0131b557 --- /dev/null +++ b/src/SixLabors.Fonts/CaretPosition.cs @@ -0,0 +1,91 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts; + +/// +/// Represents a caret line in laid-out text. +/// +public readonly struct CaretPosition +{ + /// + /// Initializes a new instance of the struct. + /// + /// The zero-based line index. + /// The grapheme insertion index in the original text. + /// The UTF-16 index in the original text. + /// The caret start point in pixel units. + /// The caret end point in pixel units. + /// Whether the caret has a second visual position. + /// The secondary caret start point in pixel units. + /// The secondary caret end point in pixel units. + /// The position to preserve when moving between visual lines. + internal CaretPosition( + int lineIndex, + int graphemeIndex, + int stringIndex, + Vector2 start, + Vector2 end, + bool hasSecondary, + Vector2 secondaryStart, + Vector2 secondaryEnd, + float lineNavigationPosition) + { + this.LineIndex = lineIndex; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; + this.Start = start; + this.End = end; + this.HasSecondary = hasSecondary; + this.SecondaryStart = secondaryStart; + this.SecondaryEnd = secondaryEnd; + this.LineNavigationPosition = lineNavigationPosition; + } + + /// + /// Gets the zero-based line index. + /// + public int LineIndex { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets the caret start point in pixel units. + /// + public Vector2 Start { get; } + + /// + /// Gets the caret end point in pixel units. + /// + public Vector2 End { get; } + + /// + /// Gets a value indicating whether a second visual caret position is available. + /// + public bool HasSecondary { get; } + + /// + /// Gets the secondary caret start point in pixel units. + /// + public Vector2 SecondaryStart { get; } + + /// + /// Gets the secondary caret end point in pixel units. + /// + public Vector2 SecondaryEnd { get; } + + /// + /// Gets the position to preserve when moving between visual lines. + /// + internal float LineNavigationPosition { get; } +} diff --git a/src/SixLabors.Fonts/FileFontMetrics.cs b/src/SixLabors.Fonts/FileFontMetrics.cs index e32572d4..313f4207 100644 --- a/src/SixLabors.Fonts/FileFontMetrics.cs +++ b/src/SixLabors.Fonts/FileFontMetrics.cs @@ -126,7 +126,7 @@ internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(tr => this.fontMetrics.Value.TryGetMarkAttachmentClass(glyphId, out markAttachmentClass); /// - public override bool TryGetVariationAxes(out VariationAxis[]? variationAxes) + public override bool TryGetVariationAxes(out ReadOnlyMemory variationAxes) => this.fontMetrics.Value.TryGetVariationAxes(out variationAxes); /// @@ -140,11 +140,11 @@ public override bool TryGetGlyphMetrics( TextDecorations textDecorations, LayoutMode layoutMode, ColorFontSupport support, - [NotNullWhen(true)] out GlyphMetrics? metrics) + [NotNullWhen(true)] out FontGlyphMetrics? metrics) => this.fontMetrics.Value.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out metrics); /// - internal override GlyphMetrics GetGlyphMetrics( + internal override FontGlyphMetrics GetGlyphMetrics( CodePoint codePoint, ushort glyphId, TextAttributes textAttributes, @@ -154,7 +154,7 @@ internal override GlyphMetrics GetGlyphMetrics( => this.fontMetrics.Value.GetGlyphMetrics(codePoint, glyphId, textAttributes, textDecorations, layoutMode, support); /// - public override IReadOnlyList GetAvailableCodePoints() + public override ReadOnlyMemory GetAvailableCodePoints() => this.fontMetrics.Value.GetAvailableCodePoints(); /// @@ -185,8 +185,8 @@ internal override ReadOnlySpan GetNormalizedCoordinates() /// Reads a from the specified stream. /// /// The file path. - /// a . - public static FileFontMetrics[] LoadFontCollection(string path) + /// A read-only memory region containing the font metrics. + public static ReadOnlyMemory LoadFontCollection(string path) { using FileStream fs = File.OpenRead(path); long startPos = fs.Position; diff --git a/src/SixLabors.Fonts/Font.cs b/src/SixLabors.Fonts/Font.cs index 477592ca..7ae6f092 100644 --- a/src/SixLabors.Fonts/Font.cs +++ b/src/SixLabors.Fonts/Font.cs @@ -261,7 +261,7 @@ public bool TryGetGlyph( [NotNullWhen(true)] out Glyph? glyph) { TextRun textRun = new() { Start = 0, End = 1, Font = this, TextAttributes = textAttributes, TextDecorations = textDecorations }; - if (this.FontMetrics.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out GlyphMetrics? metrics)) + if (this.FontMetrics.TryGetGlyphMetrics(codePoint, textAttributes, textDecorations, layoutMode, support, out FontGlyphMetrics? metrics)) { glyph = new(metrics.CloneForRendering(textRun), this.Size); return true; @@ -357,10 +357,16 @@ private string LoadFontName() } // Can't find style requested so let's just try returning the default. - IEnumerable? styles = this.Family.GetAvailableStyles(); - FontStyle defaultStyle = styles.Contains(FontStyle.Regular) - ? FontStyle.Regular - : styles.First(); + ReadOnlySpan styles = this.Family.GetAvailableStyles().Span; + FontStyle defaultStyle = styles[0]; + foreach (FontStyle style in styles) + { + if (style == FontStyle.Regular) + { + defaultStyle = FontStyle.Regular; + break; + } + } this.Family.TryGetMetrics(defaultStyle, out metrics); return metrics; diff --git a/src/SixLabors.Fonts/FontCollection.cs b/src/SixLabors.Fonts/FontCollection.cs index 2d99b98d..5d988955 100644 --- a/src/SixLabors.Fonts/FontCollection.cs +++ b/src/SixLabors.Fonts/FontCollection.cs @@ -61,19 +61,19 @@ public FontFamily Add(Stream stream, out FontDescription description) => this.AddImpl(stream, CultureInfo.InvariantCulture, out description); /// - public IEnumerable AddCollection(string path) + public ReadOnlyMemory AddCollection(string path) => this.AddCollection(path, out _); /// - public IEnumerable AddCollection(string path, out IEnumerable descriptions) + public ReadOnlyMemory AddCollection(string path, out ReadOnlyMemory descriptions) => this.AddCollectionImpl(path, CultureInfo.InvariantCulture, out descriptions); /// - public IEnumerable AddCollection(Stream stream) + public ReadOnlyMemory AddCollection(Stream stream) => this.AddCollection(stream, out _); /// - public IEnumerable AddCollection(Stream stream, out IEnumerable descriptions) + public ReadOnlyMemory AddCollection(Stream stream, out ReadOnlyMemory descriptions) => this.AddCollectionImpl(stream, CultureInfo.InvariantCulture, out descriptions); /// @@ -101,25 +101,25 @@ public FontFamily AddWithCulture(Stream stream, CultureInfo culture, out FontDes => this.AddImpl(stream, culture, out description); /// - public IEnumerable AddCollection(string path, CultureInfo culture) + public ReadOnlyMemory AddCollection(string path, CultureInfo culture) => this.AddCollection(path, culture, out _); /// - public IEnumerable AddCollection( + public ReadOnlyMemory AddCollection( string path, CultureInfo culture, - out IEnumerable descriptions) + out ReadOnlyMemory descriptions) => this.AddCollectionImpl(path, culture, out descriptions); /// - public IEnumerable AddCollection(Stream stream, CultureInfo culture) + public ReadOnlyMemory AddCollection(Stream stream, CultureInfo culture) => this.AddCollection(stream, culture, out _); /// - public IEnumerable AddCollection( + public ReadOnlyMemory AddCollection( Stream stream, CultureInfo culture, - out IEnumerable descriptions) + out ReadOnlyMemory descriptions) => this.AddCollectionImpl(stream, culture, out descriptions); /// @@ -178,7 +178,7 @@ IEnumerable IReadOnlyFontMetricsCollection.GetAllMetrics(string nam } /// - IEnumerable IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) + ReadOnlyMemory IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) => ((IReadOnlyFontMetricsCollection)this).GetAllMetrics(name, culture).Select(x => x.Description.Style).ToArray(); /// @@ -208,47 +208,58 @@ private FontFamily AddImpl(Stream stream, CultureInfo culture, out FontDescripti return ((IFontMetricsCollection)this).AddMetrics(metrics, culture); } - private HashSet AddCollectionImpl( + private ReadOnlyMemory AddCollectionImpl( string path, CultureInfo culture, - out IEnumerable descriptions) + out ReadOnlyMemory descriptions) { - FileFontMetrics[] fonts = FileFontMetrics.LoadFontCollection(path); + ReadOnlyMemory fontMetrics = FileFontMetrics.LoadFontCollection(path); + ReadOnlySpan fonts = fontMetrics.Span; FontDescription[] description = new FontDescription[fonts.Length]; - HashSet families = []; + FontFamily[] families = new FontFamily[fonts.Length]; + int familyCount = 0; for (int i = 0; i < fonts.Length; i++) { description[i] = fonts[i].Description; FontFamily family = ((IFontMetricsCollection)this).AddMetrics(fonts[i], culture); - families.Add(family); + + if (!families.AsSpan(0, familyCount).Contains(family)) + { + families[familyCount++] = family; + } } descriptions = description; - return families; + return new ReadOnlyMemory(families, 0, familyCount); } - private HashSet AddCollectionImpl( + private ReadOnlyMemory AddCollectionImpl( Stream stream, CultureInfo culture, - out IEnumerable descriptions) + out ReadOnlyMemory descriptions) { long startPos = stream.Position; using BigEndianBinaryReader reader = new(stream, true); TtcHeader ttcHeader = TtcHeader.Read(reader); - List result = new((int)ttcHeader.NumFonts); - HashSet installedFamilies = []; + FontDescription[] result = new FontDescription[(int)ttcHeader.NumFonts]; + FontFamily[] installedFamilies = new FontFamily[(int)ttcHeader.NumFonts]; + int familyCount = 0; for (int i = 0; i < ttcHeader.NumFonts; ++i) { stream.Position = startPos + ttcHeader.OffsetTable[i]; StreamFontMetrics instance = StreamFontMetrics.LoadFont(stream); - installedFamilies.Add(((IFontMetricsCollection)this).AddMetrics(instance, culture)); - FontDescription fontDescription = instance.Description; - result.Add(fontDescription); + FontFamily family = ((IFontMetricsCollection)this).AddMetrics(instance, culture); + result[i] = instance.Description; + + if (!installedFamilies.AsSpan(0, familyCount).Contains(family)) + { + installedFamilies[familyCount++] = family; + } } descriptions = result; - return installedFamilies; + return new ReadOnlyMemory(installedFamilies, 0, familyCount); } private FontFamily[] FamiliesByCultureImpl(CultureInfo culture) diff --git a/src/SixLabors.Fonts/FontDescription.cs b/src/SixLabors.Fonts/FontDescription.cs index da0d75d4..c1c19248 100644 --- a/src/SixLabors.Fonts/FontDescription.cs +++ b/src/SixLabors.Fonts/FontDescription.cs @@ -135,8 +135,8 @@ internal static FontDescription LoadDescription(FontReader reader) /// Reads all the s from the file at the specified path (typically a .ttc file like simsun.ttc). /// /// The file path. - /// a . - public static FontDescription[] LoadFontCollectionDescriptions(string path) + /// A read-only memory region containing the font descriptions. + public static ReadOnlyMemory LoadFontCollectionDescriptions(string path) { Guard.NotNullOrWhiteSpace(path, nameof(path)); @@ -148,8 +148,8 @@ public static FontDescription[] LoadFontCollectionDescriptions(string path) /// Reads all the s from the specified stream (typically a .ttc file like simsun.ttc). /// /// The stream to read the font collection from. - /// a . - public static FontDescription[] LoadFontCollectionDescriptions(Stream stream) + /// A read-only memory region containing the font descriptions. + public static ReadOnlyMemory LoadFontCollectionDescriptions(Stream stream) { long startPos = stream.Position; using var reader = new BigEndianBinaryReader(stream, true); diff --git a/src/SixLabors.Fonts/FontFamily.cs b/src/SixLabors.Fonts/FontFamily.cs index 59e8db43..94d72f0a 100644 --- a/src/SixLabors.Fonts/FontFamily.cs +++ b/src/SixLabors.Fonts/FontFamily.cs @@ -134,8 +134,8 @@ public readonly Font CreateFont(float size, FontStyle style, params FontVariatio /// /// Gets the collection of that are currently available. /// - /// The . - public readonly IEnumerable GetAvailableStyles() + /// A read-only memory region containing the available font styles. + public readonly ReadOnlyMemory GetAvailableStyles() { if (this == default) { @@ -150,31 +150,38 @@ public readonly IEnumerable GetAvailableStyles() /// /// /// When this method returns, contains the filesystem paths to the font family sources, - /// if the path exists; otherwise, an empty value for the type of the paths parameter. + /// if the path exists; otherwise, an empty memory region. /// This parameter is passed uninitialized. /// /// /// if the was created via filesystem paths; otherwise, . /// - public bool TryGetPaths(out IEnumerable paths) + public bool TryGetPaths(out ReadOnlyMemory paths) { if (this == default) { FontsThrowHelper.ThrowDefaultInstance(); } - var filePaths = new List(); - foreach (FontStyle style in this.GetAvailableStyles()) + ReadOnlySpan styles = this.GetAvailableStyles().Span; + string[]? filePaths = null; + int pathCount = 0; + + foreach (FontStyle style in styles) { if (this.collection.TryGetMetrics(this.Name, this.Culture, style, out FontMetrics? metrics) && metrics is FileFontMetrics fileMetrics) { - filePaths.Add(fileMetrics.Path); + filePaths ??= new string[styles.Length]; + filePaths[pathCount++] = fileMetrics.Path; } } - paths = filePaths; - return filePaths.Count > 0; + paths = pathCount > 0 + ? new ReadOnlyMemory(filePaths!, 0, pathCount) + : ReadOnlyMemory.Empty; + + return !paths.IsEmpty; } /// diff --git a/src/SixLabors.Fonts/FontGlyphMetrics.cs b/src/SixLabors.Fonts/FontGlyphMetrics.cs new file mode 100644 index 00000000..96978973 --- /dev/null +++ b/src/SixLabors.Fonts/FontGlyphMetrics.cs @@ -0,0 +1,534 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using System.Runtime.CompilerServices; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.General; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Represents a glyph metric from a particular font face. +/// +public abstract class FontGlyphMetrics +{ + private static readonly Vector2 YInverter = new(1, -1); + + internal FontGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + TextAttributes textAttributes, + TextDecorations textDecorations, + GlyphType glyphType) + { + this.FontMetrics = font; + this.GlyphId = glyphId; + this.CodePoint = codePoint; + this.Bounds = bounds; + this.Width = bounds.Max.X - bounds.Min.X; + this.Height = bounds.Max.Y - bounds.Min.Y; + this.UnitsPerEm = unitsPerEM; + this.AdvanceWidth = advanceWidth; + this.AdvanceHeight = advanceHeight; + this.LeftSideBearing = leftSideBearing; + this.RightSideBearing = (short)(this.AdvanceWidth - this.LeftSideBearing - this.Width); + this.TopSideBearing = topSideBearing; + this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); + this.TextAttributes = textAttributes; + this.TextDecorations = textDecorations; + this.GlyphType = glyphType; + + Vector2 offset = Vector2.Zero; + Vector2 scaleFactor = new(unitsPerEM * 72F); + + if ((textAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(font.SubscriptXSize / units, font.SubscriptYSize / units); + offset = new(font.SubscriptXOffset, font.SubscriptYOffset < 0 ? font.SubscriptYOffset : -font.SubscriptYOffset); + } + else if ((textAttributes & TextAttributes.Superscript) == TextAttributes.Superscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(font.SuperscriptXSize / units, font.SuperscriptYSize / units); + offset = new(font.SuperscriptXOffset, font.SuperscriptYOffset < 0 ? -font.SuperscriptYOffset : font.SuperscriptYOffset); + } + + this.ScaleFactor = scaleFactor; + this.Offset = offset; + } + + internal FontGlyphMetrics( + StreamFontMetrics font, + ushort glyphId, + CodePoint codePoint, + Bounds bounds, + ushort advanceWidth, + ushort advanceHeight, + short leftSideBearing, + short topSideBearing, + ushort unitsPerEM, + Vector2 offset, + Vector2 scaleFactor, + TextRun textRun, + GlyphType glyphType) + { + // This is used during cloning. Ensure anything that could be changed is copied. + this.FontMetrics = font; + this.GlyphId = glyphId; + this.CodePoint = codePoint; + this.Bounds = new Bounds(bounds.Min, bounds.Max); + this.Width = bounds.Max.X - bounds.Min.X; + this.Height = bounds.Max.Y - bounds.Min.Y; + this.UnitsPerEm = unitsPerEM; + this.AdvanceWidth = advanceWidth; + this.AdvanceHeight = advanceHeight; + this.LeftSideBearing = leftSideBearing; + this.RightSideBearing = (short)(this.AdvanceWidth - this.LeftSideBearing - this.Width); + this.TopSideBearing = topSideBearing; + this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); + this.TextAttributes = textRun.TextAttributes; + this.TextDecorations = textRun.TextDecorations; + this.GlyphType = glyphType; + this.ScaleFactor = scaleFactor; + this.Offset = offset; + this.TextRun = textRun; + } + + /// + /// Gets the font metrics. + /// + internal StreamFontMetrics FontMetrics { get; } + + /// + /// Gets the Unicode codepoint of the glyph. + /// + public CodePoint CodePoint { get; } + + /// + /// Gets the advance width for horizontal layout, expressed in font units. + /// + public ushort AdvanceWidth { get; private set; } + + /// + /// Gets the advance height for vertical layout, expressed in font units. + /// + public ushort AdvanceHeight { get; private set; } + + /// + /// Gets the left side bearing for horizontal layout, expressed in font units. + /// + public short LeftSideBearing { get; } + + /// + /// Gets the right side bearing for horizontal layout, expressed in font units. + /// + public short RightSideBearing { get; } + + /// + /// Gets the top side bearing for vertical layout, expressed in font units. + /// + public short TopSideBearing { get; } + + /// + /// Gets the bottom side bearing for vertical layout, expressed in font units. + /// + public short BottomSideBearing { get; } + + /// + /// Gets the bounds, expressed in font units. + /// + internal Bounds Bounds { get; } + + /// + /// Gets the width, expressed in font units. + /// + public float Width { get; } + + /// + /// Gets the height, expressed in font units. + /// + public float Height { get; } + + /// + /// Gets the glyph type. + /// + public GlyphType GlyphType { get; } + + /// + public ushort UnitsPerEm { get; } + + /// + /// Gets the id of the glyph within the font tables. + /// + public ushort GlyphId { get; } + + /// + /// Gets the scale factor that is applied to all glyphs in this face. + /// Normally calculated as 72 * so that 1pt = 1px + /// unless the glyph has that apply scaling adjustment. + /// + public Vector2 ScaleFactor { get; } + + /// + /// Gets or sets the offset in font design units. + /// + internal Vector2 Offset { get; set; } + + /// + /// Gets the text run that the glyph belongs to. + /// + internal TextRun TextRun { get; } = null!; + + /// + /// Gets the text attributes applied to the glyph. + /// + public TextAttributes TextAttributes { get; } + + /// + /// Gets the text decorations applied to the glyph. + /// + public TextDecorations TextDecorations { get; } + + /// + /// Performs a semi-deep clone (FontMetrics are not cloned) for rendering + /// This allows caching the original in the font metrics. + /// + /// The current text run this glyph belongs to. + /// The new . + internal abstract FontGlyphMetrics CloneForRendering(TextRun textRun); + + /// + /// Apply an offset to the glyph. + /// + /// The x-offset. + /// The y-offset. + internal void ApplyOffset(short x, short y) + => this.Offset = Vector2.Transform(this.Offset, Matrix3x2.CreateTranslation(x, y)); + + /// + /// Applies an advance to the glyph. + /// + /// The x-advance. + /// The y-advance. + internal void ApplyAdvance(short x, short y) + { + this.AdvanceWidth = (ushort)(this.AdvanceWidth + x); + + // AdvanceHeight values grow downward but font-space grows upward, hence negation + this.AdvanceHeight = (ushort)(this.AdvanceHeight - y); + } + + /// + /// Sets a new advance width. + /// + /// The x-advance. + internal void SetAdvanceWidth(ushort x) => this.AdvanceWidth = x; + + /// + /// Sets a new advance height. + /// + /// The y-advance. + internal void SetAdvanceHeight(ushort y) => this.AdvanceHeight = y; + + /// + /// Calculates the glyph bounding box in device-space (Y-down) coordinates, + /// given the layout mode, render origin, and scaled point size. + /// + /// + /// Steps: + /// 1) Select glyph bounds (or synthesize from advances if empty). + /// 2) Apply rotation if the layout mode is vertical-rotated. + /// 3) Convert from Y-up to Y-down coordinates. + /// 4) Scale and translate to device space using the specified origin. + /// + /// The glyph layout mode (horizontal, vertical, or vertical rotated). + /// The render-space origin in pixels. + /// The scaled point size, mapped to pixels by the caller. + /// + /// A representing the glyph bounds in device space. + /// + internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, float scaledPointSize) + { + Vector2 scale = new(scaledPointSize / this.ScaleFactor.X, scaledPointSize / this.ScaleFactor.Y); + Bounds b = this.Bounds; + + // 1) Substitute fallback bounds if the glyph has no outline. + if (b.Equals(Bounds.Empty)) + { + if (mode == GlyphLayoutMode.Vertical) + { + // For vertical layout, set Y-up min = -AdvanceHeight to 0 so Y-down is 0..+AdvanceHeight. + b = new Bounds(0f, -this.AdvanceHeight, 0f, 0f); + } + else + { + // For horizontal layout, just use advance width. + b = new Bounds(0f, 0f, this.AdvanceWidth, 0f); + } + } + + // 2) Rotate for vertical rotated layout. + Vector2 offsetUp = this.Offset; + if (mode == GlyphLayoutMode.VerticalRotated) + { + Matrix3x2 rot = Matrix3x2.CreateRotation(-MathF.PI / 2F); + b = Bounds.Transform(in b, rot); + offsetUp = Vector2.Transform(offsetUp, rot); + } + + // 3) Flip Y to convert to device-space (Y-down). + Vector2 minDown = b.Min * YInverter; + Vector2 maxDown = b.Max * YInverter; + Vector2 offsetDown = offsetUp * YInverter; + + // Normalize bounds after flipping. + float minX = MathF.Min(minDown.X, maxDown.X); + float maxX = MathF.Max(minDown.X, maxDown.X); + float minY = MathF.Min(minDown.Y, maxDown.Y); + float maxY = MathF.Max(minDown.Y, maxDown.Y); + + // 4) Apply scaling and origin translation. + Vector2 size = new(maxX - minX, maxY - minY); + size *= scale; + Vector2 location = origin + ((new Vector2(minX, minY) + offsetDown) * scale); + + return new FontRectangle(location.X, location.Y, size.X, size.Y); + } + + /// + /// Renders the glyph to the render surface in font units relative to a bottom left origin at (0,0) + /// + /// The surface renderer. + /// The index of the grapheme this glyph is part of. + /// The origin used to render the glyph outline. + /// The origin used to render text decorations. + /// The glyph layout mode to render using. + /// The options used to influence the rendering of this glyph. + internal abstract void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options); + + /// + /// Renders text decorations, such as underline, strikeout, and overline, for the current glyph to the specified + /// glyph renderer at the given location and layout mode. + /// + /// When rendering in vertical layout modes, decoration positions are synthesized to match common + /// typographic conventions. The renderer may override which decorations are enabled. Overline thickness is derived + /// from underline metrics if not explicitly specified. + /// The glyph renderer that receives the decoration drawing commands. + /// The position, in device-independent coordinates, where the decorations should be rendered relative to the glyph. + /// The layout mode that determines the orientation and positioning of the decorations (e.g., horizontal, vertical, + /// or vertical rotated). + /// The transformation matrix applied to the decoration coordinates before rendering. + /// The scaled pixels-per-em value used to adjust decoration size and positioning for the current rendering context. + /// Additional text rendering options that may influence decoration appearance or behavior. + protected void RenderDecorationsTo( + IGlyphRenderer renderer, + Vector2 location, + GlyphLayoutMode mode, + Matrix3x2 transform, + float scaledPPEM, + TextOptions options) + { + bool perGlyph = options.DecorationPositioningMode == DecorationPositioningMode.GlyphFont; + FontMetrics fontMetrics = perGlyph + ? this.FontMetrics + : options.Font.FontMetrics; + + // The scale factor for the decoration length is treated separately from other factors + // as it is used to scale the length of the decoration line. + // This must always be derived from the glyph's own scale factor to ensure correct length. + Vector2 lengthScaleFactor = this.ScaleFactor; + + // These factors determine horizontal and vertical scaling and offset for the decorations. + // and are either per-glyph or derived from the common font metrics. + Vector2 scaleFactor; + Vector2 offset; + if (perGlyph) + { + // Use the pre-calculated values from this glyph. + scaleFactor = this.ScaleFactor; + offset = this.Offset; + } + else + { + // To ensure that we share the scaling when sharing font metrics we need to + // recalculate the offset and scale factor here using the common font metrics. + scaleFactor = new(fontMetrics.UnitsPerEm * 72F); + offset = Vector2.Zero; + if ((this.TextAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(fontMetrics.SubscriptXSize / units, fontMetrics.SubscriptYSize / units); + offset = new(fontMetrics.SubscriptXOffset, fontMetrics.SubscriptYOffset < 0 ? fontMetrics.SubscriptYOffset : -fontMetrics.SubscriptYOffset); + } + else if ((this.TextAttributes & TextAttributes.Superscript) == TextAttributes.Superscript) + { + float units = this.UnitsPerEm; + scaleFactor /= new Vector2(fontMetrics.SuperscriptXSize / units, fontMetrics.SuperscriptYSize / units); + offset = new(fontMetrics.SuperscriptXOffset, fontMetrics.SuperscriptYOffset < 0 ? -fontMetrics.SuperscriptYOffset : fontMetrics.SuperscriptYOffset); + } + } + + bool isVerticalLayout = mode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; + (Vector2 Start, Vector2 End, float Thickness) GetEnds(TextDecorations decorations, float thickness, float decoratorPosition) + { + // For vertical layout we need to draw a vertical line. + if (isVerticalLayout) + { + float length = mode == GlyphLayoutMode.VerticalRotated ? this.AdvanceWidth : this.AdvanceHeight; + if (length == 0) + { + return (Vector2.Zero, Vector2.Zero, 0); + } + + Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; + Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; + + // Undo the vertical offset applied when laying out the text. + Vector2 scaledOffset = (offset + new Vector2(decoratorPosition, 0)) * scale; + + length *= lengthScale.Y; + thickness *= scale.X; + + Vector2 tl = new(scaledOffset.X, scaledOffset.Y); + Vector2 tr = new(scaledOffset.X + thickness, scaledOffset.Y); + Vector2 bl = new(scaledOffset.X, scaledOffset.Y + length); + + thickness = tr.X - tl.X; + + // Horizontally offset the line to the correct horizontal position + // based upon which side drawing occurs of the line. + float m = decorations switch + { + TextDecorations.Strikeout => .5F, + TextDecorations.Overline => 3, + _ => 1, + }; + + // Account for any future pixel clamping. + scaledOffset = new Vector2(thickness * m, 0) + location; + tl += scaledOffset; + bl += scaledOffset; + + return (tl, bl, thickness); + } + else + { + float length = this.AdvanceWidth; + if (length == 0) + { + return (Vector2.Zero, Vector2.Zero, 0); + } + + Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; + Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; + Vector2 scaledOffset = (offset + new Vector2(0, decoratorPosition)) * scale; + + length *= lengthScale.X; + thickness *= scale.Y; + + Vector2 tl = new(scaledOffset.X, scaledOffset.Y); + Vector2 tr = new(scaledOffset.X + length, scaledOffset.Y); + Vector2 bl = new(scaledOffset.X, scaledOffset.Y + thickness); + + thickness = bl.Y - tl.Y; + tl = (Vector2.Transform(tl, transform) * YInverter) + location; + tr = (Vector2.Transform(tr, transform) * YInverter) + location; + + return (tl, tr, thickness); + } + } + + void SetDecoration(TextDecorations decorations, float thickness, float position) + { + (Vector2 start, Vector2 end, float calcThickness) = GetEnds(decorations, thickness, position); + if (calcThickness != 0) + { + renderer.SetDecoration(decorations, start, end, calcThickness); + } + } + + // Allow the renderer to override the decorations to attach. + // When rendering glyphs vertically we use synthesized positions based upon comparisons with Pango/browsers. + // We deviate from browsers in a few ways: + // - When rendering rotated glyphs and use the default values because it fits the glyphs better. + // - We include the adjusted scale for subscript and superscript glyphs. + // - We make no attempt to adjust the underline position along a text line to render at the same position. + TextDecorations decorations = renderer.EnabledDecorations(); + bool synthesized = mode == GlyphLayoutMode.Vertical; + if ((decorations & TextDecorations.Underline) == TextDecorations.Underline) + { + SetDecoration(TextDecorations.Underline, fontMetrics.UnderlineThickness, synthesized ? Math.Abs(fontMetrics.UnderlinePosition) : fontMetrics.UnderlinePosition); + } + + if ((decorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) + { + SetDecoration(TextDecorations.Strikeout, fontMetrics.StrikeoutSize, synthesized ? fontMetrics.UnitsPerEm * .5F : fontMetrics.StrikeoutPosition); + } + + if ((decorations & TextDecorations.Overline) == TextDecorations.Overline) + { + // There's no built in metrics for overline thickness so use underline. + SetDecoration(TextDecorations.Overline, fontMetrics.UnderlineThickness, fontMetrics.UnitsPerEm - fontMetrics.UnderlinePosition); + } + } + + /// + /// Gets a value indicating whether the specified code point should be skipped when rendering. + /// + /// The code point. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + protected internal static bool ShouldSkipGlyphRendering(CodePoint codePoint) + => UnicodeUtility.ShouldNotBeRendered(codePoint); + + /// + /// Returns the size to render/measure the glyph based on the given size and resolution in px units. + /// + /// The font size in pt units. + /// The DPI (Dots Per Inch) to render/measure the glyph at + /// The . + internal float GetScaledSize(float pointSize, float dpi) + { + float scaledPPEM = dpi * pointSize; + bool forcePPEMToInt = (this.FontMetrics.HeadFlags & HeadTable.HeadFlags.ForcePPEMToInt) != 0; + + if (forcePPEMToInt) + { + scaledPPEM = MathF.Round(scaledPPEM); + } + + return scaledPPEM; + } + + /// + /// Gets the rotation matrix for the glyph based on the layout mode. + /// + /// The glyph layout mode. + /// The. + internal static Matrix3x2 GetRotationMatrix(GlyphLayoutMode mode) + { + if (mode == GlyphLayoutMode.VerticalRotated) + { + // Rotate 90 degrees clockwise. + return Matrix3x2.CreateRotation(-MathF.PI / 2F); + } + + return Matrix3x2.Identity; + } +} diff --git a/src/SixLabors.Fonts/FontMetrics.cs b/src/SixLabors.Fonts/FontMetrics.cs index 85e897fe..892c9a7a 100644 --- a/src/SixLabors.Fonts/FontMetrics.cs +++ b/src/SixLabors.Fonts/FontMetrics.cs @@ -177,9 +177,9 @@ internal FontMetrics() /// Tries to get the variation axes that this font supports. /// The font needs to have a fvar table. /// - /// An array with Variation axes. + /// A read-only memory region containing the variation axes. /// True, if fvar table is present. - public abstract bool TryGetVariationAxes(out VariationAxis[]? variationAxes); + public abstract bool TryGetVariationAxes(out ReadOnlyMemory variationAxes); /// /// Returns a value indicating whether the specified glyph is in the given mark filtering set. @@ -213,13 +213,13 @@ public abstract bool TryGetGlyphMetrics( TextDecorations textDecorations, LayoutMode layoutMode, ColorFontSupport support, - [NotNullWhen(true)] out GlyphMetrics? metrics); + [NotNullWhen(true)] out FontGlyphMetrics? metrics); /// /// Gets the unicode codepoints for which a glyph exists in the font. /// - /// The . - public abstract IReadOnlyList GetAvailableCodePoints(); + /// A read-only memory region containing the available codepoints. + public abstract ReadOnlyMemory GetAvailableCodePoints(); /// /// Gets the glyph metrics for a given code point and glyph id. @@ -233,8 +233,8 @@ public abstract bool TryGetGlyphMetrics( /// The text decorations applied to the glyph. /// The layout mode applied to the glyph. /// Options for enabling color font support during layout and rendering. - /// The . - internal abstract GlyphMetrics GetGlyphMetrics( + /// The font glyph metrics. + internal abstract FontGlyphMetrics GetGlyphMetrics( CodePoint codePoint, ushort glyphId, TextAttributes textAttributes, diff --git a/src/SixLabors.Fonts/Glyph.cs b/src/SixLabors.Fonts/Glyph.cs index c69247ec..863512f7 100644 --- a/src/SixLabors.Fonts/Glyph.cs +++ b/src/SixLabors.Fonts/Glyph.cs @@ -7,42 +7,48 @@ namespace SixLabors.Fonts; /// -/// A glyph from a particular font face. +/// Represents a font-specific glyph at the point size used for layout and rendering. /// public readonly struct Glyph { private readonly float pointSize; - internal Glyph(GlyphMetrics glyphMetrics, float pointSize) + internal Glyph(FontGlyphMetrics glyphMetrics, float pointSize) { this.GlyphMetrics = glyphMetrics; this.pointSize = pointSize; } /// - /// Gets the glyph metrics. + /// Gets the font metrics for this glyph. /// - public GlyphMetrics GlyphMetrics { get; } + public FontGlyphMetrics GlyphMetrics { get; } /// - /// Calculates the bounding box. + /// Calculates the rendered glyph bounds for the specified layout mode and origin. /// - /// The glyph layout mode to measure using. - /// The location to calculate from. - /// The DPI (Dots Per Inch) to measure the glyph at. - /// The bounding box - public FontRectangle BoundingBox(GlyphLayoutMode mode, Vector2 location, float dpi) - => this.GlyphMetrics.GetBoundingBox(mode, location, this.pointSize * dpi); + /// The glyph layout mode to measure with. + /// The glyph origin to calculate the bounds from. + /// The DPI to measure the glyph at. + /// The rendered glyph bounds. + public FontRectangle BoundingBox(GlyphLayoutMode mode, Vector2 glyphOrigin, float dpi) + => this.GlyphMetrics.GetBoundingBox(mode, glyphOrigin, this.pointSize * dpi); /// - /// Renders the glyph to the render surface relative to a top left origin. + /// Renders the glyph to the render surface. /// - /// The surface. + /// The target render surface. /// The index of the grapheme this glyph is part of. - /// The location to render the glyph at. - /// The offset of the glyph vector relative to the top-left position of the glyph advance. + /// The origin used to render the glyph outline. + /// The origin used to render text decorations. /// The glyph layout mode to render using. /// The options to render using. - internal void RenderTo(IGlyphRenderer surface, int graphemeIndex, Vector2 location, Vector2 offset, GlyphLayoutMode mode, TextOptions options) - => this.GlyphMetrics.RenderTo(surface, graphemeIndex, location, offset, mode, options); + internal void RenderTo( + IGlyphRenderer surface, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options) + => this.GlyphMetrics.RenderTo(surface, graphemeIndex, glyphOrigin, decorationOrigin, mode, options); } diff --git a/src/SixLabors.Fonts/GlyphBounds.cs b/src/SixLabors.Fonts/GlyphBounds.cs deleted file mode 100644 index 87730e98..00000000 --- a/src/SixLabors.Fonts/GlyphBounds.cs +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright (c) Six Labors. -// Licensed under the Six Labors Split License. - -using SixLabors.Fonts.Unicode; - -namespace SixLabors.Fonts; - -/// -/// Represents the bounds of a for a given . -/// -public readonly struct GlyphBounds -{ - /// - /// Initializes a new instance of the struct. - /// - /// The Unicode codepoint for the glyph. - /// The glyph bounds. - /// The index of the grapheme in original text. - /// The index of the codepoint in original text.. - public GlyphBounds(CodePoint codePoint, in FontRectangle bounds, int graphemeIndex, int stringIndex) - { - this.Codepoint = codePoint; - this.Bounds = bounds; - this.GraphemeIndex = graphemeIndex; - this.StringIndex = stringIndex; - } - - /// - /// Gets the Unicode codepoint of the glyph. - /// - public CodePoint Codepoint { get; } - - /// - /// Gets the glyph bounds. - /// - public FontRectangle Bounds { get; } - - /// - /// Gets grapheme index of glyph in original text. - /// - public int GraphemeIndex { get; } - - /// - /// Gets string index of glyph in original text. - /// - public int StringIndex { get; } - - /// - public override string ToString() - => $"Codepoint: {this.Codepoint}, Bounds: {this.Bounds}."; -} diff --git a/src/SixLabors.Fonts/GlyphLayout.cs b/src/SixLabors.Fonts/GlyphLayout.cs index 35ebf5db..67480e2a 100644 --- a/src/SixLabors.Fonts/GlyphLayout.cs +++ b/src/SixLabors.Fonts/GlyphLayout.cs @@ -7,69 +7,85 @@ namespace SixLabors.Fonts; /// -/// A glyphs layout and location +/// Represents the layout positions of a glyph entry emitted from a laid-out . /// internal readonly struct GlyphLayout { internal GlyphLayout( Glyph glyph, - Vector2 boxLocation, - Vector2 penLocation, - Vector2 offset, + Vector2 advanceOrigin, + Vector2 glyphOrigin, + Vector2 decorationOrigin, float advanceWidth, float advanceHeight, GlyphLayoutMode layoutMode, + int bidiLevel, bool isStartOfLine, int graphemeIndex, int stringIndex) { this.Glyph = glyph; this.CodePoint = glyph.GlyphMetrics.CodePoint; - this.BoxLocation = boxLocation; - this.PenLocation = penLocation; - this.Offset = offset; + this.AdvanceOrigin = advanceOrigin; + this.GlyphOrigin = glyphOrigin; + this.DecorationOrigin = decorationOrigin; this.AdvanceX = advanceWidth; this.AdvanceY = advanceHeight; this.LayoutMode = layoutMode; + this.BidiLevel = bidiLevel; this.IsStartOfLine = isStartOfLine; this.GraphemeIndex = graphemeIndex; this.StringIndex = stringIndex; } /// - /// Gets the glyph. + /// Gets the font-specific glyph for this laid-out glyph entry. /// public Glyph Glyph { get; } /// - /// Gets the codepoint represented by this glyph. + /// Gets the code point represented by this glyph. /// public CodePoint CodePoint { get; } /// - /// Gets the location of the glyph box. + /// Gets the origin of the logical advance box in DPI-normalized layout units. /// - public Vector2 BoxLocation { get; } + /// + /// Multiply by the target DPI to convert to device pixels. + /// + public Vector2 AdvanceOrigin { get; } /// - /// Gets the location to render the glyph at. + /// Gets the origin used to render the glyph outline in DPI-normalized layout units. /// - public Vector2 PenLocation { get; } + /// + /// Multiply by the target DPI to convert to device pixels. + /// + public Vector2 GlyphOrigin { get; } /// - /// Gets the offset of the glyph vector relative to the top-left position of the glyph advance. - /// For horizontal layout this will always be . + /// Gets the origin used to render text decorations in DPI-normalized layout units. /// - public Vector2 Offset { get; } + /// + /// Multiply by the target DPI to convert to device pixels. + /// + public Vector2 DecorationOrigin { get; } /// - /// Gets the width. + /// Gets the advance in the x direction in DPI-normalized layout units. /// + /// + /// Multiply by the target DPI to convert to device pixels. + /// public float AdvanceX { get; } /// - /// Gets the height. + /// Gets the advance in the y direction in DPI-normalized layout units. /// + /// + /// Multiply by the target DPI to convert to device pixels. + /// public float AdvanceY { get; } /// @@ -77,18 +93,23 @@ internal GlyphLayout( /// public GlyphLayoutMode LayoutMode { get; } + /// + /// Gets the resolved bidi embedding level. + /// + internal int BidiLevel { get; } + /// /// Gets a value indicating whether this glyph is the first glyph on a new line. /// public bool IsStartOfLine { get; } /// - /// Gets grapheme index of glyph in original text. + /// Gets the zero-based grapheme index in the original text. /// public int GraphemeIndex { get; } /// - /// Gets string index of glyph in original text. + /// Gets the zero-based UTF-16 code unit index in the original text. /// public int StringIndex { get; } @@ -98,22 +119,33 @@ internal GlyphLayout( /// The . public bool IsWhiteSpace() => UnicodeUtility.ShouldRenderWhiteSpaceOnly(this.CodePoint); - internal FontRectangle BoundingBox(float dpi) - { - // Same logic as in GlyphMetrics.RenderTo - Vector2 location = this.PenLocation; - Vector2 offset = this.Offset; - - location *= dpi; - offset *= dpi; - Vector2 renderLocation = location + offset; + /// + /// Measures the positioned logical advance rectangle in pixel units. + /// + /// The target DPI. + /// The measured advance rectangle. + internal FontRectangle MeasureAdvance(float dpi) + => new( + this.AdvanceOrigin.X * dpi, + this.AdvanceOrigin.Y * dpi, + this.AdvanceX * dpi, + this.AdvanceY * dpi); - FontRectangle box = this.Glyph.BoundingBox(this.LayoutMode, renderLocation, dpi); + /// + /// Measures the rendered glyph bounds in pixel units. + /// + /// The target DPI. + /// The measured rendered bounds. + internal FontRectangle MeasureBounds(float dpi) + { + // Same logic as in GlyphMetrics.RenderTo. + Vector2 glyphOrigin = this.GlyphOrigin * dpi; + FontRectangle box = this.Glyph.BoundingBox(this.LayoutMode, glyphOrigin, dpi); + // Whitespace uses the layout advance because it occupies measurable + // text space even though the renderer suppresses its outline. if (this.IsWhiteSpace()) { - // Take the layout advance width/height to account for advance multipliers that can cause - // the glyph to extend beyond the box. For example '\t'. if (this.LayoutMode == GlyphLayoutMode.Vertical) { return new FontRectangle( @@ -147,7 +179,7 @@ public override string ToString() { string s = this.IsStartOfLine ? "@ " : string.Empty; string ws = this.IsWhiteSpace() ? "!" : string.Empty; - Vector2 l = this.PenLocation; + Vector2 l = this.GlyphOrigin; return $"{s}{ws}{this.CodePoint.ToDebuggerDisplay()} {l.X},{l.Y} {this.AdvanceX}x{this.AdvanceY}"; } } diff --git a/src/SixLabors.Fonts/GlyphLayoutData.cs b/src/SixLabors.Fonts/GlyphLayoutData.cs new file mode 100644 index 00000000..d9643bf0 --- /dev/null +++ b/src/SixLabors.Fonts/GlyphLayoutData.cs @@ -0,0 +1,140 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Diagnostics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Per-codepoint shaping data stored inside a . +/// Each entry corresponds to a single codepoint — complex scripts may map one grapheme to +/// multiple entries (tracked via ). +/// +[DebuggerDisplay("{DebuggerDisplay,nq}")] +internal struct GlyphLayoutData +{ + internal const int NoHyphenationMarker = -1; + + /// + /// Initializes a new instance of the struct. + /// + /// The shaped glyph metrics for this codepoint. + /// The point size at which the glyph is rendered. + /// The scaled advance of this entry. + /// The scaled line height contributed by this entry. + /// The scaled typographic ascender. + /// The scaled typographic descender. + /// The symmetric metrics delta applied during line-box construction. + /// The minimum scaled Y (topmost ink) across . + /// The resolved bidi run this entry belongs to. + /// The grapheme index in the source text. + /// Whether this is the last codepoint in its grapheme cluster. + /// The codepoint index in the source text. + /// The index of this codepoint within its grapheme cluster. + /// Whether the entry participates in a transformed vertical layout. + /// Whether the entry was produced by Unicode decomposition. + /// The UTF-16 character index in the source string. + /// The marker index to use if this entry becomes a selected soft-hyphen break. + public GlyphLayoutData( + IReadOnlyList metrics, + float pointSize, + float scaledAdvance, + float scaledLineHeight, + float scaledAscender, + float scaledDescender, + float scaledDelta, + float scaledMinY, + BidiRun bidiRun, + int graphemeIndex, + bool isLastInGrapheme, + int codePointIndex, + int graphemeCodePointIndex, + bool isTransformed, + bool isDecomposed, + int stringIndex, + int hyphenationMarkerIndex = NoHyphenationMarker) + { + this.Metrics = metrics; + this.PointSize = pointSize; + this.ScaledAdvance = scaledAdvance; + this.ScaledLineHeight = scaledLineHeight; + this.ScaledAscender = scaledAscender; + this.ScaledDescender = scaledDescender; + this.ScaledDelta = scaledDelta; + this.ScaledMinY = scaledMinY; + this.BidiRun = bidiRun; + this.GraphemeIndex = graphemeIndex; + this.IsLastInGrapheme = isLastInGrapheme; + this.CodePointIndex = codePointIndex; + this.GraphemeCodePointIndex = graphemeCodePointIndex; + this.IsTransformed = isTransformed; + this.IsDecomposed = isDecomposed; + this.StringIndex = stringIndex; + this.HyphenationMarkerIndex = hyphenationMarkerIndex; + } + + /// Gets the source codepoint for this entry. + public readonly CodePoint CodePoint => this.Metrics[0].CodePoint; + + /// Gets the shaped glyph metrics produced for this codepoint (one codepoint may map to several glyphs). + public IReadOnlyList Metrics { get; } + + /// Gets the point size at which this entry is rendered. + public float PointSize { get; } + + /// Gets or sets the scaled advance of this entry (mutated by justification). + public float ScaledAdvance { get; set; } + + /// Gets the scaled line height contributed by this entry, before line-spacing is applied. + public float ScaledLineHeight { get; } + + /// Gets the scaled typographic ascender. + public float ScaledAscender { get; } + + /// Gets the scaled typographic descender. + public float ScaledDescender { get; } + + /// Gets the symmetric ascender/descender delta applied during line-box construction. + public float ScaledDelta { get; } + + /// Gets the smallest (most negative) scaled Y across . + public float ScaledMinY { get; } + + /// Gets the resolved bidi run this entry belongs to. + public BidiRun BidiRun { get; } + + /// Gets the text direction derived from . + public readonly TextDirection TextDirection => (TextDirection)this.BidiRun.Direction; + + /// Gets the zero-based grapheme index in the original text. + public int GraphemeIndex { get; } + + /// Gets or sets a value indicating whether this is the last entry in its grapheme cluster. + public bool IsLastInGrapheme { get; set; } + + /// Gets the index of this codepoint within its grapheme cluster (0-based). + public int GraphemeCodePointIndex { get; } + + /// Gets the codepoint index in the source text. + public int CodePointIndex { get; } + + /// Gets a value indicating whether the entry participates in a transformed vertical layout. + public bool IsTransformed { get; } + + /// Gets a value indicating whether the entry was produced by Unicode decomposition. + public bool IsDecomposed { get; } + + /// Gets the zero-based UTF-16 code unit index in the original text. + public int StringIndex { get; } + + /// Gets the marker index to use if this entry becomes a selected soft-hyphen break. + public int HyphenationMarkerIndex { get; } + + /// Gets a value indicating whether the codepoint is a line-break character. + public readonly bool IsNewLine => CodePoint.IsNewLine(this.CodePoint); + + private readonly string DebuggerDisplay => FormattableString + .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {this.TextDirection} : {this.CodePointIndex}, level: {this.BidiRun.Level}"); +} diff --git a/src/SixLabors.Fonts/GlyphMetrics.cs b/src/SixLabors.Fonts/GlyphMetrics.cs index fef13c07..0becda66 100644 --- a/src/SixLabors.Fonts/GlyphMetrics.cs +++ b/src/SixLabors.Fonts/GlyphMetrics.cs @@ -1,528 +1,71 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Numerics; -using System.Runtime.CompilerServices; -using SixLabors.Fonts.Rendering; -using SixLabors.Fonts.Tables.General; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; /// -/// Represents a glyph metric from a particular font face. +/// Represents one laid-out glyph entry in final layout order. /// -public abstract class GlyphMetrics +public readonly struct GlyphMetrics { - private static readonly Vector2 YInverter = new(1, -1); - + /// + /// Initializes a new instance of the struct. + /// + /// The Unicode code point represented by the glyph entry. + /// The positioned logical advance rectangle for the glyph entry in pixel units. + /// The rendered rectangle for the glyph entry in pixel units. + /// The union of the positioned logical advance rectangle and rendered rectangle in pixel units. + /// The grapheme index in the original text. + /// The UTF-16 index in the original text where the glyph entry begins. internal GlyphMetrics( - StreamFontMetrics font, - ushort glyphId, CodePoint codePoint, - Bounds bounds, - ushort advanceWidth, - ushort advanceHeight, - short leftSideBearing, - short topSideBearing, - ushort unitsPerEM, - TextAttributes textAttributes, - TextDecorations textDecorations, - GlyphType glyphType) + in FontRectangle advance, + in FontRectangle bounds, + in FontRectangle renderableBounds, + int graphemeIndex, + int stringIndex) { - this.FontMetrics = font; - this.GlyphId = glyphId; this.CodePoint = codePoint; + this.Advance = advance; this.Bounds = bounds; - this.Width = bounds.Max.X - bounds.Min.X; - this.Height = bounds.Max.Y - bounds.Min.Y; - this.UnitsPerEm = unitsPerEM; - this.AdvanceWidth = advanceWidth; - this.AdvanceHeight = advanceHeight; - this.LeftSideBearing = leftSideBearing; - this.RightSideBearing = (short)(this.AdvanceWidth - this.LeftSideBearing - this.Width); - this.TopSideBearing = topSideBearing; - this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); - this.TextAttributes = textAttributes; - this.TextDecorations = textDecorations; - this.GlyphType = glyphType; - - Vector2 offset = Vector2.Zero; - Vector2 scaleFactor = new(unitsPerEM * 72F); - - if ((textAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) - { - float units = this.UnitsPerEm; - scaleFactor /= new Vector2(font.SubscriptXSize / units, font.SubscriptYSize / units); - offset = new(font.SubscriptXOffset, font.SubscriptYOffset < 0 ? font.SubscriptYOffset : -font.SubscriptYOffset); - } - else if ((textAttributes & TextAttributes.Superscript) == TextAttributes.Superscript) - { - float units = this.UnitsPerEm; - scaleFactor /= new Vector2(font.SuperscriptXSize / units, font.SuperscriptYSize / units); - offset = new(font.SuperscriptXOffset, font.SuperscriptYOffset < 0 ? -font.SuperscriptYOffset : font.SuperscriptYOffset); - } - - this.ScaleFactor = scaleFactor; - this.Offset = offset; - } - - internal GlyphMetrics( - StreamFontMetrics font, - ushort glyphId, - CodePoint codePoint, - Bounds bounds, - ushort advanceWidth, - ushort advanceHeight, - short leftSideBearing, - short topSideBearing, - ushort unitsPerEM, - Vector2 offset, - Vector2 scaleFactor, - TextRun textRun, - GlyphType glyphType) - { - // This is used during cloning. Ensure anything that could be changed is copied. - this.FontMetrics = font; - this.GlyphId = glyphId; - this.CodePoint = codePoint; - this.Bounds = new Bounds(bounds.Min, bounds.Max); - this.Width = bounds.Max.X - bounds.Min.X; - this.Height = bounds.Max.Y - bounds.Min.Y; - this.UnitsPerEm = unitsPerEM; - this.AdvanceWidth = advanceWidth; - this.AdvanceHeight = advanceHeight; - this.LeftSideBearing = leftSideBearing; - this.RightSideBearing = (short)(this.AdvanceWidth - this.LeftSideBearing - this.Width); - this.TopSideBearing = topSideBearing; - this.BottomSideBearing = (short)(this.AdvanceHeight - this.TopSideBearing - this.Height); - this.TextAttributes = textRun.TextAttributes; - this.TextDecorations = textRun.TextDecorations; - this.GlyphType = glyphType; - this.ScaleFactor = scaleFactor; - this.Offset = offset; - this.TextRun = textRun; + this.RenderableBounds = renderableBounds; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; } /// - /// Gets the font metrics. - /// - internal StreamFontMetrics FontMetrics { get; } - - /// - /// Gets the Unicode codepoint of the glyph. + /// Gets the Unicode code point represented by the glyph entry. /// public CodePoint CodePoint { get; } /// - /// Gets the advance width for horizontal layout, expressed in font units. + /// Gets the positioned logical advance rectangle for the glyph entry in pixel units. /// - public ushort AdvanceWidth { get; private set; } + public FontRectangle Advance { get; } /// - /// Gets the advance height for vertical layout, expressed in font units. + /// Gets the rendered rectangle for the glyph entry in pixel units. /// - public ushort AdvanceHeight { get; private set; } + public FontRectangle Bounds { get; } /// - /// Gets the left side bearing for horizontal layout, expressed in font units. + /// Gets the union of the positioned logical advance rectangle and rendered rectangle in pixel units. /// - public short LeftSideBearing { get; } + public FontRectangle RenderableBounds { get; } /// - /// Gets the right side bearing for horizontal layout, expressed in font units. + /// Gets the zero-based grapheme index in the original text. /// - public short RightSideBearing { get; } + public int GraphemeIndex { get; } /// - /// Gets the top side bearing for vertical layout, expressed in font units. + /// Gets the zero-based UTF-16 code unit index in the original text. /// - public short TopSideBearing { get; } - - /// - /// Gets the bottom side bearing for vertical layout, expressed in font units. - /// - public short BottomSideBearing { get; } - - /// - /// Gets the bounds, expressed in font units. - /// - internal Bounds Bounds { get; } - - /// - /// Gets the width, expressed in font units. - /// - public float Width { get; } - - /// - /// Gets the height, expressed in font units. - /// - public float Height { get; } - - /// - /// Gets the glyph type. - /// - public GlyphType GlyphType { get; } - - /// - public ushort UnitsPerEm { get; } - - /// - /// Gets the id of the glyph within the font tables. - /// - public ushort GlyphId { get; } - - /// - /// Gets the scale factor that is applied to all glyphs in this face. - /// Normally calculated as 72 * so that 1pt = 1px - /// unless the glyph has that apply scaling adjustment. - /// - public Vector2 ScaleFactor { get; } - - /// - /// Gets or sets the offset in font design units. - /// - internal Vector2 Offset { get; set; } - - /// - /// Gets the text run that the glyph belongs to. - /// - internal TextRun TextRun { get; } = null!; - - /// - /// Gets the text attributes applied to the glyph. - /// - public TextAttributes TextAttributes { get; } - - /// - /// Gets the text decorations applied to the glyph. - /// - public TextDecorations TextDecorations { get; } - - /// - /// Performs a semi-deep clone (FontMetrics are not cloned) for rendering - /// This allows caching the original in the font metrics. - /// - /// The current text run this glyph belongs to. - /// The new . - internal abstract GlyphMetrics CloneForRendering(TextRun textRun); - - /// - /// Apply an offset to the glyph. - /// - /// The x-offset. - /// The y-offset. - internal void ApplyOffset(short x, short y) - => this.Offset = Vector2.Transform(this.Offset, Matrix3x2.CreateTranslation(x, y)); - - /// - /// Applies an advance to the glyph. - /// - /// The x-advance. - /// The y-advance. - internal void ApplyAdvance(short x, short y) - { - this.AdvanceWidth = (ushort)(this.AdvanceWidth + x); - - // AdvanceHeight values grow downward but font-space grows upward, hence negation - this.AdvanceHeight = (ushort)(this.AdvanceHeight - y); - } + public int StringIndex { get; } - /// - /// Sets a new advance width. - /// - /// The x-advance. - internal void SetAdvanceWidth(ushort x) => this.AdvanceWidth = x; - - /// - /// Sets a new advance height. - /// - /// The y-advance. - internal void SetAdvanceHeight(ushort y) => this.AdvanceHeight = y; - - /// - /// Calculates the glyph bounding box in device-space (Y-down) coordinates, - /// given the layout mode, render origin, and scaled point size. - /// - /// - /// Steps: - /// 1) Select glyph bounds (or synthesize from advances if empty). - /// 2) Apply rotation if the layout mode is vertical-rotated. - /// 3) Convert from Y-up to Y-down coordinates. - /// 4) Scale and translate to device space using the specified origin. - /// - /// The glyph layout mode (horizontal, vertical, or vertical rotated). - /// The render-space origin in pixels. - /// The scaled point size, mapped to pixels by the caller. - /// - /// A representing the glyph bounds in device space. - /// - internal FontRectangle GetBoundingBox(GlyphLayoutMode mode, Vector2 origin, float scaledPointSize) - { - Vector2 scale = new(scaledPointSize / this.ScaleFactor.X, scaledPointSize / this.ScaleFactor.Y); - Bounds b = this.Bounds; - - // 1) Substitute fallback bounds if the glyph has no outline. - if (b.Equals(Bounds.Empty)) - { - if (mode == GlyphLayoutMode.Vertical) - { - // For vertical layout, set Y-up min = -AdvanceHeight to 0 so Y-down is 0..+AdvanceHeight. - b = new Bounds(0f, -this.AdvanceHeight, 0f, 0f); - } - else - { - // For horizontal layout, just use advance width. - b = new Bounds(0f, 0f, this.AdvanceWidth, 0f); - } - } - - // 2) Rotate for vertical rotated layout. - Vector2 offsetUp = this.Offset; - if (mode == GlyphLayoutMode.VerticalRotated) - { - Matrix3x2 rot = Matrix3x2.CreateRotation(-MathF.PI / 2F); - b = Bounds.Transform(in b, rot); - offsetUp = Vector2.Transform(offsetUp, rot); - } - - // 3) Flip Y to convert to device-space (Y-down). - Vector2 minDown = b.Min * YInverter; - Vector2 maxDown = b.Max * YInverter; - Vector2 offsetDown = offsetUp * YInverter; - - // Normalize bounds after flipping. - float minX = MathF.Min(minDown.X, maxDown.X); - float maxX = MathF.Max(minDown.X, maxDown.X); - float minY = MathF.Min(minDown.Y, maxDown.Y); - float maxY = MathF.Max(minDown.Y, maxDown.Y); - - // 4) Apply scaling and origin translation. - Vector2 size = new(maxX - minX, maxY - minY); - size *= scale; - Vector2 location = origin + ((new Vector2(minX, minY) + offsetDown) * scale); - - return new FontRectangle(location.X, location.Y, size.X, size.Y); - } - - /// - /// Renders the glyph to the render surface in font units relative to a bottom left origin at (0,0) - /// - /// The surface renderer. - /// The index of the grapheme this glyph is part of. - /// The location representing offset of the glyph outer bounds relative to the origin. - /// The offset of the glyph vector relative to the top-left position of the glyph advance. - /// The glyph layout mode to render using. - /// The options used to influence the rendering of this glyph. - internal abstract void RenderTo(IGlyphRenderer renderer, int graphemeIndex, Vector2 location, Vector2 offset, GlyphLayoutMode mode, TextOptions options); - - /// - /// Renders text decorations, such as underline, strikeout, and overline, for the current glyph to the specified - /// glyph renderer at the given location and layout mode. - /// - /// When rendering in vertical layout modes, decoration positions are synthesized to match common - /// typographic conventions. The renderer may override which decorations are enabled. Overline thickness is derived - /// from underline metrics if not explicitly specified. - /// The glyph renderer that receives the decoration drawing commands. - /// The position, in device-independent coordinates, where the decorations should be rendered relative to the glyph. - /// The layout mode that determines the orientation and positioning of the decorations (e.g., horizontal, vertical, - /// or vertical rotated). - /// The transformation matrix applied to the decoration coordinates before rendering. - /// The scaled pixels-per-em value used to adjust decoration size and positioning for the current rendering context. - /// Additional text rendering options that may influence decoration appearance or behavior. - protected void RenderDecorationsTo( - IGlyphRenderer renderer, - Vector2 location, - GlyphLayoutMode mode, - Matrix3x2 transform, - float scaledPPEM, - TextOptions options) - { - bool perGlyph = options.DecorationPositioningMode == DecorationPositioningMode.GlyphFont; - FontMetrics fontMetrics = perGlyph - ? this.FontMetrics - : options.Font.FontMetrics; - - // The scale factor for the decoration length is treated separately from other factors - // as it is used to scale the length of the decoration line. - // This must always be derived from the glyph's own scale factor to ensure correct length. - Vector2 lengthScaleFactor = this.ScaleFactor; - - // These factors determine horizontal and vertical scaling and offset for the decorations. - // and are either per-glyph or derived from the common font metrics. - Vector2 scaleFactor; - Vector2 offset; - if (perGlyph) - { - // Use the pre-calculated values from this glyph. - scaleFactor = this.ScaleFactor; - offset = this.Offset; - } - else - { - // To ensure that we share the scaling when sharing font metrics we need to - // recalculate the offset and scale factor here using the common font metrics. - scaleFactor = new(fontMetrics.UnitsPerEm * 72F); - offset = Vector2.Zero; - if ((this.TextAttributes & TextAttributes.Subscript) == TextAttributes.Subscript) - { - float units = this.UnitsPerEm; - scaleFactor /= new Vector2(fontMetrics.SubscriptXSize / units, fontMetrics.SubscriptYSize / units); - offset = new(fontMetrics.SubscriptXOffset, fontMetrics.SubscriptYOffset < 0 ? fontMetrics.SubscriptYOffset : -fontMetrics.SubscriptYOffset); - } - else if ((this.TextAttributes & TextAttributes.Superscript) == TextAttributes.Superscript) - { - float units = this.UnitsPerEm; - scaleFactor /= new Vector2(fontMetrics.SuperscriptXSize / units, fontMetrics.SuperscriptYSize / units); - offset = new(fontMetrics.SuperscriptXOffset, fontMetrics.SuperscriptYOffset < 0 ? -fontMetrics.SuperscriptYOffset : fontMetrics.SuperscriptYOffset); - } - } - - bool isVerticalLayout = mode is GlyphLayoutMode.Vertical or GlyphLayoutMode.VerticalRotated; - (Vector2 Start, Vector2 End, float Thickness) GetEnds(TextDecorations decorations, float thickness, float decoratorPosition) - { - // For vertical layout we need to draw a vertical line. - if (isVerticalLayout) - { - float length = mode == GlyphLayoutMode.VerticalRotated ? this.AdvanceWidth : this.AdvanceHeight; - if (length == 0) - { - return (Vector2.Zero, Vector2.Zero, 0); - } - - Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; - Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; - - // Undo the vertical offset applied when laying out the text. - Vector2 scaledOffset = (offset + new Vector2(decoratorPosition, 0)) * scale; - - length *= lengthScale.Y; - thickness *= scale.X; - - Vector2 tl = new(scaledOffset.X, scaledOffset.Y); - Vector2 tr = new(scaledOffset.X + thickness, scaledOffset.Y); - Vector2 bl = new(scaledOffset.X, scaledOffset.Y + length); - - thickness = tr.X - tl.X; - - // Horizontally offset the line to the correct horizontal position - // based upon which side drawing occurs of the line. - float m = decorations switch - { - TextDecorations.Strikeout => .5F, - TextDecorations.Overline => 3, - _ => 1, - }; - - // Account for any future pixel clamping. - scaledOffset = new Vector2(thickness * m, 0) + location; - tl += scaledOffset; - bl += scaledOffset; - - return (tl, bl, thickness); - } - else - { - float length = this.AdvanceWidth; - if (length == 0) - { - return (Vector2.Zero, Vector2.Zero, 0); - } - - Vector2 lengthScale = new Vector2(scaledPPEM) / lengthScaleFactor; - Vector2 scale = new Vector2(scaledPPEM) / scaleFactor; - Vector2 scaledOffset = (offset + new Vector2(0, decoratorPosition)) * scale; - - length *= lengthScale.X; - thickness *= scale.Y; - - Vector2 tl = new(scaledOffset.X, scaledOffset.Y); - Vector2 tr = new(scaledOffset.X + length, scaledOffset.Y); - Vector2 bl = new(scaledOffset.X, scaledOffset.Y + thickness); - - thickness = bl.Y - tl.Y; - tl = (Vector2.Transform(tl, transform) * YInverter) + location; - tr = (Vector2.Transform(tr, transform) * YInverter) + location; - - return (tl, tr, thickness); - } - } - - void SetDecoration(TextDecorations decorations, float thickness, float position) - { - (Vector2 start, Vector2 end, float calcThickness) = GetEnds(decorations, thickness, position); - if (calcThickness != 0) - { - renderer.SetDecoration(decorations, start, end, calcThickness); - } - } - - // Allow the renderer to override the decorations to attach. - // When rendering glyphs vertically we use synthesized positions based upon comparisons with Pango/browsers. - // We deviate from browsers in a few ways: - // - When rendering rotated glyphs and use the default values because it fits the glyphs better. - // - We include the adjusted scale for subscript and superscript glyphs. - // - We make no attempt to adjust the underline position along a text line to render at the same position. - TextDecorations decorations = renderer.EnabledDecorations(); - bool synthesized = mode == GlyphLayoutMode.Vertical; - if ((decorations & TextDecorations.Underline) == TextDecorations.Underline) - { - SetDecoration(TextDecorations.Underline, fontMetrics.UnderlineThickness, synthesized ? Math.Abs(fontMetrics.UnderlinePosition) : fontMetrics.UnderlinePosition); - } - - if ((decorations & TextDecorations.Strikeout) == TextDecorations.Strikeout) - { - SetDecoration(TextDecorations.Strikeout, fontMetrics.StrikeoutSize, synthesized ? fontMetrics.UnitsPerEm * .5F : fontMetrics.StrikeoutPosition); - } - - if ((decorations & TextDecorations.Overline) == TextDecorations.Overline) - { - // There's no built in metrics for overline thickness so use underline. - SetDecoration(TextDecorations.Overline, fontMetrics.UnderlineThickness, fontMetrics.UnitsPerEm - fontMetrics.UnderlinePosition); - } - } - - /// - /// Gets a value indicating whether the specified code point should be skipped when rendering. - /// - /// The code point. - /// The . - [MethodImpl(MethodImplOptions.AggressiveInlining)] - protected internal static bool ShouldSkipGlyphRendering(CodePoint codePoint) - => UnicodeUtility.ShouldNotBeRendered(codePoint); - - /// - /// Returns the size to render/measure the glyph based on the given size and resolution in px units. - /// - /// The font size in pt units. - /// The DPI (Dots Per Inch) to render/measure the glyph at - /// The . - internal float GetScaledSize(float pointSize, float dpi) - { - float scaledPPEM = dpi * pointSize; - bool forcePPEMToInt = (this.FontMetrics.HeadFlags & HeadTable.HeadFlags.ForcePPEMToInt) != 0; - - if (forcePPEMToInt) - { - scaledPPEM = MathF.Round(scaledPPEM); - } - - return scaledPPEM; - } - - /// - /// Gets the rotation matrix for the glyph based on the layout mode. - /// - /// The glyph layout mode. - /// The. - internal static Matrix3x2 GetRotationMatrix(GlyphLayoutMode mode) - { - if (mode == GlyphLayoutMode.VerticalRotated) - { - // Rotate 90 degrees clockwise. - return Matrix3x2.CreateRotation(-MathF.PI / 2F); - } - - return Matrix3x2.Identity; - } + /// + public override string ToString() + => $"CodePoint: {this.CodePoint}, Advance: {this.Advance}, Bounds: {this.Bounds}, RenderableBounds: {this.RenderableBounds}."; } diff --git a/src/SixLabors.Fonts/GlyphPositioningCollection.cs b/src/SixLabors.Fonts/GlyphPositioningCollection.cs index 2a4d3389..a8e23645 100644 --- a/src/SixLabors.Fonts/GlyphPositioningCollection.cs +++ b/src/SixLabors.Fonts/GlyphPositioningCollection.cs @@ -97,7 +97,7 @@ public void DisableShapingFeature(int index, Tag feature) /// Whether the glyph is the result of a substitution. /// Whether the glyph is the result of a vertical substitution. /// Whether the glyph is the result of a decomposition substitution. - /// + /// /// When this method returns, contains the glyph metrics associated with the specified offset, /// if the value is found; otherwise, the default value for the type of the metrics parameter. /// This parameter is passed uninitialized. @@ -110,9 +110,9 @@ public bool TryGetGlyphMetricsAtOffset( out bool isSubstituted, out bool isVerticalSubstitution, out bool isDecomposed, - [NotNullWhen(true)] out IReadOnlyList? metrics) + [NotNullWhen(true)] out IReadOnlyList? data) { - List match = []; + List match = []; pointSize = 0; isSubstituted = false; isVerticalSubstitution = false; @@ -132,18 +132,22 @@ public bool TryGetGlyphMetricsAtOffset( } GlyphPositioningData glyph = this.glyphs[i]; - isSubstituted = glyph.Data.IsSubstituted; - isDecomposed = glyph.Data.IsDecomposed; - - foreach (Tag feature in glyph.Data.AppliedFeatures) + if (!glyph.Data.IsPlaceholder) { - isVerticalSubstitution |= feature == vert; - isVerticalSubstitution |= feature == vrt2; - isVerticalSubstitution |= feature == vrtr; + isSubstituted = glyph.Data.IsSubstituted; + isDecomposed = glyph.Data.IsDecomposed; + + foreach (Tag feature in glyph.Data.AppliedFeatures) + { + isVerticalSubstitution |= feature == vert; + isVerticalSubstitution |= feature == vrt2; + isVerticalSubstitution |= feature == vrtr; + } + + pointSize = glyph.PointSize; } - pointSize = glyph.PointSize; - match.Add(glyph.Metrics); + match.Add(glyph); } else if (match.Count > 0) { @@ -152,7 +156,7 @@ public bool TryGetGlyphMetricsAtOffset( } } - metrics = match; + data = match; return match.Count > 0; } @@ -208,7 +212,7 @@ public bool TryUpdate(Font font, GlyphSubstitutionCollection collection) isVertical |= feature == vrtr; } - GlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); { // If the glyphs are fallbacks we don't want them as // we've already captured them on the first run. @@ -278,6 +282,35 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) CodePoint codePoint = data.CodePoint; ushort id = data.GlyphId; + if (data.IsPlaceholder) + { + // Placeholders are synthetic glyphs: they need layout metrics but must not + // go through font glyph lookup, fallback resolution, or GPOS positioning. + StreamFontMetrics streamFontMetrics = fontMetrics is FileFontMetrics fileFontMetrics + ? fileFontMetrics.StreamFontMetrics + : (StreamFontMetrics)fontMetrics; + + FontGlyphMetrics placeholderMetrics = new PlaceholderGlyphMetrics( + streamFontMetrics, + data.TextRun.Placeholder.GetValueOrDefault(), + font.Size, + this.TextOptions.Dpi, + data.TextRun); + + GlyphShapingBounds placeholderBounds = layoutMode.IsVertical() + ? new(0, 0, 0, placeholderMetrics.AdvanceHeight) + : new(0, 0, placeholderMetrics.AdvanceWidth, 0); + + GlyphShapingData placeholderData = new(data, true) + { + Bounds = placeholderBounds, + IsPositioned = true + }; + + this.glyphs.Add(new(offset, placeholderData, font.Size, placeholderMetrics)); + continue; + } + // Perform a semi-deep clone (FontMetrics is not cloned) so we can continue to // cache the original in the font metrics and only update our collection. TextAttributes textAttributes = data.TextRun.TextAttributes; @@ -291,7 +324,7 @@ public bool TryAdd(Font font, GlyphSubstitutionCollection collection) isVertical |= feature == vrtr; } - GlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); + FontGlyphMetrics metrics = fontMetrics.GetGlyphMetrics(codePoint, id, textAttributes, textDecorations, layoutMode, colorFontSupport); if (metrics.GlyphType == GlyphType.Fallback && !CodePoint.IsControl(codePoint)) { @@ -327,7 +360,7 @@ public void UpdatePosition(FontMetrics fontMetrics, int index) } ushort glyphId = data.GlyphId; - GlyphMetrics m = this.glyphs[index].Metrics; + FontGlyphMetrics m = this.glyphs[index].Metrics; if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { @@ -363,7 +396,7 @@ public void Advance(FontMetrics fontMetrics, int index, ushort glyphId, short dx Tag vrtr = KnownFeatureTags.VerticalAlternatesForRotation; GlyphPositioningData glyph = this.glyphs[index]; - GlyphMetrics m = glyph.Metrics; + FontGlyphMetrics m = glyph.Metrics; if (m.GlyphId == glyphId && fontMetrics == m.FontMetrics) { @@ -398,9 +431,9 @@ public bool ShouldProcess(FontMetrics fontMetrics, int index) } [DebuggerDisplay("{DebuggerDisplay,nq}")] - private class GlyphPositioningData + public class GlyphPositioningData { - public GlyphPositioningData(int offset, GlyphShapingData data, float pointSize, GlyphMetrics metrics) + public GlyphPositioningData(int offset, GlyphShapingData data, float pointSize, FontGlyphMetrics metrics) { this.Offset = offset; this.Data = data; @@ -414,7 +447,7 @@ public GlyphPositioningData(int offset, GlyphShapingData data, float pointSize, public float PointSize { get; set; } - public GlyphMetrics Metrics { get; set; } + public FontGlyphMetrics Metrics { get; set; } private string DebuggerDisplay => FormattableString.Invariant($"Offset: {this.Offset}, Data: {this.Data.ToDebuggerDisplay()}"); } diff --git a/src/SixLabors.Fonts/GlyphShapingData.cs b/src/SixLabors.Fonts/GlyphShapingData.cs index e144a2f6..910cc895 100644 --- a/src/SixLabors.Fonts/GlyphShapingData.cs +++ b/src/SixLabors.Fonts/GlyphShapingData.cs @@ -41,6 +41,8 @@ public GlyphShapingData(GlyphShapingData data, bool clearFeatures = false) this.CursiveAttachment = data.CursiveAttachment; this.IsSubstituted = data.IsSubstituted; this.IsDecomposed = data.IsDecomposed; + this.IsPlaceholder = data.IsPlaceholder; + this.BidiRun = data.BidiRun; this.IsPositioned = data.IsPositioned; this.IsKerned = data.IsKerned; @@ -183,6 +185,16 @@ public ushort GlyphId /// public bool IsDecomposed { get; set; } + /// + /// Gets or sets a value indicating whether this glyph represents an inline placeholder. + /// + public bool IsPlaceholder { get; set; } + + /// + /// Gets or sets the bidi run assigned to an inline placeholder. + /// + public BidiRun BidiRun { get; set; } + /// /// Gets or sets a value indicating whether this glyph has been positioned. /// diff --git a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs index 68ed4d59..23895fa2 100644 --- a/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs +++ b/src/SixLabors.Fonts/GlyphSubstitutionCollection.cs @@ -130,6 +130,23 @@ public void AddGlyph(ushort glyphId, CodePoint codePoint, TextDirection directio GlyphId = glyphId, })); + /// + /// Adds an atomic inline placeholder to the collection. + /// + /// The object replacement codepoint used for Unicode processing. + /// The resolved bidi run for the placeholder. + /// The text run this placeholder belongs to. + /// The zero-based index within the input codepoint collection. + public void AddPlaceholder(CodePoint codePoint, BidiRun bidiRun, TextRun textRun, int offset) + => this.glyphs.Add(new(offset, new(textRun) + { + CodePoint = codePoint, + Direction = (TextDirection)bidiRun.Direction, + GlyphId = 0, + IsPlaceholder = true, + BidiRun = bidiRun, + })); + /// /// Moves the specified glyph to the specified position. /// @@ -218,9 +235,7 @@ public void Sort(int startIndex, int endIndex, Comparison comp while (j > startIndex && comparer(glyphs[j - 1].Data, glyphs[j].Data) > 0) { // Swap Data references between adjacent slots. - GlyphShapingData temp = glyphs[j - 1].Data; - glyphs[j - 1].Data = glyphs[j].Data; - glyphs[j].Data = temp; + (glyphs[j].Data, glyphs[j - 1].Data) = (glyphs[j - 1].Data, glyphs[j].Data); j--; } } diff --git a/src/SixLabors.Fonts/GlyphType.cs b/src/SixLabors.Fonts/GlyphType.cs index 38dd0417..c627d643 100644 --- a/src/SixLabors.Fonts/GlyphType.cs +++ b/src/SixLabors.Fonts/GlyphType.cs @@ -21,5 +21,10 @@ public enum GlyphType /// /// This is a multi-layer colored glyph (emoji). /// - Painted + Painted, + + /// + /// This is an atomic inline placeholder supplied by the caller. + /// + Placeholder } diff --git a/src/SixLabors.Fonts/GraphemeMetrics.cs b/src/SixLabors.Fonts/GraphemeMetrics.cs new file mode 100644 index 00000000..d99f8172 --- /dev/null +++ b/src/SixLabors.Fonts/GraphemeMetrics.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Represents one coalesced grapheme in final layout order. +/// +public readonly struct GraphemeMetrics +{ + /// + /// Initializes a new instance of the struct. + /// + /// The positioned logical advance rectangle for the grapheme in pixel units. + /// The rendered glyph bounds for the grapheme in pixel units. + /// The union of the positioned logical advance bounds and rendered glyph bounds in pixel units. + /// The grapheme index in the original text. + /// The UTF-16 index in the original text where the grapheme begins. + /// The resolved bidi embedding level. + /// Whether the grapheme represents a line break. + internal GraphemeMetrics( + FontRectangle advance, + FontRectangle bounds, + FontRectangle renderableBounds, + int graphemeIndex, + int stringIndex, + int bidiLevel, + bool isLineBreak) + { + this.Advance = advance; + this.Bounds = bounds; + this.RenderableBounds = renderableBounds; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; + this.BidiLevel = bidiLevel; + this.IsLineBreak = isLineBreak; + } + + /// + /// Gets the positioned logical advance rectangle for the grapheme in pixel units. + /// + public FontRectangle Advance { get; } + + /// + /// Gets the rendered glyph bounds for the grapheme in pixel units. + /// + public FontRectangle Bounds { get; } + + /// + /// Gets the union of the positioned logical advance bounds and rendered glyph bounds in pixel units. + /// + public FontRectangle RenderableBounds { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets the resolved bidi embedding level. + /// + internal int BidiLevel { get; } + + /// + /// Gets a value indicating whether this grapheme represents a line break. + /// + public bool IsLineBreak { get; } +} diff --git a/src/SixLabors.Fonts/IFontCollection.cs b/src/SixLabors.Fonts/IFontCollection.cs index 162e4ebc..0577b961 100644 --- a/src/SixLabors.Fonts/IFontCollection.cs +++ b/src/SixLabors.Fonts/IFontCollection.cs @@ -45,31 +45,31 @@ public interface IFontCollection : IReadOnlyFontCollection /// Adds a true type font collection (.ttc). /// /// The font collection path. - /// The new . - public IEnumerable AddCollection(string path); + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(string path); /// /// Adds a true type font collection (.ttc). /// /// The font collection path. /// The descriptions of the added fonts. - /// The new . - public IEnumerable AddCollection(string path, out IEnumerable descriptions); + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(string path, out ReadOnlyMemory descriptions); /// /// Adds a true type font collection (.ttc). /// /// The font stream. - /// The new . - public IEnumerable AddCollection(Stream stream); + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(Stream stream); /// /// Adds a true type font collection (.ttc). /// /// The font stream. /// The descriptions of the added fonts. - /// The new . - public IEnumerable AddCollection(Stream stream, out IEnumerable descriptions); + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(Stream stream, out ReadOnlyMemory descriptions); /// /// Adds a font to the collection. @@ -110,8 +110,8 @@ public interface IFontCollection : IReadOnlyFontCollection /// /// The font collection path. /// The culture of the fonts to add. - /// The new . - public IEnumerable AddCollection(string path, CultureInfo culture); + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(string path, CultureInfo culture); /// /// Adds a true type font collection (.ttc). @@ -119,19 +119,19 @@ public interface IFontCollection : IReadOnlyFontCollection /// The font collection path. /// The culture of the fonts to add. /// The descriptions of the added fonts. - /// The new . - public IEnumerable AddCollection( + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection( string path, CultureInfo culture, - out IEnumerable descriptions); + out ReadOnlyMemory descriptions); /// /// Adds a true type font collection (.ttc). /// /// The font stream. /// The culture of the fonts to add. - /// The new . - public IEnumerable AddCollection(Stream stream, CultureInfo culture); + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection(Stream stream, CultureInfo culture); /// /// Adds a true type font collection (.ttc). @@ -139,9 +139,9 @@ public IEnumerable AddCollection( /// The font stream. /// The culture of the fonts to add. /// The descriptions of the added fonts. - /// The new . - public IEnumerable AddCollection( + /// A read-only memory region containing the new values. + public ReadOnlyMemory AddCollection( Stream stream, CultureInfo culture, - out IEnumerable descriptions); + out ReadOnlyMemory descriptions); } diff --git a/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs b/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs index 0b11f645..4816825a 100644 --- a/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs +++ b/src/SixLabors.Fonts/IReadonlyFontMetricsCollection.cs @@ -44,9 +44,9 @@ internal interface IReadOnlyFontMetricsCollection /// /// The font family name. /// The culture to use when searching for a match. - /// The . + /// A read-only memory region containing the available font styles. /// is - public IEnumerable GetAllStyles(string name, CultureInfo culture); + public ReadOnlyMemory GetAllStyles(string name, CultureInfo culture); /// public IEnumerator GetEnumerator(); diff --git a/src/SixLabors.Fonts/LineLayout.cs b/src/SixLabors.Fonts/LineLayout.cs new file mode 100644 index 00000000..dd949f0f --- /dev/null +++ b/src/SixLabors.Fonts/LineLayout.cs @@ -0,0 +1,204 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.Fonts; + +/// +/// Represents one laid-out line from a . +/// +public sealed class LineLayout +{ + private readonly TextBox textBox; + private readonly TextOptions options; + private readonly float wrappingLength; + private readonly int lineIndex; + private readonly LayoutMode layoutMode; + private readonly ReadOnlyMemory graphemeMetrics; + private readonly ReadOnlyMemory wordMetrics; + private GlyphMetrics[]? glyphMetrics; + + internal LineLayout( + TextBox textBox, + TextOptions options, + float wrappingLength, + int lineIndex, + in LineMetrics metrics, + ReadOnlyMemory graphemeMetrics, + ReadOnlyMemory wordMetrics) + { + this.textBox = textBox; + this.options = options; + this.wrappingLength = wrappingLength; + this.lineIndex = lineIndex; + this.layoutMode = options.LayoutMode; + this.LineMetrics = metrics; + this.graphemeMetrics = graphemeMetrics; + this.wordMetrics = wordMetrics; + } + + /// + /// Gets the measured line metrics. + /// + public LineMetrics LineMetrics { get; } + + /// + /// Gets the grapheme metrics entries for this line in final layout order. + /// + public ReadOnlySpan GraphemeMetrics => this.graphemeMetrics.Span; + + /// + /// Hit tests the supplied point against this line's grapheme advance bounds. + /// + /// The point in pixel units. + /// The hit-tested grapheme position. + public TextHit HitTest(Vector2 point) + => TextInteraction.HitTestLine(this.lineIndex, this.GraphemeMetrics, point, this.layoutMode); + + /// + /// Gets the caret position for the supplied hit. + /// + /// The hit-tested grapheme position. + /// The caret position in pixel units. + public CaretPosition GetCaretPosition(TextHit hit) + => TextInteraction.GetCaretPositionLine( + this.lineIndex, + this.LineMetrics, + this.GraphemeMetrics, + hit.GraphemeInsertionIndex, + this.layoutMode); + + /// + /// Gets an absolute caret position in the laid-out line. + /// + /// The absolute caret placement. + /// The caret position in pixel units. + public CaretPosition GetCaret(CaretPlacement placement) + => TextInteraction.GetCaretLine( + this.lineIndex, + this.LineMetrics, + this.GraphemeMetrics, + placement, + this.layoutMode, + this.textBox.TextDirection()); + + /// + /// Moves the supplied caret by the requested operation within this line. + /// + /// The current caret position. + /// The movement operation. + /// The moved caret position in pixel units. + public CaretPosition MoveCaret(CaretPosition caret, CaretMovement movement) + => TextInteraction.MoveCaretLine( + this.lineIndex, + this.LineMetrics, + this.GraphemeMetrics, + this.wordMetrics.Span, + caret, + movement, + this.layoutMode, + this.textBox.TextDirection()); + + /// + /// Gets the word metrics for the word-boundary segment containing the supplied hit-tested grapheme position. + /// + /// The hit-tested grapheme position. + /// The word metrics containing the hit grapheme. + public WordMetrics GetWordMetrics(TextHit hit) + => TextInteraction.GetWordMetrics(this.wordMetrics.Span, hit.GraphemeIndex); + + /// + /// Gets the word metrics for the word-boundary segment containing the supplied caret position. + /// + /// The caret position. + /// The word metrics containing the caret's grapheme insertion index. + public WordMetrics GetWordMetrics(CaretPosition caret) + => TextInteraction.GetWordMetrics(this.wordMetrics.Span, caret.GraphemeIndex); + + /// + /// Gets selection bounds between two hit-tested grapheme positions. + /// + /// The fixed selection endpoint. + /// The active selection endpoint. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(TextHit anchor, TextHit focus) + => TextInteraction.GetSelectionBoundsLine( + this.LineMetrics, + this.GraphemeMetrics, + anchor.GraphemeInsertionIndex, + focus.GraphemeInsertionIndex, + this.layoutMode); + + /// + /// Gets selection bounds between two caret positions. + /// + /// The fixed selection endpoint. + /// The active selection endpoint. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(CaretPosition anchor, CaretPosition focus) + => TextInteraction.GetSelectionBoundsLine( + this.LineMetrics, + this.GraphemeMetrics, + anchor.GraphemeIndex, + focus.GraphemeIndex, + this.layoutMode); + + /// + /// Gets line-local selection bounds for the supplied grapheme metrics. + /// + /// The grapheme metrics to select. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(GraphemeMetrics metrics) + => TextInteraction.GetSelectionBoundsLine( + this.LineMetrics, + metrics, + this.layoutMode); + + /// + /// Gets line-local selection bounds for the supplied word metrics. + /// + /// The word metrics to select. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(WordMetrics metrics) + => TextInteraction.GetSelectionBoundsLine( + this.LineMetrics, + this.GraphemeMetrics, + metrics.GraphemeStart, + metrics.GraphemeEnd, + this.layoutMode); + + /// + public ReadOnlyMemory GetGlyphMetrics() + => this.glyphMetrics ??= TextBlock.GetGlyphMetricsArray( + this.textBox, + this.options, + this.wrappingLength, + this.lineIndex); + + /// + /// Renders this line to the supplied glyph renderer. + /// + /// The target renderer. + public void RenderTo(IGlyphRenderer renderer) + { + FontRectangle bounds = FontRectangle.Empty; + ReadOnlySpan glyphMetrics = this.GetGlyphMetrics().Span; + + for (int i = 0; i < glyphMetrics.Length; i++) + { + bounds = i == 0 + ? glyphMetrics[i].Bounds + : FontRectangle.Union(bounds, glyphMetrics[i].Bounds); + } + + TextBlock.RenderTo( + renderer, + this.textBox, + this.options, + this.wrappingLength, + bounds, + this.lineIndex); + } +} diff --git a/src/SixLabors.Fonts/LineLayoutEnumerator.cs b/src/SixLabors.Fonts/LineLayoutEnumerator.cs new file mode 100644 index 00000000..bcfad3f1 --- /dev/null +++ b/src/SixLabors.Fonts/LineLayoutEnumerator.cs @@ -0,0 +1,67 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Walks a one laid-out line at a time. +/// +/// +/// Each produced line is positioned independently, without cumulative offsets from earlier or later lines. +/// +public sealed class LineLayoutEnumerator +{ + private readonly TextBlock textBlock; + private readonly TextLineBreakEnumerator lineEnumerator; + private readonly TextDirection textDirection; + private readonly bool suppressLayout; + private LineLayout? current; + + /// + /// Initializes a new instance of the class. + /// + /// The prepared text block to enumerate. + internal LineLayoutEnumerator(TextBlock textBlock) + { + this.textBlock = textBlock; + this.lineEnumerator = new(textBlock.LogicalLine, textBlock.Options); + this.textDirection = TextLayout.GetTextDirection(textBlock.LogicalLine, textBlock.Options); + this.suppressLayout = textBlock.Options.MaxLines == 0; + } + + /// + /// Gets the current line layout. + /// + public LineLayout Current => this.current!; + + /// + /// Advances to the next line using the supplied wrapping length. + /// + /// + /// The wrapping length applies only to the line being produced by this call. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// when a line was produced. + public bool MoveNext(float wrappingLength) + { + if (this.suppressLayout) + { + return false; + } + + if (!this.lineEnumerator.MoveNext(wrappingLength)) + { + return false; + } + + // The walker lays out each produced line independently so callers can + // place variable-width lines into custom columns, shapes, or virtualized + // surfaces without inheriting block-level line offsets. + this.current = this.textBlock.GetLineLayout( + this.lineEnumerator.Current, + wrappingLength, + this.textDirection); + + return true; + } +} diff --git a/src/SixLabors.Fonts/LineMetrics.cs b/src/SixLabors.Fonts/LineMetrics.cs index 00968cbf..712481ce 100644 --- a/src/SixLabors.Fonts/LineMetrics.cs +++ b/src/SixLabors.Fonts/LineMetrics.cs @@ -1,36 +1,39 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.Fonts; /// /// Encapsulates measured metrics for a single laid-out text line. /// -/// -/// This type is layout-mode agnostic: -/// -/// Horizontal layouts: is the X start position and is the width. -/// Vertical layouts: is the Y start position and is the height. -/// -/// public readonly struct LineMetrics { /// /// Initializes a new instance of the struct. /// - /// Ascender line position within the line box. - /// Baseline position within the line box. - /// Descender line position within the line box. - /// Total line-box size (includes effective line spacing). - /// Line start position in the primary layout flow direction after alignment. - /// Line extent in the primary layout flow direction. - public LineMetrics( + /// The ascender line position within the line box. + /// The baseline position within the line box. + /// The descender line position within the line box. + /// The total line-box size for this line. + /// The logical line box start position in pixel units. + /// The logical line box extent in pixel units. + /// The UTF-16 index in the original text where this line begins. + /// The grapheme index in the original text where this line begins. + /// The number of graphemes in the line. + /// The offset of this line's first grapheme metrics entry. + internal LineMetrics( float ascender, float baseline, float descender, float lineHeight, - float start, - float extent) + Vector2 start, + Vector2 extent, + int stringIndex, + int graphemeIndex, + int graphemeCount, + int graphemeOffset) { this.Ascender = ascender; this.Baseline = baseline; @@ -38,6 +41,10 @@ public LineMetrics( this.LineHeight = lineHeight; this.Start = start; this.Extent = extent; + this.StringIndex = stringIndex; + this.GraphemeIndex = graphemeIndex; + this.GraphemeCount = graphemeCount; + this.GraphemeOffset = graphemeOffset; } /// @@ -72,12 +79,32 @@ public LineMetrics( public float LineHeight { get; } /// - /// Gets the line start position in the primary layout flow direction. + /// Gets the logical line box start position in pixel units. + /// + public Vector2 Start { get; } + + /// + /// Gets the logical line box extent in pixel units. + /// + public Vector2 Extent { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the number of graphemes in the line. /// - public float Start { get; } + public int GraphemeCount { get; } /// - /// Gets the line extent in the primary layout flow direction. + /// Gets the offset of this line's first entry in the flattened grapheme metrics buffer. /// - public float Extent { get; } + internal int GraphemeOffset { get; } } diff --git a/src/SixLabors.Fonts/LogicalTextLine.cs b/src/SixLabors.Fonts/LogicalTextLine.cs new file mode 100644 index 00000000..437e86e6 --- /dev/null +++ b/src/SixLabors.Fonts/LogicalTextLine.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Contains a composed logical text line and its width-independent line break opportunities. +/// +internal readonly struct LogicalTextLine +{ + /// + /// Initializes a new instance of the struct. + /// + /// The composed logical text line. + /// The collected line break opportunities. + /// The collected word-boundary segment runs. + /// The visible hyphenation markers created for soft hyphen entries. + public LogicalTextLine( + TextLine textLine, + List lineBreaks, + List wordSegments, + List hyphenationMarkers) + { + this.TextLine = textLine; + this.LineBreaks = lineBreaks; + this.WordSegments = wordSegments; + this.HyphenationMarkers = hyphenationMarkers; + } + + /// + /// Gets the composed logical text line. + /// + public TextLine TextLine { get; } + + /// + /// Gets the collected line break opportunities. + /// + public List LineBreaks { get; } + + /// + /// Gets the collected word-boundary segment runs. + /// + public List WordSegments { get; } + + /// + /// Gets the visible hyphenation markers created for soft hyphen entries. + /// + public List HyphenationMarkers { get; } +} diff --git a/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs new file mode 100644 index 00000000..edfa262d --- /dev/null +++ b/src/SixLabors.Fonts/PlaceholderGlyphMetrics.cs @@ -0,0 +1,165 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Represents synthetic glyph metrics for an atomic inline placeholder. +/// +internal sealed class PlaceholderGlyphMetrics : FontGlyphMetrics +{ + private readonly TextPlaceholder placeholder; + private readonly float pointSize; + private readonly float dpi; + + /// + /// Initializes a new instance of the class. + /// + /// The font metrics used for shared line metrics and decoration settings. + /// The placeholder dimensions and alignment settings. + /// The point size used for layout. + /// The resolution used to convert placeholder pixels into layout units. + /// The text run this placeholder belongs to. + internal PlaceholderGlyphMetrics( + StreamFontMetrics font, + TextPlaceholder placeholder, + float pointSize, + float dpi, + TextRun textRun) + : base( + font, + 0, + CodePoint.ObjectReplacementChar, + GetBounds(placeholder, pointSize, dpi, font), + ToGlyphUnits(placeholder.Width, pointSize, dpi, font.ScaleFactor), + ToGlyphUnits(placeholder.Height, pointSize, dpi, font.ScaleFactor), + 0, + 0, + font.UnitsPerEm, + Vector2.Zero, + new Vector2(font.ScaleFactor), + textRun, + GlyphType.Placeholder) + { + this.placeholder = placeholder; + this.pointSize = pointSize; + this.dpi = dpi; + } + + /// + internal override FontGlyphMetrics CloneForRendering(TextRun textRun) + => new PlaceholderGlyphMetrics( + this.FontMetrics, + this.placeholder, + this.pointSize, + this.dpi, + textRun); + + /// + internal override void RenderTo( + IGlyphRenderer renderer, + int graphemeIndex, + Vector2 glyphOrigin, + Vector2 decorationOrigin, + GlyphLayoutMode mode, + TextOptions options) + { + // Placeholders reserve layout space only; the caller owns the object rendering. + } + + /// + /// Converts the placeholder box into glyph bounds in the same synthetic font-unit space as its advances. + /// + /// The placeholder dimensions and baseline offset. + /// The point size used for layout. + /// The resolution used to convert placeholder pixels into layout units. + /// The font metrics used by glyph layout. + /// The placeholder bounds expressed in synthetic font units. + private static Bounds GetBounds(TextPlaceholder placeholder, float pointSize, float dpi, StreamFontMetrics font) + { + float scaleFactor = font.ScaleFactor; + float width = ToGlyphUnitsFloat(placeholder.Width, pointSize, dpi, scaleFactor); + float height = ToGlyphUnitsFloat(placeholder.Height, pointSize, dpi, scaleFactor); + float baselineOffset = ToGlyphUnitsFloat(placeholder.BaselineOffset, pointSize, dpi, scaleFactor); + float lineHeight = font.UnitsPerEm; + float metricsDelta = (font.HorizontalMetrics.LineHeight - lineHeight) * .5F; + float ascender = font.HorizontalMetrics.Ascender - metricsDelta; + float descender = Math.Abs(font.HorizontalMetrics.Descender) - metricsDelta; + float coreHeight = ascender + descender + (2 * metricsDelta); + float extra = lineHeight - coreHeight; + + // Top/middle/bottom align against the surrounding run font's normal + // line box, expressed relative to the text baseline in Y-up font units. + float lineTop = ascender + metricsDelta + (extra * .5F); + float lineBottom = lineTop - lineHeight; + float top = baselineOffset; + float bottom = baselineOffset - height; + + switch (placeholder.Alignment) + { + case TextPlaceholderAlignment.AboveBaseline: + top = height; + bottom = 0; + break; + + case TextPlaceholderAlignment.BelowBaseline: + top = 0; + bottom = -height; + break; + + case TextPlaceholderAlignment.Top: + top = lineTop; + bottom = top - height; + break; + + case TextPlaceholderAlignment.Bottom: + // Top, middle, and bottom align against the full line-height + // box, not just the ascender/descender band. + bottom = lineBottom; + top = bottom + height; + break; + + case TextPlaceholderAlignment.Middle: + float center = (lineTop + lineBottom) * .5F; + top = center + (height * .5F); + bottom = center - (height * .5F); + break; + + default: + top = baselineOffset; + bottom = baselineOffset - height; + break; + } + + // Placeholder bounds are authored in device pixels and converted into + // synthetic font units so the normal glyph scaling path maps them back + // to device-space size while preserving the requested baseline alignment. + return new Bounds(0, top, width, bottom); + } + + /// + /// Converts a placeholder pixel measurement into glyph units for the current layout scale. + /// + /// The placeholder measurement in pixels. + /// The point size used for layout. + /// The resolution used to convert placeholder pixels into layout units. + /// The font scale factor used by glyph layout. + /// The measurement expressed in synthetic font units. + private static ushort ToGlyphUnits(float pixels, float pointSize, float dpi, float scaleFactor) + => (ushort)MathF.Round(ToGlyphUnitsFloat(pixels, pointSize, dpi, scaleFactor)); + + /// + /// Converts a placeholder pixel measurement into fractional glyph units for bounds placement. + /// + /// The placeholder measurement in pixels. + /// The point size used for layout. + /// The resolution used to convert placeholder pixels into layout units. + /// The font scale factor used by glyph layout. + /// The measurement expressed in synthetic font units. + private static float ToGlyphUnitsFloat(float pixels, float pointSize, float dpi, float scaleFactor) + => pixels * scaleFactor / (pointSize * dpi); +} diff --git a/src/SixLabors.Fonts/PreparedTextLayoutDesign.md b/src/SixLabors.Fonts/PreparedTextLayoutDesign.md new file mode 100644 index 00000000..164b5587 --- /dev/null +++ b/src/SixLabors.Fonts/PreparedTextLayoutDesign.md @@ -0,0 +1,534 @@ +# Text Measurement and Interaction APIs + +This document describes the public measurement and selection surface for laid-out +text. The intent is that callers can measure, render, hit-test, place carets, +and draw selections without reimplementing bidi, grapheme, hard-break, or layout +mode rules outside the library. + +All positional metrics exposed by these APIs are in pixel units. + +## API Layers + +There are four layers: + +- `TextMeasurer`: one-shot convenience APIs for measuring a string. +- `TextBlock`: prepared text that can be measured or rendered repeatedly. +- `TextMetrics`: the full measurement result for one laid-out text block. +- `LineLayout`: one laid-out line with line-local measurement and interaction APIs. + +Use `TextMeasurer` for simple one-off work. Use `TextBlock` when the same text +will be measured, rendered, wrapped, or inspected more than once. + +## One-Shot Measurement + +`TextMeasurer` is the shortest path from text and options to measurements. +`TextOptions.WrappingLength` controls wrapping for these methods. + +```csharp +TextOptions options = new(font) +{ + Origin = new Vector2(20, 30), + + // TextMeasurer reads WrappingLength from TextOptions. + WrappingLength = 320 +}; + +TextMetrics metrics = TextMeasurer.Measure(text, options); +FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); +FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); +FontRectangle renderableBounds = TextMeasurer.MeasureRenderableBounds(text, options); +``` + +The aggregate rectangles answer different questions: + +- `MeasureAdvance`: the logical line-box advance of the text. +- `MeasureBounds`: the rendered glyph bounds. +- `MeasureRenderableBounds`: the union of logical advance and rendered glyph bounds. + +Use `MeasureAdvance` for layout flow. Use `MeasureBounds` for tight ink bounds. +Use `MeasureRenderableBounds` when both typographic advance and rendered glyph +overshoot must fit. + +## Prepared Measurement + +`TextBlock` prepares the wrapping-independent text work once. Pass the wrapping +length to each operation. `TextOptions.WrappingLength` is ignored by the +constructor. + +```csharp +TextBlock block = new(text, options); + +// Each operation supplies the wrapping length; the constructor does not. +TextMetrics narrow = block.Measure(240); +TextMetrics wide = block.Measure(480); + +FontRectangle narrowBounds = block.MeasureBounds(240); +FontRectangle wideBounds = block.MeasureBounds(480); +``` + +Use `-1` as the wrapping length to disable wrapping. + +```csharp +// -1 disables wrapping for TextBlock operations. +TextMetrics unwrapped = block.Measure(-1); +``` + +`TextBlock` also exposes direct detail APIs when a full `TextMetrics` object is +not needed: + +```csharp +ReadOnlyMemory lines = block.GetLineMetrics(320); +ReadOnlyMemory graphemes = block.GetGraphemeMetrics(320); +ReadOnlyMemory words = block.GetWordMetrics(320); +ReadOnlyMemory glyphs = block.GetGlyphMetrics(320); +``` + +Method-returned measurement collections use `ReadOnlyMemory` because they are +snapshots that callers may store with their own layout state. Owner-backed +properties, such as `TextMetrics.LineMetrics` and `LineLayout.GraphemeMetrics`, +use `ReadOnlySpan` because the owner object already controls the lifetime. + +## TextMetrics + +`TextMetrics` is the result to keep when callers need several measurements from +the same laid-out text. + +```csharp +TextMetrics metrics = TextMeasurer.Measure(text, options); + +// These aggregate measurements answer different layout and rendering questions. +FontRectangle advance = metrics.Advance; +FontRectangle bounds = metrics.Bounds; +FontRectangle renderableBounds = metrics.RenderableBounds; +int lineCount = metrics.LineCount; + +ReadOnlySpan lines = metrics.LineMetrics; +ReadOnlySpan graphemes = metrics.GraphemeMetrics; +ReadOnlySpan words = metrics.WordMetrics; +``` + +The line and grapheme collections are in final layout order. That matters for +bidi text and reverse line-order layout modes: source order and visual order can +be different. + +`WordMetrics` are in source order because word-boundary navigation is a logical +text operation. Selection and caret APIs convert those logical metrics back into +visual geometry when needed. + +## Line Metrics + +`LineMetrics` describes one laid-out line. + +```csharp +foreach (LineMetrics line in metrics.LineMetrics) +{ + // Start and Extent describe the positioned line box. + Vector2 start = line.Start; + Vector2 extent = line.Extent; + float baseline = line.Baseline; +} +``` + +`Start` and `Extent` describe the positioned line box in pixel units. Selection +and caret APIs use the line box for the cross-axis size, which matches normal +text editor and browser behavior: selecting mixed font sizes on the same line +paints a consistent line-height rectangle rather than one rectangle per glyph +height. + +`StringIndex`, `GraphemeIndex`, and `GraphemeCount` describe the source text +range owned by the line. `GraphemeCount` is not a glyph count. + +## Grapheme Metrics + +Use `GraphemeMetrics` for text interaction: hit testing, caret positioning, +range selection, and UI overlays. + +```csharp +foreach (GraphemeMetrics grapheme in metrics.GraphemeMetrics) +{ + // Use Advance for interaction and Bounds for rendered ink. + FontRectangle advance = grapheme.Advance; + FontRectangle bounds = grapheme.Bounds; + FontRectangle renderableBounds = grapheme.RenderableBounds; + bool isLineBreak = grapheme.IsLineBreak; +} +``` + +The rectangles answer different questions: + +- `Advance`: the positioned logical advance rectangle for the grapheme. +- `Bounds`: the rendered glyph bounds for the grapheme. +- `RenderableBounds`: the union of advance and rendered glyph bounds. + +Use `Advance` for hit targets, carets, and selection geometry. Ink bounds can be +empty, overhang the advance, or exclude whitespace, so they are not a reliable +interaction target. + +`IsLineBreak` identifies hard-break graphemes that remain in the laid-out +metrics. Hard breaks at the end of non-empty lines are trimmed with other +trailing breaking whitespace; hard breaks that own blank lines remain because +they provide the line geometry for selection and caret behavior. + +## Word Metrics + +`WordMetrics` describes one Unicode word-boundary segment from UAX #29. + +```csharp +foreach (WordMetrics word in metrics.WordMetrics) +{ + FontRectangle advance = word.Advance; + FontRectangle bounds = word.Bounds; + FontRectangle renderableBounds = word.RenderableBounds; + int graphemeStart = word.GraphemeStart; + int graphemeEnd = word.GraphemeEnd; + int stringStart = word.StringStart; + int stringEnd = word.StringEnd; +} +``` + +`Advance`, `Bounds`, and `RenderableBounds` have the same meanings as the +equivalent `GraphemeMetrics` rectangles, but accumulated across the +word-boundary segment. Whitespace segments keep their positioned bounds; they +are not discarded just because they are separators. + +All `Start` values on `WordMetrics` are inclusive. All `End` values are exclusive. +`GraphemeStart` and `GraphemeEnd` are grapheme insertion indices. `StringStart` +and `StringEnd` are UTF-16 indices into the original text. + +Unicode word-boundary segments include separators. For example, `can't stop` +contains three segments: + +```text +can't +[space] +stop +``` + +This keeps the raw API aligned with the Unicode standard. Higher-level editor +commands can choose whether to stop on separator boundaries or skip over them. + +## Glyph Metrics + +Glyph detail APIs expose laid-out glyph entries. + +```csharp +ReadOnlyMemory glyphs = metrics.GetGlyphMetrics(); + +foreach (GlyphMetrics glyph in glyphs.Span) +{ + FontRectangle advance = glyph.Advance; + FontRectangle bounds = glyph.Bounds; + FontRectangle renderableBounds = glyph.RenderableBounds; + CodePoint codePoint = glyph.CodePoint; +} +``` + +Use glyph detail for rendering diagnostics, glyph-level visualization, or +advanced inspection. Do not use glyph entries as character or caret positions: +ligatures, decomposition, fallback, emoji, and combining marks mean one +grapheme can map to multiple glyph entries, and multiple source characters can +map to one visual glyph sequence. + +## Per-Line Layout + +`TextBlock.GetLineLayouts` returns line objects when callers want line-local +inspection or interaction. + +```csharp +TextBlock block = new(text, options); +ReadOnlyMemory layout = block.GetLineLayouts(320); + +foreach (LineLayout line in layout.Span) +{ + // LineLayout exposes the slice of grapheme metrics owned by this line. + LineMetrics lineMetrics = line.LineMetrics; + ReadOnlySpan lineGraphemes = line.GraphemeMetrics; +} +``` + +`LineLayout` mirrors the interaction and glyph-detail surface for a single line: + +```csharp +TextHit hit = line.HitTest(point); + +// Passing the hit keeps trailing-edge and bidi handling inside the library. +CaretPosition caret = line.GetCaretPosition(hit); +CaretPosition next = line.MoveCaret(caret, CaretMovement.Next); +WordMetrics word = line.GetWordMetrics(hit); +ReadOnlyMemory selection = line.GetSelectionBounds(caret, next); +ReadOnlyMemory wordSelection = line.GetSelectionBounds(word); +ReadOnlyMemory glyphs = line.GetGlyphMetrics(); +``` + +Use the full `TextMetrics` interaction methods for selections that can cross +line boundaries. Use `LineLayout` when the caller already knows interaction is +line-local. + +## Hit Testing + +Hit testing maps a point to the nearest grapheme and side. + +```csharp +TextHit hit = metrics.HitTest(mousePosition); + +int lineIndex = hit.LineIndex; +int graphemeIndex = hit.GraphemeIndex; +// Use this value for carets and selection endpoints. +int insertionIndex = hit.GraphemeInsertionIndex; +``` + +`GraphemeIndex` identifies the hit grapheme. `GraphemeInsertionIndex` identifies +the logical caret position represented by the hit. For left-to-right text, the +trailing side is usually `GraphemeIndex + 1`. For right-to-left text, the +physical side is reversed, but callers do not need to apply that rule. Use +`GraphemeInsertionIndex` or pass the `TextHit` directly to caret and selection +APIs. + +For word selection, pass the hit directly to `GetWordMetrics`. This uses the +grapheme that was hit, so clicking the trailing side of the final grapheme in a +word still selects that word rather than the following separator segment. + +```csharp +TextHit hit = metrics.HitTest(mousePosition); +WordMetrics word = metrics.GetWordMetrics(hit); +ReadOnlyMemory selection = metrics.GetSelectionBounds(word); +``` + +## Caret Positioning + +Caret APIs return positioned caret lines in pixel units. A caret is also the +navigation token for keyboard/editor interaction. + +```csharp +TextHit hit = metrics.HitTest(mousePosition); + +// The hit overload applies the correct grapheme insertion index. +CaretPosition caret = metrics.GetCaretPosition(hit); + +DrawCaret(caret.Start, caret.End); + +if (caret.HasSecondary) +{ + DrawSecondaryCaret(caret.SecondaryStart, caret.SecondaryEnd); +} +``` + +Use absolute placement when initializing a keyboard caret without a pointer hit. + +```csharp +CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); +``` + +At bidi boundaries, one logical insertion position can have two visual edges. +`CaretPosition` exposes the secondary edge so editor-style callers can choose how +to present or navigate that boundary without recomputing bidi affinity. + +## Caret Movement + +`MoveCaret` applies editor-style movement to a caret and returns the new caret. + +```csharp +CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); + +// Previous and Next move through logical grapheme insertion positions. +caret = metrics.MoveCaret(caret, CaretMovement.Next); + +// PreviousWord and NextWord move through Unicode word boundaries. +caret = metrics.MoveCaret(caret, CaretMovement.NextWord); + +// LineStart and LineEnd are the Home/End-style line movement operations. +caret = metrics.MoveCaret(caret, CaretMovement.LineEnd); + +// TextStart and TextEnd are the whole-block equivalents. +caret = metrics.MoveCaret(caret, CaretMovement.TextStart); +``` + +`LineUp` and `LineDown` move to adjacent visual lines while preserving the +caret's requested position on the line. + +```csharp +CaretPosition firstLineEnd = metrics.GetCaret(CaretPlacement.Start); +firstLineEnd = metrics.MoveCaret(firstLineEnd, CaretMovement.LineEnd); + +// Repeated LineDown keeps the original line position even when an intermediate +// line is shorter and the visible caret has to clamp to that line's end. +CaretPosition middleLine = metrics.MoveCaret(firstLineEnd, CaretMovement.LineDown); +CaretPosition finalLine = metrics.MoveCaret(middleLine, CaretMovement.LineDown); +``` + +This preserves normal rich-text editor behavior: moving down through a short line +does not permanently lose the user's original horizontal or vertical line +position. + +## Selection Bounds + +Selection APIs return rectangles in visual order and pixel units. The result is +`ReadOnlyMemory` so callers can store it with selection state and +use `.Span` when drawing. + +For pointer selection, use the hit overload. This keeps bidi and trailing-edge +logic inside the library. + +```csharp +TextHit anchor = metrics.HitTest(mouseDown); +TextHit focus = metrics.HitTest(mouseMove); + +// The hit overload converts both endpoints to logical insertion indices. +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); + +foreach (FontRectangle rectangle in selection.Span) +{ + FillSelectionRectangle(rectangle); +} +``` + +For keyboard selection, keep an anchor caret and move the focus caret. + +```csharp +CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); +CaretPosition focus = anchor; + +// Shift+Right-style behavior updates only the focus caret. +focus = metrics.MoveCaret(focus, CaretMovement.Next); + +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); +``` + +For word selection, use the word metrics overload. + +```csharp +TextHit hit = metrics.HitTest(doubleClickPosition); +WordMetrics word = metrics.GetWordMetrics(hit); + +ReadOnlyMemory selection = metrics.GetSelectionBounds(word); +``` + +Do not sort, union, or merge the returned rectangles unless the UI explicitly +wants a different visual. A single logical selection can be visually +discontinuous inside one line when it crosses bidi runs. Returning multiple +rectangles allows browser-style selection where the unselected visual gap stays +unpainted. + +## Bidi Drag Selection + +Consider a line whose source text is: + +```text +Tall שלום عرب +``` + +In a left-to-right paragraph, the right-to-left run can paint with Arabic before +Hebrew. When a user drags from the left edge of `Tall` toward the Hebrew word, +the selection can become visually split: + +```text +[Tall ] عرب [שלום] +``` + +Application code should not manually decide which physical edge of the Hebrew +glyph means "before" or "after". The correct flow is: + +```csharp +TextHit anchor = metrics.HitTest(mouseDown); +TextHit focus = metrics.HitTest(mouseMove); + +// Bidi split selection is represented by the returned rectangle list. +ReadOnlyMemory rectangles = metrics.GetSelectionBounds(anchor, focus); +``` + +The hit-test result carries the logical insertion index. The selection result is +already split into the visual rectangles that should be painted. + +## Hard Line Breaks + +Hard line breaks that end non-empty lines are trimmed with trailing breaking +whitespace. Hard line breaks that own blank lines remain as graphemes for source +ranges, hit testing, caret movement, and selection painting. + +For text with two hard breaks in the middle: + +```text +Tall عرب שלום + +Small مرحبا שלום +``` + +Full selection should paint three visual rows: the first text line, the blank +line, and the second text line. The line break that ends a non-empty line should +not add a separate painted box; the line break that owns the blank line should. + +Consumers should not special-case this. Draw the rectangles returned by +`GetSelectionBounds`. Consumers that inspect individual graphemes can use +`IsLineBreak` to identify the blank-line hard breaks that remain in the metrics. + +## Recommended Workflows + +For one-off measuring: + +```csharp +// One-shot path for a single layout result. +TextMetrics metrics = TextMeasurer.Measure(text, options); +``` + +For repeated wrapping or rendering: + +```csharp +TextBlock block = new(text, options); + +// Reuse the prepared text for each requested wrapping length. +TextMetrics narrow = block.Measure(240); +TextMetrics wide = block.Measure(480); +block.RenderTo(renderer, 480); +``` + +For text editor interaction: + +```csharp +TextMetrics metrics = block.Measure(wrappingLength); + +TextHit anchor = metrics.HitTest(mouseDown); +TextHit focus = metrics.HitTest(mouseMove); + +// Use hit-based overloads so interaction follows the laid-out bidi result. +CaretPosition caret = metrics.GetCaretPosition(focus); +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, focus); +``` + +For keyboard navigation and selection: + +```csharp +TextMetrics metrics = block.Measure(wrappingLength); +CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); +CaretPosition anchor = caret; + +// The movement operation owns grapheme, line, and hard-break navigation rules. +caret = metrics.MoveCaret(caret, CaretMovement.LineDown); +caret = metrics.MoveCaret(caret, CaretMovement.NextWord); +ReadOnlyMemory selection = metrics.GetSelectionBounds(anchor, caret); +``` + +For per-line UI: + +```csharp +ReadOnlyMemory lines = block.GetLineLayouts(wrappingLength); + +foreach (LineLayout line in lines.Span) +{ + ReadOnlySpan graphemes = line.GraphemeMetrics; + ReadOnlyMemory glyphs = line.GetGlyphMetrics(); +} +``` + +## Design Principles + +- The library owns bidi, grapheme, hard-break, wrapping, and layout-mode rules. +- Callers should pass points, hits, or logical ranges and draw the returned geometry. +- Caret movement should flow through `MoveCaret`, not caller-side grapheme arithmetic. +- Word selection should flow through `GetWordMetrics`, not caller-side Unicode boundary logic. +- Grapheme metrics are the text interaction unit. +- Word metrics describe logical source segments and their positioned geometry; + selection bounds are the visual geometry. +- Glyph metrics are rendering-detail data, not caret or character data. +- Selection rectangles are visual geometry, not a single logical union. +- Per-line selection uses line-box height so selection remains visually stable + across mixed fonts and font sizes. diff --git a/src/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs b/src/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs index e55e2f3a..ec3376b5 100644 --- a/src/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs +++ b/src/SixLabors.Fonts/Rendering/GlyphRendererParameters.cs @@ -15,7 +15,7 @@ namespace SixLabors.Fonts.Rendering; public readonly struct GlyphRendererParameters : IEquatable { internal GlyphRendererParameters( - GlyphMetrics metrics, + FontGlyphMetrics metrics, TextRun textRun, float pointSize, float dpi, @@ -60,7 +60,7 @@ internal GlyphRendererParameters( public ushort CompositeGlyphId { get; } /// - /// Gets the index of the grapheme this glyph belongs to. + /// Gets the zero-based grapheme index in the original text. /// public int GraphemeIndex { get; } diff --git a/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs b/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs index 8bc7d343..15acfaec 100644 --- a/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Rendering/PaintedGlyphMetrics.cs @@ -11,7 +11,7 @@ namespace SixLabors.Fonts.Rendering; /// Geometry and paints are supplied in document-space by an interpreter; all layout transforms /// (UPEM mapping, DPI/point-size scaling, rotation, final placement) are applied here. /// -public sealed class PaintedGlyphMetrics : GlyphMetrics +public sealed class PaintedGlyphMetrics : FontGlyphMetrics { private readonly IPaintedGlyphSource source; @@ -92,7 +92,7 @@ internal PaintedGlyphMetrics( => this.source = source; /// - internal override GlyphMetrics CloneForRendering(TextRun textRun) + internal override FontGlyphMetrics CloneForRendering(TextRun textRun) => new PaintedGlyphMetrics( this.FontMetrics, this.GlyphId, @@ -112,8 +112,8 @@ internal override GlyphMetrics CloneForRendering(TextRun textRun) internal override void RenderTo( IGlyphRenderer renderer, int graphemeIndex, - Vector2 location, - Vector2 offset, + Vector2 glyphOrigin, + Vector2 decorationOrigin, GlyphLayoutMode mode, TextOptions options) { @@ -126,9 +126,8 @@ internal override void RenderTo( float dpi = options.Dpi; // Device-space placement. - location *= dpi; - offset *= dpi; - Vector2 renderLocation = location + offset; + glyphOrigin *= dpi; + decorationOrigin *= dpi; float scaledPpem = this.GetScaledSize(pointSize, dpi); Vector2 scale = new Vector2(scaledPpem) / this.ScaleFactor; // uniform @@ -138,10 +137,10 @@ internal override void RenderTo( // Layout similarity: uniform scale then rotation; translation added below. Matrix3x2 layout = Matrix3x2.CreateScale(scale); layout *= rotation; - layout.Translation = (this.Offset * scale) + renderLocation; + layout.Translation = (this.Offset * scale) + glyphOrigin; // Bounds in device space for BeginGlyph. - FontRectangle box = this.GetBoundingBox(mode, renderLocation, scaledPpem); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPpem); GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); if (renderer.BeginGlyph(in box, in parameters)) @@ -160,7 +159,7 @@ internal override void RenderTo( } renderer.EndGlyph(); - this.RenderDecorationsTo(renderer, location, mode, rotation, scaledPpem, options); + this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPpem, options); } } diff --git a/src/SixLabors.Fonts/Rendering/TextRenderer.cs b/src/SixLabors.Fonts/Rendering/TextRenderer.cs index 2b78466c..01ff5a85 100644 --- a/src/SixLabors.Fonts/Rendering/TextRenderer.cs +++ b/src/SixLabors.Fonts/Rendering/TextRenderer.cs @@ -4,7 +4,7 @@ namespace SixLabors.Fonts.Rendering; /// -/// Encapsulated logic for laying out and then rendering text to a surface. +/// Encapsulates logic for laying out and then rendering text to a surface. /// public class TextRenderer { @@ -21,7 +21,7 @@ public class TextRenderer /// /// The target renderer. /// The text to render. - /// The text options. + /// The text options. controls wrapping; use -1 to disable wrapping. public static void RenderTextTo(IGlyphRenderer renderer, ReadOnlySpan text, TextOptions options) => new TextRenderer(renderer).RenderText(text, options); @@ -30,35 +30,26 @@ public static void RenderTextTo(IGlyphRenderer renderer, ReadOnlySpan text /// /// The target renderer. /// The text to render. - /// The text option. + /// The text options. controls wrapping; use -1 to disable wrapping. public static void RenderTextTo(IGlyphRenderer renderer, string text, TextOptions options) => new TextRenderer(renderer).RenderText(text, options); /// - /// Renders the text to the default renderer. + /// Renders the text to the configured renderer. /// /// The text to render. - /// The text options. + /// The text options. controls wrapping; use -1 to disable wrapping. public void RenderText(string text, TextOptions options) => this.RenderText(text.AsSpan(), options); /// - /// Renders the text to the default renderer. + /// Renders the text to the configured renderer. /// /// The text to render. - /// The style. + /// The text options. controls wrapping; use -1 to disable wrapping. public void RenderText(ReadOnlySpan text, TextOptions options) { - IReadOnlyList glyphsToRender = TextLayout.GenerateLayout(text, options); - FontRectangle rect = TextMeasurer.GetBounds(glyphsToRender, options.Dpi); - - this.renderer.BeginText(in rect); - - foreach (GlyphLayout g in glyphsToRender) - { - g.Glyph.RenderTo(this.renderer, g.GraphemeIndex, g.PenLocation, g.Offset, g.LayoutMode, options); - } - - this.renderer.EndText(); + TextBlock block = new(text, options); + block.RenderTo(this.renderer, options.WrappingLength); } } diff --git a/src/SixLabors.Fonts/ShapedText.cs b/src/SixLabors.Fonts/ShapedText.cs new file mode 100644 index 00000000..b91307d0 --- /dev/null +++ b/src/SixLabors.Fonts/ShapedText.cs @@ -0,0 +1,51 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Contains the width-independent result of shaping text before logical line composition. +/// +internal readonly struct ShapedText +{ + /// + /// Initializes a new instance of the struct. + /// + /// The positioned glyph shaping collection. + /// The resolved bidi runs covering the shaped text. + /// The code point to bidi-run mapping built during shaping. + /// The layout mode used while shaping. + public ShapedText( + GlyphPositioningCollection positionings, + BidiRun[] bidiRuns, + Dictionary bidiMap, + LayoutMode layoutMode) + { + this.Positionings = positionings; + this.BidiRuns = bidiRuns; + this.BidiMap = bidiMap; + this.LayoutMode = layoutMode; + } + + /// + /// Gets the positioned glyph shaping collection. + /// + public GlyphPositioningCollection Positionings { get; } + + /// + /// Gets the resolved bidi runs covering the shaped text. + /// + public BidiRun[] BidiRuns { get; } + + /// + /// Gets the code point to bidi-run mapping built during shaping. + /// + public Dictionary BidiMap { get; } + + /// + /// Gets the layout mode used while shaping. + /// + public LayoutMode LayoutMode { get; } +} diff --git a/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs b/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs index 4a0a6d13..7c0f0bf5 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.Cff.cs @@ -97,7 +97,7 @@ private static StreamFontMetrics LoadCompactFont(FontReader reader) return new StreamFontMetrics(tables, glyphVariationProcessor); } - private GlyphMetrics CreateCffGlyphMetrics( + private FontGlyphMetrics CreateCffGlyphMetrics( in CodePoint codePoint, ushort glyphId, GlyphType glyphType, diff --git a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs index 26b57fae..b29ad190 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.TrueType.cs @@ -48,7 +48,7 @@ private TrueTypeInterpreter CreateInterpreter() return interpreter; } - internal void ApplyTrueTypeHinting(HintingMode hintingMode, GlyphMetrics metrics, ref GlyphVector glyphVector, Vector2 scaleXY, float pixelSize) + internal void ApplyTrueTypeHinting(HintingMode hintingMode, FontGlyphMetrics metrics, ref GlyphVector glyphVector, Vector2 scaleXY, float pixelSize) { if (hintingMode == HintingMode.None || this.outlineType != OutlineType.TrueType) { @@ -181,7 +181,7 @@ private static StreamFontMetrics LoadTrueTypeFont(FontReader reader) return new StreamFontMetrics(tables, glyphVariationProcessor); } - private GlyphMetrics CreateTrueTypeGlyphMetrics( + private FontGlyphMetrics CreateTrueTypeGlyphMetrics( in CodePoint codePoint, ushort glyphId, GlyphType glyphType, diff --git a/src/SixLabors.Fonts/StreamFontMetrics.cs b/src/SixLabors.Fonts/StreamFontMetrics.cs index 1642e178..29dc9d05 100644 --- a/src/SixLabors.Fonts/StreamFontMetrics.cs +++ b/src/SixLabors.Fonts/StreamFontMetrics.cs @@ -32,7 +32,7 @@ internal partial class StreamFontMetrics : FontMetrics private readonly OutlineType outlineType; // https://docs.microsoft.com/en-us/typography/opentype/spec/otff#font-tables - private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, ColorFontSupport ColorSupport, bool IsVerticalLayout), GlyphMetrics> glyphCache; + private readonly ConcurrentDictionary<(int CodePoint, ushort Id, TextAttributes Attributes, ColorFontSupport ColorSupport, bool IsVerticalLayout), FontGlyphMetrics> glyphCache; private readonly ConcurrentDictionary<(int CodePoint, int NextCodePoint), (bool Success, ushort GlyphId, bool SkipNextCodePoint)> glyphIdCache; private readonly ConcurrentDictionary codePointCache; private SvgGlyphSource? svgGlyphSource; @@ -277,23 +277,23 @@ internal override bool TryGetMarkAttachmentClass(ushort glyphId, [NotNullWhen(tr } /// - public override bool TryGetVariationAxes(out VariationAxis[]? variationAxes) + public override bool TryGetVariationAxes(out ReadOnlyMemory variationAxes) { FVarTable? fvar = this.trueTypeFontTables?.Fvar ?? this.compactFontTables?.FVar; Tables.General.Name.NameTable? names = this.trueTypeFontTables?.Name ?? this.compactFontTables?.Name; if (fvar == null) { - variationAxes = []; + variationAxes = ReadOnlyMemory.Empty; return false; } - variationAxes = new VariationAxis[fvar.Axes.Length]; + VariationAxis[] axes = new VariationAxis[fvar.Axes.Length]; for (int i = 0; i < fvar.Axes.Length; i++) { VariationAxisRecord axis = fvar.Axes[i]; string name = names != null ? names.GetNameById(CultureInfo.InvariantCulture, axis.AxisNameId) : string.Empty; - variationAxes[i] = new VariationAxis() + axes[i] = new VariationAxis() { Tag = axis.Tag, Min = axis.MinValue, @@ -303,6 +303,7 @@ public override bool TryGetVariationAxes(out VariationAxis[]? variationAxes) }; } + variationAxes = axes; return true; } @@ -323,7 +324,7 @@ public override bool TryGetGlyphMetrics( TextDecorations textDecorations, LayoutMode layoutMode, ColorFontSupport support, - [NotNullWhen(true)] out GlyphMetrics? metrics) + [NotNullWhen(true)] out FontGlyphMetrics? metrics) { // We return metrics for the special glyph representing a missing character, commonly known as .notdef. this.TryGetGlyphId(codePoint, out ushort glyphId); @@ -332,7 +333,7 @@ public override bool TryGetGlyphMetrics( } /// - internal override GlyphMetrics GetGlyphMetrics( + internal override FontGlyphMetrics GetGlyphMetrics( CodePoint codePoint, ushort glyphId, TextAttributes textAttributes, @@ -356,7 +357,7 @@ internal override GlyphMetrics GetGlyphMetrics( (textDecorations, codePoint, this)); /// - public override IReadOnlyList GetAvailableCodePoints() + public override ReadOnlyMemory GetAvailableCodePoints() { CMapTable cmap = this.outlineType == OutlineType.TrueType ? this.trueTypeFontTables!.Cmap @@ -780,8 +781,8 @@ private void ApplyMVarDeltas(HorizontalMetrics horizontalMetrics, VerticalMetric /// Reads a from the specified stream. /// /// The file path. - /// a . - public static StreamFontMetrics[] LoadFontCollection(string path) + /// A read-only memory region containing the font metrics. + public static ReadOnlyMemory LoadFontCollection(string path) { using FileStream fs = File.OpenRead(path); return LoadFontCollection(fs); @@ -791,8 +792,8 @@ public static StreamFontMetrics[] LoadFontCollection(string path) /// Reads a from the specified stream. /// /// The stream. - /// a . - public static StreamFontMetrics[] LoadFontCollection(Stream stream) + /// A read-only memory region containing the font metrics. + public static ReadOnlyMemory LoadFontCollection(Stream stream) { long startPos = stream.Position; BigEndianBinaryReader reader = new(stream, true); @@ -816,7 +817,7 @@ private static (int CodePoint, ushort Id, TextAttributes Attributes, ColorFontSu LayoutMode layoutMode) => (codePoint.Value, glyphId, textAttributes, colorSupport, AdvancedTypographicUtils.IsVerticalGlyph(codePoint, layoutMode)); - private GlyphMetrics CreateGlyphMetrics( + private FontGlyphMetrics CreateGlyphMetrics( in CodePoint codePoint, ushort glyphId, GlyphType glyphType, diff --git a/src/SixLabors.Fonts/SystemFontCollection.cs b/src/SixLabors.Fonts/SystemFontCollection.cs index ac54d28e..65bb26fd 100644 --- a/src/SixLabors.Fonts/SystemFontCollection.cs +++ b/src/SixLabors.Fonts/SystemFontCollection.cs @@ -135,7 +135,7 @@ IEnumerable IReadOnlyFontMetricsCollection.GetAllMetrics(string nam => ((IReadOnlyFontMetricsCollection)this.collection).GetAllMetrics(name, culture); /// - IEnumerable IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) + ReadOnlyMemory IReadOnlyFontMetricsCollection.GetAllStyles(string name, CultureInfo culture) => ((IReadOnlyFontMetricsCollection)this.collection).GetAllStyles(name, culture); /// diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs index 1ea3f256..d10b49f4 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/AnchorTable.cs @@ -160,7 +160,7 @@ public override AnchorXY GetAnchor(FontMetrics fontMetrics, GlyphShapingData dat TextDecorations textDecorations = data.TextRun.TextDecorations; LayoutMode layoutMode = collection.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; - if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out GlyphMetrics? metrics)) + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics)) { if (metrics is TrueTypeGlyphMetrics ttmetric) { diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs index f6fc7a2a..38f02f16 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPos/LookupType3SubTable.cs @@ -185,30 +185,19 @@ public override bool TryUpdatePosition( } else { - // Vertical : Top to bottom - if (current.Direction == TextDirection.LeftToRight) - { - current.Bounds.Height = exitXY.YCoordinate + current.Bounds.Y; + // Vertical layout modes advance top-to-bottom; column progression is handled by layout. + current.Bounds.Height = exitXY.YCoordinate + current.Bounds.Y; - int delta = entryXY.YCoordinate + next.Bounds.Y; - next.Bounds.Height -= delta; - next.Bounds.Y -= delta; - } - else - { - int delta = exitXY.YCoordinate + current.Bounds.Y; - current.Bounds.Height -= delta; - current.Bounds.Y -= delta; - - next.Bounds.Height = entryXY.YCoordinate + next.Bounds.Y; - } + int delta = entryXY.YCoordinate + next.Bounds.Y; + next.Bounds.Height -= delta; + next.Bounds.Y -= delta; } int child = index; int parent = nextIndex; int xOffset = entryXY.XCoordinate - exitXY.XCoordinate; int yOffset = entryXY.YCoordinate - exitXY.YCoordinate; - if ((this.LookupFlags & LookupFlags.RightToLeft) == LookupFlags.RightToLeft) + if ((this.LookupFlags & LookupFlags.RightToLeft) != LookupFlags.RightToLeft) { (parent, child) = (child, parent); @@ -235,10 +224,22 @@ public override bool TryUpdatePosition( } // If parent was attached to child, separate them. + // https://github.com/harfbuzz/harfbuzz/issues/2469 GlyphShapingData p = collection[parent]; if (p.CursiveAttachment == -c.CursiveAttachment) { p.CursiveAttachment = 0; + + // Bounds.X/Y carry shaping placement offsets here, matching + // HarfBuzz x_offset/y_offset. Clear only the detached parent's minor axis. + if (horizontal) + { + p.Bounds.Y = 0; + } + else + { + p.Bounds.X = 0; + } } return true; diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs index 7aefb848..d2241c8d 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/GPosTable.cs @@ -460,7 +460,7 @@ private static void FixCursiveAttachment(GlyphPositioningCollection collection, if (data.CursiveAttachment != -1) { int j = data.CursiveAttachment + currentIndex; - if (j > count) + if (j < index || j >= index + count) { return; } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs index 999661e3..b447ba2b 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/ArabicShaper.cs @@ -112,6 +112,17 @@ protected override void PlanFeatures(IGlyphShapingCollection collection, int ind this.AddFeature(collection, index, count, MediTag, false); this.AddFeature(collection, index, count, Med2Tag, false); this.AddFeature(collection, index, count, InitTag, false); + + // HarfBuzz plans these as Arabic-script features, independently of the + // generic horizontal feature list. Horizontal runs already get them from + // DefaultShaper; forced vertical Arabic needs them here as well. + if (collection.TextOptions.LayoutMode.IsVertical()) + { + this.AddFeature(collection, index, count, CaltTag); + this.AddFeature(collection, index, count, LigaTag); + this.AddFeature(collection, index, count, CligTag); + } + this.AddFeature(collection, index, count, MsetTag); } diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs index 82b0a4df..6194f4b2 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/DefaultShaper.cs @@ -81,7 +81,7 @@ internal class DefaultShaper : BaseShaper private static readonly CodePoint Slash = new(0x002F); /// The set of shaping stages accumulated during feature planning. - private readonly HashSet shapingStages = new(); + private readonly HashSet shapingStages = []; /// The kerning mode from the text options. private readonly KerningMode kerningMode; @@ -125,7 +125,7 @@ protected override void PlanPreprocessingFeatures(IGlyphShapingCollection collec this.AddFeature(collection, index, count, RvnrTag); // Add directional features. - for (int i = index; i < count; i++) + for (int i = index; i < index + count; i++) { GlyphShapingData shapingData = collection[i]; @@ -157,7 +157,7 @@ protected override void PlanPostprocessingFeatures(IGlyphShapingCollection colle LayoutMode layoutMode = collection.TextOptions.LayoutMode; bool isVerticalLayout = false; - for (int i = index; i < count; i++) + for (int i = index; i < index + count; i++) { GlyphShapingData shapingData = collection[i]; isVerticalLayout |= AdvancedTypographicUtils.IsVerticalGlyph(shapingData.CodePoint, layoutMode); diff --git a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs index 41443077..c6453db0 100644 --- a/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs +++ b/src/SixLabors.Fonts/Tables/AdvancedTypographic/Shapers/HangulShaper.cs @@ -447,7 +447,7 @@ private void ReOrderToneMark(GlyphSubstitutionCollection collection, GlyphShapin TextDecorations textDecorations = data.TextRun.TextDecorations; LayoutMode layoutMode = collection.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; - if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out GlyphMetrics? metrics) + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics) && metrics.AdvanceWidth == 0) { return; @@ -477,7 +477,7 @@ private int InsertDottedCircle(GlyphSubstitutionCollection collection, GlyphShap TextDecorations textDecorations = data.TextRun.TextDecorations; LayoutMode layoutMode = collection.TextOptions.LayoutMode; ColorFontSupport colorFontSupport = collection.TextOptions.ColorFontSupport; - if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out GlyphMetrics? metrics) + if (fontMetrics.TryGetGlyphMetrics(data.CodePoint, textAttributes, textDecorations, layoutMode, colorFontSupport, out FontGlyphMetrics? metrics) && metrics.AdvanceWidth != 0) { after = true; diff --git a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs index f4ca7ff3..2b7e6898 100644 --- a/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/Cff/CffGlyphMetrics.cs @@ -10,7 +10,7 @@ namespace SixLabors.Fonts.Tables.Cff; /// /// Represents a glyph metric from a particular Compact Font Face. /// -internal class CffGlyphMetrics : GlyphMetrics +internal class CffGlyphMetrics : FontGlyphMetrics { private CffGlyphData glyphData; @@ -108,7 +108,7 @@ internal CffGlyphMetrics( => this.glyphData = glyphData; /// - internal override GlyphMetrics CloneForRendering(TextRun textRun) + internal override FontGlyphMetrics CloneForRendering(TextRun textRun) => new CffGlyphMetrics( this.FontMetrics, this.GlyphId, @@ -129,8 +129,8 @@ internal override GlyphMetrics CloneForRendering(TextRun textRun) internal override void RenderTo( IGlyphRenderer renderer, int graphemeIndex, - Vector2 location, - Vector2 offset, + Vector2 glyphOrigin, + Vector2 decorationOrigin, GlyphLayoutMode mode, TextOptions options) { @@ -143,16 +143,12 @@ internal override void RenderTo( float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; float dpi = options.Dpi; - // The glyph vector is rendered offset to the location. - // For horizontal text, the offset is always zero but vertical or rotated text - // will be offset against the location. - location *= dpi; - offset *= dpi; - Vector2 renderLocation = location + offset; + glyphOrigin *= dpi; + decorationOrigin *= dpi; float scaledPPEM = this.GetScaledSize(pointSize, dpi); Matrix3x2 rotation = GetRotationMatrix(mode); - FontRectangle box = this.GetBoundingBox(mode, renderLocation, scaledPPEM); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); if (renderer.BeginGlyph(in box, in parameters)) @@ -171,11 +167,11 @@ internal override void RenderTo( } Vector2 scaledOffset = this.Offset * scale; - this.glyphData.RenderTo(renderer, renderLocation, scale, scaledOffset, rotation); + this.glyphData.RenderTo(renderer, glyphOrigin, scale, scaledOffset, rotation); } renderer.EndGlyph(); - this.RenderDecorationsTo(renderer, location, mode, rotation, scaledPPEM, options); + this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPPEM, options); } } } diff --git a/src/SixLabors.Fonts/Tables/General/CMapTable.cs b/src/SixLabors.Fonts/Tables/General/CMapTable.cs index 9dc3ce6f..a1713f0c 100644 --- a/src/SixLabors.Fonts/Tables/General/CMapTable.cs +++ b/src/SixLabors.Fonts/Tables/General/CMapTable.cs @@ -142,8 +142,8 @@ public bool TryGetCodePoint(ushort glyphId, out CodePoint codePoint) /// /// Gets the unicode codepoints for which a glyph exists in the font. /// - /// The . - public IReadOnlyList GetAvailableCodePoints() + /// A read-only memory region containing the available codepoints. + public ReadOnlyMemory GetAvailableCodePoints() { if (this.codepoints is not null) { diff --git a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs index 15412eb7..257779c3 100644 --- a/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs +++ b/src/SixLabors.Fonts/Tables/TrueType/TrueTypeGlyphMetrics.cs @@ -12,7 +12,7 @@ namespace SixLabors.Fonts.Tables.TrueType; /// /// Represents a glyph metric from a particular TrueType font face. /// -public partial class TrueTypeGlyphMetrics : GlyphMetrics +public partial class TrueTypeGlyphMetrics : FontGlyphMetrics { private static readonly Vector2 YInverter = new(1, -1); private readonly GlyphVector vector; @@ -109,7 +109,7 @@ internal TrueTypeGlyphMetrics( => this.vector = vector; /// - internal override GlyphMetrics CloneForRendering(TextRun textRun) + internal override FontGlyphMetrics CloneForRendering(TextRun textRun) => new TrueTypeGlyphMetrics( this.FontMetrics, this.GlyphId, @@ -135,8 +135,8 @@ internal override GlyphMetrics CloneForRendering(TextRun textRun) internal override void RenderTo( IGlyphRenderer renderer, int graphemeIndex, - Vector2 location, - Vector2 offset, + Vector2 glyphOrigin, + Vector2 decorationOrigin, GlyphLayoutMode mode, TextOptions options) { @@ -149,16 +149,12 @@ internal override void RenderTo( float pointSize = this.TextRun.Font?.Size ?? options.Font.Size; float dpi = options.Dpi; - // The glyph vector is rendered offset to the location. - // For horizontal text, the offset is always zero but vertical or rotated text - // will be offset against the location. - location *= dpi; - offset *= dpi; - Vector2 renderLocation = location + offset; + glyphOrigin *= dpi; + decorationOrigin *= dpi; float scaledPPEM = this.GetScaledSize(pointSize, dpi); Matrix3x2 rotation = GetRotationMatrix(mode); - FontRectangle box = this.GetBoundingBox(mode, renderLocation, scaledPPEM); + FontRectangle box = this.GetBoundingBox(mode, glyphOrigin, scaledPPEM); GlyphRendererParameters parameters = new(this, this.TextRun, pointSize, dpi, mode, graphemeIndex); if (renderer.BeginGlyph(in box, in parameters)) @@ -195,8 +191,8 @@ internal override void RenderTo( endOfContour = endPoints[i]; Vector2 prev; - Vector2 curr = (YInverter * controlPoints[endOfContour].Point) + renderLocation; - Vector2 next = (YInverter * controlPoints[startOfContour].Point) + renderLocation; + Vector2 curr = (YInverter * controlPoints[endOfContour].Point) + glyphOrigin; + Vector2 next = (YInverter * controlPoints[startOfContour].Point) + glyphOrigin; if (controlPoints[endOfContour].OnCurve) { @@ -224,7 +220,7 @@ internal override void RenderTo( int currentIndex = startOfContour + p; int nextIndex = startOfContour + ((p + 1) % length); int prevIndex = startOfContour + ((length + p - 1) % length); - next = (YInverter * controlPoints[nextIndex].Point) + renderLocation; + next = (YInverter * controlPoints[nextIndex].Point) + glyphOrigin; if (controlPoints[currentIndex].OnCurve) { @@ -257,7 +253,7 @@ internal override void RenderTo( } renderer.EndGlyph(); - this.RenderDecorationsTo(renderer, location, mode, rotation, scaledPPEM, options); + this.RenderDecorationsTo(renderer, decorationOrigin, mode, rotation, scaledPPEM, options); } } } diff --git a/src/SixLabors.Fonts/TextBidiMode.cs b/src/SixLabors.Fonts/TextBidiMode.cs new file mode 100644 index 00000000..243e439b --- /dev/null +++ b/src/SixLabors.Fonts/TextBidiMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Specifies how bidirectional text is resolved. +/// +public enum TextBidiMode +{ + /// + /// Uses the Unicode Bidirectional Algorithm with each character's bidirectional class. + /// + Normal = 0, + + /// + /// Lays out text in the resolved text direction, ignoring each character's normal bidirectional class. + /// + Override = 1, +} diff --git a/src/SixLabors.Fonts/TextBlock.Visitors.cs b/src/SixLabors.Fonts/TextBlock.Visitors.cs new file mode 100644 index 00000000..15fc3f3c --- /dev/null +++ b/src/SixLabors.Fonts/TextBlock.Visitors.cs @@ -0,0 +1,808 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Visitor types for streaming laid-out glyphs into operations. +/// +public sealed partial class TextBlock +{ + /// + /// Adds a flushed grapheme to its source-order word-boundary segment. + /// + /// The source-order word-boundary segments. + /// The word metrics array being accumulated. + /// The grapheme metrics just emitted. + private static void AccumulateWordMetrics( + List wordSegments, + WordMetrics[] wordMetrics, + in GraphemeMetrics grapheme) + => AccumulateWordMetrics( + wordSegments, + wordMetrics, + grapheme.GraphemeIndex, + grapheme.Advance, + grapheme.Bounds, + grapheme.RenderableBounds); + + /// + /// Adds one coalesced grapheme rectangle set to its source-order word-boundary segment. + /// + /// The source-order word-boundary segments. + /// The word metrics array being accumulated. + /// The source grapheme index owning the rectangles. + /// The positioned logical advance rectangle for the grapheme. + /// The rendered glyph bounds for the grapheme. + /// The union of logical advance and rendered glyph bounds for the grapheme. + private static void AccumulateWordMetrics( + List wordSegments, + WordMetrics[] wordMetrics, + int graphemeIndex, + FontRectangle advance, + FontRectangle bounds, + FontRectangle renderableBounds) + { + int wordIndex = FindWordMetricIndex(wordSegments, graphemeIndex); + WordSegmentRun segment = wordSegments[wordIndex]; + WordMetrics metrics = wordMetrics[wordIndex]; + + // WordMetrics is the final value type, but this array slot also acts as the running + // accumulator for its source segment. Once a slot has source ranges, subsequent + // graphemes in the same word segment union into the rectangles already stored there. + bool hasMetrics = HasWordMetrics(metrics); + + wordMetrics[wordIndex] = new WordMetrics( + hasMetrics ? FontRectangle.Union(metrics.Advance, advance) : advance, + hasMetrics ? FontRectangle.Union(metrics.Bounds, bounds) : bounds, + hasMetrics ? FontRectangle.Union(metrics.RenderableBounds, renderableBounds) : renderableBounds, + segment.GraphemeStart, + segment.GraphemeEnd, + segment.StringStart, + segment.StringEnd); + } + + /// + /// Coalesces consecutive laid-out glyph entries that belong to the same grapheme. + /// + private struct GraphemeMetricsAccumulator + { + private readonly GraphemeMetrics[] graphemes; + private readonly float dpi; + private int count; + private int graphemeIndex; + private int stringIndex; + private int bidiLevel; + private bool isLineBreak; + private FontRectangle advanceBounds; + private FontRectangle bounds; + private bool hasCurrent; + + /// + /// Initializes a new instance of the struct. + /// + /// The target grapheme array to fill. + /// The target DPI. + public GraphemeMetricsAccumulator(GraphemeMetrics[] graphemes, float dpi) + { + this.graphemes = graphemes; + this.dpi = dpi; + this.count = 0; + this.graphemeIndex = 0; + this.stringIndex = 0; + this.bidiLevel = 0; + this.isLineBreak = false; + this.advanceBounds = FontRectangle.Empty; + this.bounds = FontRectangle.Empty; + this.hasCurrent = false; + } + + /// + /// Gets the number of graphemes emitted so far. + /// + public readonly int Count => this.count; + + /// + /// Adds one laid-out glyph entry to the current grapheme, flushing the previous grapheme when needed. + /// + /// The laid-out glyph entry. + public void Visit(in GlyphLayout glyph) + => this.Visit(glyph, out _); + + /// + /// Adds one laid-out glyph entry to the current grapheme, returning emitted metrics when the previous grapheme is flushed. + /// + /// The laid-out glyph entry. + /// The emitted grapheme metrics when this method returns . + /// when a grapheme was emitted. + public bool Visit( + in GlyphLayout glyph, + out GraphemeMetrics metrics) + { + FontRectangle advanceBounds = glyph.MeasureAdvance(this.dpi); + FontRectangle bounds = glyph.MeasureBounds(this.dpi); + + if (!this.hasCurrent) + { + this.Start(glyph, advanceBounds, bounds); + metrics = default; + return false; + } + + if (glyph.GraphemeIndex != this.graphemeIndex) + { + bool emitted = this.Flush(out metrics); + this.Start(glyph, advanceBounds, bounds); + return emitted; + } + + this.advanceBounds = FontRectangle.Union(this.advanceBounds, advanceBounds); + this.bounds = FontRectangle.Union(this.bounds, bounds); + this.isLineBreak |= CodePoint.IsNewLine(glyph.CodePoint); + metrics = default; + return false; + } + + /// + /// Flushes the current line's pending grapheme. + /// + public void EndLine() => this.Flush(out _); + + /// + /// Flushes the current line's pending grapheme. + /// + /// The emitted grapheme metrics when this method returns . + /// when a grapheme was emitted. + public bool EndLine(out GraphemeMetrics metrics) + => this.Flush(out metrics); + + /// + /// Starts a new grapheme from the first emitted glyph in a consecutive grapheme run. + /// + /// The first glyph in the grapheme. + /// The positioned logical advance bounds for . + /// The rendered bounds for . + private void Start( + in GlyphLayout glyph, + in FontRectangle advanceBounds, + in FontRectangle bounds) + { + this.graphemeIndex = glyph.GraphemeIndex; + this.stringIndex = glyph.StringIndex; + this.bidiLevel = glyph.BidiLevel; + this.isLineBreak = CodePoint.IsNewLine(glyph.CodePoint); + this.advanceBounds = advanceBounds; + this.bounds = bounds; + this.hasCurrent = true; + } + + /// + /// Emits the current grapheme while preserving the visual order produced by text layout. + /// + /// The emitted grapheme metrics when this method returns . + /// when a grapheme was emitted. + private bool Flush(out GraphemeMetrics metrics) + { + if (!this.hasCurrent) + { + metrics = default; + return false; + } + + FontRectangle renderableBounds = FontRectangle.Union(this.advanceBounds, this.bounds); + metrics = new GraphemeMetrics( + this.advanceBounds, + this.bounds, + renderableBounds, + this.graphemeIndex, + this.stringIndex, + this.bidiLevel, + this.isLineBreak); + + this.graphemes[this.count] = metrics; + this.count++; + this.hasCurrent = false; + return true; + } + } + + /// + /// Coalesces laid-out glyph entries into grapheme metrics and word metrics in the same stream. + /// + private struct GraphemeAndWordMetricsAccumulator + { + private readonly List wordSegments; + private readonly WordMetrics[] wordMetrics; + private GraphemeMetricsAccumulator graphemes; + + /// + /// Initializes a new instance of the struct. + /// + /// The target grapheme array to fill. + /// The target DPI. + /// The source-order word-boundary segments. + /// The target word metrics array to fill. + public GraphemeAndWordMetricsAccumulator( + GraphemeMetrics[] graphemes, + float dpi, + List wordSegments, + WordMetrics[] wordMetrics) + { + this.wordSegments = wordSegments; + this.wordMetrics = wordMetrics; + this.graphemes = new(graphemes, dpi); + } + + /// + /// Gets the number of graphemes emitted so far. + /// + public readonly int Count => this.graphemes.Count; + + /// + /// Adds one laid-out glyph entry to the current grapheme and updates word metrics when a grapheme is emitted. + /// + /// The laid-out glyph entry. + public void Visit(in GlyphLayout glyph) + { + if (this.graphemes.Visit(glyph, out GraphemeMetrics metrics)) + { + AccumulateWordMetrics(this.wordSegments, this.wordMetrics, metrics); + } + } + + /// + /// Flushes the current line's pending grapheme and updates word metrics when a grapheme is emitted. + /// + public void EndLine() + { + if (this.graphemes.EndLine(out GraphemeMetrics metrics)) + { + AccumulateWordMetrics(this.wordSegments, this.wordMetrics, metrics); + } + } + } + + /// + /// Coalesces laid-out glyph entries into word metrics without storing grapheme metrics. + /// + private struct WordMetricsVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly List wordSegments; + private readonly WordMetrics[] wordMetrics; + private readonly float dpi; + private int graphemeIndex; + private FontRectangle advanceBounds; + private FontRectangle bounds; + private bool hasCurrent; + + /// + /// Initializes a new instance of the struct. + /// + /// The source-order word-boundary segments. + /// The target word metrics array to fill. + /// The target DPI. + public WordMetricsVisitor( + List wordSegments, + WordMetrics[] wordMetrics, + float dpi) + { + this.wordSegments = wordSegments; + this.wordMetrics = wordMetrics; + this.dpi = dpi; + this.graphemeIndex = 0; + this.advanceBounds = FontRectangle.Empty; + this.bounds = FontRectangle.Empty; + this.hasCurrent = false; + } + + /// + public readonly void BeginLine(int lineIndex) + { + } + + /// + public void Visit(in GlyphLayout glyph) + { + FontRectangle advanceBounds = glyph.MeasureAdvance(this.dpi); + FontRectangle bounds = glyph.MeasureBounds(this.dpi); + + if (!this.hasCurrent) + { + this.Start(glyph, advanceBounds, bounds); + return; + } + + if (glyph.GraphemeIndex != this.graphemeIndex) + { + this.Flush(); + this.Start(glyph, advanceBounds, bounds); + return; + } + + this.advanceBounds = FontRectangle.Union(this.advanceBounds, advanceBounds); + this.bounds = FontRectangle.Union(this.bounds, bounds); + } + + /// + public void EndLine() => this.Flush(); + + /// + /// Starts a new word-metrics grapheme from the first emitted glyph in a consecutive grapheme run. + /// + /// The first glyph in the grapheme. + /// The positioned logical advance bounds for . + /// The rendered bounds for . + private void Start( + in GlyphLayout glyph, + in FontRectangle advanceBounds, + in FontRectangle bounds) + { + this.graphemeIndex = glyph.GraphemeIndex; + this.advanceBounds = advanceBounds; + this.bounds = bounds; + this.hasCurrent = true; + } + + /// + /// Emits the current grapheme directly into its source-order word-boundary segment. + /// + private void Flush() + { + if (!this.hasCurrent) + { + return; + } + + FontRectangle renderableBounds = FontRectangle.Union(this.advanceBounds, this.bounds); + AccumulateWordMetrics( + this.wordSegments, + this.wordMetrics, + this.graphemeIndex, + this.advanceBounds, + this.bounds, + renderableBounds); + + this.hasCurrent = false; + } + } + + /// + /// Accumulates the rendered rectangle as glyphs stream from layout. + /// + private struct RenderedRectangleAccumulator : TextLayout.IGlyphLayoutVisitor + { + private readonly float dpi; + private float left; + private float top; + private float right; + private float bottom; + private bool any; + + /// + /// Initializes a new instance of the struct. + /// + /// The target DPI. + public RenderedRectangleAccumulator(float dpi) + { + this.dpi = dpi; + this.left = float.MaxValue; + this.top = float.MaxValue; + this.right = float.MinValue; + this.bottom = float.MinValue; + this.any = false; + } + + /// + public readonly void BeginLine(int lineIndex) + { + } + + /// + public void Visit(in GlyphLayout glyph) + { + FontRectangle box = glyph.MeasureBounds(this.dpi); + if (box.Width <= 0 && box.Height <= 0) + { + return; + } + + if (box.Left < this.left) + { + this.left = box.Left; + } + + if (box.Top < this.top) + { + this.top = box.Top; + } + + if (box.Right > this.right) + { + this.right = box.Right; + } + + if (box.Bottom > this.bottom) + { + this.bottom = box.Bottom; + } + + this.any = true; + } + + /// + /// Returns the accumulated rendered bounds. + /// + /// The rendered bounds of all visited glyphs. + public readonly FontRectangle Result() + => this.any ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; + + /// + public readonly void EndLine() + { + } + } + + /// + /// Builds the bounds and grapheme metrics array while glyphs stream from layout. + /// + private struct GraphemeMetricsVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly float dpi; + private GraphemeMetricsAccumulator graphemes; + private float left; + private float top; + private float right; + private float bottom; + private bool hasBounds; + + /// + /// Initializes a new instance of the struct. + /// + /// The target DPI. + /// The grapheme metrics array to fill. + public GraphemeMetricsVisitor( + float dpi, + GraphemeMetrics[] graphemes) + { + this.dpi = dpi; + this.graphemes = new(graphemes, dpi); + this.left = float.MaxValue; + this.top = float.MaxValue; + this.right = float.MinValue; + this.bottom = float.MinValue; + this.hasBounds = false; + } + + /// + public readonly void BeginLine(int lineIndex) + { + } + + /// + public void Visit(in GlyphLayout glyph) + { + FontRectangle glyphBox = glyph.MeasureBounds(this.dpi); + bool hasGlyphBox = glyphBox.Width > 0 || glyphBox.Height > 0; + + if (hasGlyphBox && glyphBox.Left < this.left) + { + this.left = glyphBox.Left; + } + + if (hasGlyphBox && glyphBox.Top < this.top) + { + this.top = glyphBox.Top; + } + + if (hasGlyphBox && glyphBox.Right > this.right) + { + this.right = glyphBox.Right; + } + + if (hasGlyphBox && glyphBox.Bottom > this.bottom) + { + this.bottom = glyphBox.Bottom; + } + + this.hasBounds |= hasGlyphBox; + this.graphemes.Visit(glyph); + } + + /// + /// Returns the accumulated rendered bounds. + /// + /// The rendered bounds of all visited glyphs. + public readonly FontRectangle Bounds() + => this.hasBounds ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; + + /// + public void EndLine() => this.graphemes.EndLine(); + } + + /// + /// Builds the bounds, grapheme metrics, and word metrics arrays while glyphs stream from layout. + /// + private struct GraphemeAndWordMetricsVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly float dpi; + private GraphemeAndWordMetricsAccumulator graphemes; + private float left; + private float top; + private float right; + private float bottom; + private bool hasBounds; + + /// + /// Initializes a new instance of the struct. + /// + /// The target DPI. + /// The grapheme metrics array to fill. + /// The source-order word-boundary segments. + /// The word metrics array to fill. + public GraphemeAndWordMetricsVisitor( + float dpi, + GraphemeMetrics[] graphemes, + List wordSegments, + WordMetrics[] wordMetrics) + { + this.dpi = dpi; + this.graphemes = new(graphemes, dpi, wordSegments, wordMetrics); + this.left = float.MaxValue; + this.top = float.MaxValue; + this.right = float.MinValue; + this.bottom = float.MinValue; + this.hasBounds = false; + } + + /// + public readonly void BeginLine(int lineIndex) + { + } + + /// + public void Visit(in GlyphLayout glyph) + { + FontRectangle glyphBox = glyph.MeasureBounds(this.dpi); + bool hasGlyphBox = glyphBox.Width > 0 || glyphBox.Height > 0; + + if (hasGlyphBox && glyphBox.Left < this.left) + { + this.left = glyphBox.Left; + } + + if (hasGlyphBox && glyphBox.Top < this.top) + { + this.top = glyphBox.Top; + } + + if (hasGlyphBox && glyphBox.Right > this.right) + { + this.right = glyphBox.Right; + } + + if (hasGlyphBox && glyphBox.Bottom > this.bottom) + { + this.bottom = glyphBox.Bottom; + } + + this.hasBounds |= hasGlyphBox; + this.graphemes.Visit(glyph); + } + + /// + /// Returns the accumulated rendered bounds. + /// + /// The rendered bounds of all visited glyphs. + public readonly FontRectangle Bounds() + => this.hasBounds ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; + + /// + public void EndLine() => this.graphemes.EndLine(); + } + + /// + /// Builds the per-line grapheme metrics results while glyphs stream from layout. + /// + private struct LineLayoutVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly TextBox textBox; + private readonly TextOptions options; + private readonly float wrappingLength; + private readonly LineMetrics[] metrics; + private readonly LineLayout[] lines; + private readonly GraphemeMetrics[] graphemes; + private readonly WordMetrics[] wordMetrics; + private GraphemeAndWordMetricsAccumulator graphemeAccumulator; + private int lineIndex; + private int lineGraphemeStart; + private int metricIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The shaped and line-broken text box. + /// The text options used for layout. + /// The wrapping length in pixels. + /// The grapheme metrics array to fill. + /// The line metrics aligned with the line-broken text box. + /// The line layout array to fill. + /// The source-order word-boundary segments. + /// The word metrics for the source text. + /// The target DPI. + public LineLayoutVisitor( + TextBox textBox, + TextOptions options, + float wrappingLength, + GraphemeMetrics[] graphemes, + LineMetrics[] metrics, + LineLayout[] lines, + List wordSegments, + WordMetrics[] wordMetrics, + float dpi) + { + this.textBox = textBox; + this.options = options; + this.wrappingLength = wrappingLength; + this.metrics = metrics; + this.lines = lines; + this.graphemes = graphemes; + this.wordMetrics = wordMetrics; + this.graphemeAccumulator = new(graphemes, dpi, wordSegments, wordMetrics); + this.lineIndex = 0; + this.lineGraphemeStart = 0; + this.metricIndex = 0; + } + + /// + public void BeginLine(int lineIndex) + { + this.lineGraphemeStart = this.graphemeAccumulator.Count; + this.metricIndex = lineIndex; + } + + /// + public void Visit(in GlyphLayout glyph) + => this.graphemeAccumulator.Visit(glyph); + + /// + public void EndLine() + { + this.graphemeAccumulator.EndLine(); + + // TextLayout owns the visual line loop, so the slice is recorded here instead of + // reconstructing line membership from metrics after glyph emission. + ReadOnlyMemory lineGraphemes = new(this.graphemes, this.lineGraphemeStart, this.graphemeAccumulator.Count - this.lineGraphemeStart); + this.lines[this.lineIndex] = new LineLayout( + this.textBox, + this.options, + this.wrappingLength, + this.metricIndex, + in this.metrics[this.metricIndex], + lineGraphemes, + this.wordMetrics); + + this.lineIndex++; + } + } + + /// + /// Builds one per-glyph metrics array while glyphs stream from layout. + /// + private struct GlyphMetricsVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly GlyphMetrics[] glyphMetrics; + private readonly float dpi; + private readonly int lineIndex; + private int count; + private int currentLineIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to fill. + /// The target DPI. + public GlyphMetricsVisitor( + GlyphMetrics[] glyphMetrics, + float dpi) + : this(glyphMetrics, dpi, -1) + { + } + + /// + /// Initializes a new instance of the struct. + /// + /// The target array to fill. + /// The target DPI. + /// The line index to collect. + public GlyphMetricsVisitor( + GlyphMetrics[] glyphMetrics, + float dpi, + int lineIndex) + { + this.glyphMetrics = glyphMetrics; + this.dpi = dpi; + this.lineIndex = lineIndex; + this.count = 0; + this.currentLineIndex = -1; + } + + /// + public void BeginLine(int lineIndex) + => this.currentLineIndex = lineIndex; + + /// + public void Visit(in GlyphLayout glyph) + { + if (this.lineIndex >= 0 && this.currentLineIndex != this.lineIndex) + { + return; + } + + FontRectangle advance = glyph.MeasureAdvance(this.dpi); + FontRectangle bounds = glyph.MeasureBounds(this.dpi); + FontRectangle renderableBounds = FontRectangle.Union(advance, bounds); + + this.glyphMetrics[this.count] = new GlyphMetrics( + glyph.Glyph.GlyphMetrics.CodePoint, + advance, + bounds, + renderableBounds, + glyph.GraphemeIndex, + glyph.StringIndex); + + this.count++; + } + + /// + public readonly void EndLine() + { + } + } + + /// + /// Renders glyphs as they stream from layout. + /// + private struct GlyphRendererVisitor : TextLayout.IGlyphLayoutVisitor + { + private readonly IGlyphRenderer renderer; + private readonly TextOptions options; + private readonly int lineIndex; + private int currentLineIndex; + + /// + /// Initializes a new instance of the struct. + /// + /// The target renderer. + /// The text options used for rendering. + /// The line index to render, or -1 to render every line. + public GlyphRendererVisitor(IGlyphRenderer renderer, TextOptions options, int lineIndex) + { + this.renderer = renderer; + this.options = options; + this.lineIndex = lineIndex; + this.currentLineIndex = -1; + } + + /// + public void BeginLine(int lineIndex) => this.currentLineIndex = lineIndex; + + /// + public readonly void Visit(in GlyphLayout glyph) + { + if (this.lineIndex > -1 && this.currentLineIndex != this.lineIndex) + { + return; + } + + glyph.Glyph.RenderTo(this.renderer, glyph.GraphemeIndex, glyph.GlyphOrigin, glyph.DecorationOrigin, glyph.LayoutMode, this.options); + } + + /// + public readonly void EndLine() + { + } + } +} diff --git a/src/SixLabors.Fonts/TextBlock.cs b/src/SixLabors.Fonts/TextBlock.cs new file mode 100644 index 00000000..2c2b6aa5 --- /dev/null +++ b/src/SixLabors.Fonts/TextBlock.cs @@ -0,0 +1,583 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Rendering; + +namespace SixLabors.Fonts; + +/// +/// Represents text prepared for repeated line layout, measurement, and rendering. +/// +public sealed partial class TextBlock +{ + /// + /// Initializes a new instance of the class. + /// + /// The text to prepare. + /// The text options used to prepare, measure, and render the block. + /// + /// is ignored while preparing the block; pass the wrapping length + /// to the measurement or rendering method. Use -1 there to disable wrapping. + /// + public TextBlock(string text, TextOptions options) + : this(text.AsSpan(), options) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The text to prepare. + /// The text options used to prepare, measure, and render the block. + /// + /// is ignored while preparing the block; pass the wrapping length + /// to the measurement or rendering method. Use -1 there to disable wrapping. + /// + public TextBlock(ReadOnlySpan text, TextOptions options) + { + this.Options = options; + + if (text.IsEmpty) + { + this.LogicalLine = new(new TextLine(), [], [], []); + return; + } + + ShapedText shaped = TextLayout.ShapeText(text, options); + this.LogicalLine = TextLayout.ComposeLogicalLine(shaped, text, options); + } + + /// + /// Gets the text options used by this block. + /// + internal TextOptions Options { get; } + + /// + /// Gets the prepared logical line and line break opportunities. + /// + internal LogicalTextLine LogicalLine { get; } + + /// + /// Breaks this block into lines for the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The line-broken text box. + internal TextBox BreakLines(float wrappingLength) + => TextLayout.BreakLines(this.LogicalLine, this.Options, wrappingLength); + + /// + /// Measures the full set of layout metrics for this block at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A instance containing every measurement for the laid-out text. + public TextMetrics Measure(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + float dpi = this.Options.Dpi; + bool isHorizontal = this.Options.LayoutMode.IsHorizontal(); + + FontRectangle advance = GetAdvance(textBox, dpi, isHorizontal); + + GraphemeMetrics[] graphemes = new GraphemeMetrics[CountGraphemeMetrics(textBox)]; + WordMetrics[] wordMetrics = new WordMetrics[this.LogicalLine.WordSegments.Count]; + + GraphemeAndWordMetricsVisitor visitor = new(dpi, graphemes, this.LogicalLine.WordSegments, wordMetrics); + TextLayout.LayoutText(textBox, this.Options, wrappingLength, ref visitor); + + FontRectangle bounds = visitor.Bounds(); + FontRectangle absoluteAdvance = new(this.Options.Origin.X, this.Options.Origin.Y, advance.Width, advance.Height); + FontRectangle renderableBounds = FontRectangle.Union(absoluteAdvance, bounds); + + LineMetrics[] lineMetrics = GetLineMetrics(textBox, this.Options, wrappingLength); + + return new TextMetrics( + this, + textBox, + wrappingLength, + advance, + bounds, + renderableBounds, + textBox.TextLines.Count, + graphemes, + lineMetrics, + wordMetrics); + } + + /// + /// Measures the logical advance of this block at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The logical advance rectangle. + public FontRectangle MeasureAdvance(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + return GetAdvance(textBox, this.Options.Dpi, this.Options.LayoutMode.IsHorizontal()); + } + + /// + /// Measures the rendered glyph bounds of this block at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The rendered glyph bounds. + public FontRectangle MeasureBounds(float wrappingLength) + => GetBounds(this.BreakLines(wrappingLength), this.Options, wrappingLength); + + /// + /// Measures the union of logical advance and rendered glyph bounds at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The full renderable bounds. + public FontRectangle MeasureRenderableBounds(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + FontRectangle advance = GetAdvance(textBox, this.Options.Dpi, this.Options.LayoutMode.IsHorizontal()); + FontRectangle absoluteAdvance = new(this.Options.Origin.X, this.Options.Origin.Y, advance.Width, advance.Height); + FontRectangle bounds = GetBounds(textBox, this.Options, wrappingLength); + return FontRectangle.Union(absoluteAdvance, bounds); + } + + /// + /// Gets the positioned metrics of each laid-out glyph entry. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing per-glyph metrics entries. + public ReadOnlyMemory GetGlyphMetrics(float wrappingLength) + => this.GetGlyphMetricsArray(wrappingLength); + + /// + /// Gets the positioned metrics of each laid-out grapheme. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing per-grapheme metrics entries. + public ReadOnlyMemory GetGraphemeMetrics(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + return GetGraphemeMetricsArray(textBox, this.Options, wrappingLength); + } + + /// + /// Gets the positioned metrics of each Unicode word-boundary segment. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing per-word-boundary segment metrics entries. + public ReadOnlyMemory GetWordMetrics(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + WordMetrics[] wordMetrics = new WordMetrics[this.LogicalLine.WordSegments.Count]; + WordMetricsVisitor visitor = new(this.LogicalLine.WordSegments, wordMetrics, this.Options.Dpi); + TextLayout.LayoutText(textBox, this.Options, wrappingLength, ref visitor); + return wordMetrics; + } + + /// + /// Gets the number of laid-out lines at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The laid-out line count. + public int CountLines(float wrappingLength) + => this.BreakLines(wrappingLength).TextLines.Count; + + /// + /// Gets per-line layout metrics at the supplied wrapping length. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing in pixel units. + public ReadOnlyMemory GetLineMetrics(float wrappingLength) + => GetLineMetrics(this.BreakLines(wrappingLength), this.Options, wrappingLength); + + /// + /// Gets visual line layouts for this block at the supplied wrapping length. + /// + /// + /// The returned memory contains every laid-out line, including lines produced by hard line breaks. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// A read-only memory region containing entries in final layout order. + public ReadOnlyMemory GetLineLayouts(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + if (textBox.TextLines.Count == 0) + { + return ReadOnlyMemory.Empty; + } + + return this.GetLineLayouts(textBox, wrappingLength); + } + + /// + /// Creates an enumerator that lays out this block one line at a time. + /// + /// A line layout enumerator for this block. + public LineLayoutEnumerator EnumerateLineLayouts() + => new(this); + + /// + /// Gets a single line layout for an already line-broken text line. + /// + /// The line to lay out. + /// The wrapping length in pixels. + /// The block-level text direction used for alignment. + /// The line layout for the supplied line. + internal LineLayout GetLineLayout( + TextLine textLine, + float wrappingLength, + TextDirection textDirection) + { + TextBox textBox = new([textLine], textDirection); + + return this.GetLineLayouts(textBox, wrappingLength)[0]; + } + + /// + /// Gets visual line layouts for an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The wrapping length in pixels. + /// The line layouts for the supplied text box. + private LineLayout[] GetLineLayouts(TextBox textBox, float wrappingLength) + { + GraphemeMetrics[] graphemes = new GraphemeMetrics[CountGraphemeMetrics(textBox)]; + LineMetrics[] metrics = GetLineMetrics(textBox, this.Options, wrappingLength); + LineLayout[] lines = new LineLayout[textBox.TextLines.Count]; + + WordMetrics[] wordMetrics = new WordMetrics[this.LogicalLine.WordSegments.Count]; + LineLayoutVisitor visitor = new(textBox, this.Options, wrappingLength, graphemes, metrics, lines, this.LogicalLine.WordSegments, wordMetrics, this.Options.Dpi); + TextLayout.LayoutText(textBox, this.Options, wrappingLength, ref visitor); + + return lines; + } + + /// + /// Renders this block to the supplied glyph renderer at the supplied wrapping length. + /// + /// The target renderer. + /// The wrapping length in pixels. Use -1 to disable wrapping. + public void RenderTo(IGlyphRenderer renderer, float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + FontRectangle rect = GetBounds(textBox, this.Options, wrappingLength); + + RenderTo(renderer, textBox, this.Options, wrappingLength, rect); + } + + /// + /// Renders an already line-broken text box to the supplied glyph renderer. + /// + /// The target renderer. + /// The shaped and line-broken text box. + /// The text options used for rendering. + /// The wrapping length in pixels. + /// The bounds passed to the renderer. + /// The line index to render, or -1 to render every line. + internal static void RenderTo( + IGlyphRenderer renderer, + TextBox textBox, + TextOptions options, + float wrappingLength, + in FontRectangle bounds, + int lineIndex = -1) + { + renderer.BeginText(in bounds); + + GlyphRendererVisitor visitor = new(renderer, options, lineIndex); + TextLayout.LayoutText(textBox, options, wrappingLength, ref visitor); + + renderer.EndText(); + } + + /// + /// Measures the rendered glyph bounds of an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The text options used for layout. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The union of the rendered glyph bounds. + private static FontRectangle GetBounds(TextBox textBox, TextOptions options, float wrappingLength) + { + if (textBox.TextLines.Count == 0) + { + return FontRectangle.Empty; + } + + RenderedRectangleAccumulator visitor = new(options.Dpi); + TextLayout.LayoutText(textBox, options, wrappingLength, ref visitor); + return visitor.Result(); + } + + /// + /// Gets per-line layout metrics for an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The text options used to calculate line metrics. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// An array of in pixel units. + private static LineMetrics[] GetLineMetrics(TextBox textBox, TextOptions options, float wrappingLength) + { + if (textBox.TextLines.Count == 0) + { + return []; + } + + LineMetrics[] metrics = new LineMetrics[textBox.TextLines.Count]; + + // Determine the line-box extent used for alignment within the flow direction. + float maxScaledAdvance = textBox.ScaledMaxAdvance(); + if (options.TextAlignment != TextAlignment.Start && wrappingLength > 0) + { + maxScaledAdvance = MathF.Max(wrappingLength / options.Dpi, maxScaledAdvance); + } + + TextDirection direction = textBox.TextDirection(); + LayoutMode layoutMode = options.LayoutMode; + + bool isHorizontalLayout = layoutMode.IsHorizontal(); + float lineOffset = isHorizontalLayout ? options.Origin.Y : options.Origin.X; + + bool reverseLineOrder = layoutMode is + LayoutMode.HorizontalBottomTop + or LayoutMode.VerticalRightLeft + or LayoutMode.VerticalMixedRightLeft; + + int i = reverseLineOrder ? textBox.TextLines.Count - 1 : 0; + int step = reverseLineOrder ? -1 : 1; + int graphemeOffset = 0; + + while (i >= 0 && i < textBox.TextLines.Count) + { + TextLine line = textBox.TextLines[i]; + + // Calculate the line start position in the current flow direction. + float offset = isHorizontalLayout + ? TextLayout.CalculateLineOffsetX( + line.ScaledLineAdvance, + maxScaledAdvance, + options.HorizontalAlignment, + options.TextAlignment, + direction) + : TextLayout.CalculateLineOffsetY( + line.ScaledLineAdvance, + maxScaledAdvance, + options.VerticalAlignment, + options.TextAlignment, + direction); + + // Delta captured during layout when ascender/descender were symmetrically + // adjusted to match browser-like line-box behavior. + float delta = line.ScaledMaxDelta; + + // Core typographic region within the line box. + // We add back 2*delta to recover the pre-adjustment ascender+descender span + // used for deriving guide positions. + float coreHeight = line.ScaledMaxAscender + line.ScaledMaxDescender + (2 * delta); + + // Additional leading in the line box (for example from line spacing). + float extra = line.ScaledMaxLineHeight - coreHeight; + + // Baseline position within the line box. + float baseline = (extra * 0.5f) + line.ScaledMaxAscender + delta; + + // Ascender line position relative to the same origin. + float ascender = baseline - line.ScaledMaxAscender + delta; + + // Descender line position relative to the same origin. + float descender = baseline + line.ScaledMaxDescender + delta; + Vector2 start = isHorizontalLayout + ? new(options.Origin.X + (offset * options.Dpi), lineOffset) + : new(lineOffset, options.Origin.Y + (offset * options.Dpi)); + + Vector2 extent = isHorizontalLayout + ? new(line.ScaledLineAdvance * options.Dpi, line.ScaledMaxLineHeight * options.Dpi) + : new(line.ScaledMaxLineHeight * options.Dpi, line.ScaledLineAdvance * options.Dpi); + + // Bidi reordering mutates entries into visual order, so the source + // start is the minimum original source index rather than line[0]. + int stringIndex = line[0].StringIndex; + int graphemeIndex = line[0].GraphemeIndex; + for (int j = 1; j < line.Count; j++) + { + stringIndex = Math.Min(stringIndex, line[j].StringIndex); + graphemeIndex = Math.Min(graphemeIndex, line[j].GraphemeIndex); + } + + metrics[i] = new LineMetrics( + ascender * options.Dpi, + baseline * options.Dpi, + descender * options.Dpi, + line.ScaledMaxLineHeight * options.Dpi, + start, + extent, + stringIndex, + graphemeIndex, + line.GraphemeCount, + graphemeOffset); + + graphemeOffset += line.GraphemeCount; + lineOffset += line.ScaledMaxLineHeight * options.Dpi; + i += step; + } + + return metrics; + } + + /// + /// Counts grapheme metrics entries across all lines in an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The number of grapheme metrics entries. + private static int CountGraphemeMetrics(TextBox textBox) + { + int count = 0; + for (int i = 0; i < textBox.TextLines.Count; i++) + { + count += textBox.TextLines[i].GraphemeCount; + } + + return count; + } + + /// + /// Gets grapheme metrics entries by streaming laid-out glyphs. + /// + /// The shaped and line-broken text box. + /// The text options used for layout. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The grapheme metrics entries. + internal static GraphemeMetrics[] GetGraphemeMetricsArray( + TextBox textBox, + TextOptions options, + float wrappingLength) + { + int count = CountGraphemeMetrics(textBox); + if (count == 0) + { + return []; + } + + GraphemeMetrics[] graphemes = new GraphemeMetrics[count]; + GraphemeMetricsVisitor visitor = new(options.Dpi, graphemes); + TextLayout.LayoutText(textBox, options, wrappingLength, ref visitor); + return graphemes; + } + + /// + /// Finds the source-order word-boundary range containing the supplied grapheme index. + /// + /// The source-order word-boundary segments. + /// The grapheme index to locate. + /// The matching word metrics index, or -1 when no range contains the grapheme. + private static int FindWordMetricIndex(List wordSegments, int graphemeIndex) + { + int min = 0; + int max = wordSegments.Count - 1; + while (min <= max) + { + int mid = (min + max) >> 1; + WordSegmentRun segment = wordSegments[mid]; + if (graphemeIndex < segment.GraphemeStart) + { + max = mid - 1; + continue; + } + + if (graphemeIndex >= segment.GraphemeEnd) + { + min = mid + 1; + continue; + } + + return mid; + } + + return -1; + } + + /// + /// Gets a value indicating whether positioned metrics have been added to a word segment. + /// + /// The word metrics to inspect. + /// when a grapheme has been accumulated for the segment. + private static bool HasWordMetrics(in WordMetrics metrics) + { + // Default WordMetrics has no source range. Any real word segment has an exclusive end + // index, so the range is the sentinel that avoids treating FontRectangle.Empty as geometry. + return metrics.GraphemeEnd != 0 || metrics.StringEnd != 0; + } + + /// + /// Gets one per-glyph metrics collection by streaming laid-out glyphs. + /// + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The positioned glyph metrics. + internal GlyphMetrics[] GetGlyphMetricsArray(float wrappingLength) + { + TextBox textBox = this.BreakLines(wrappingLength); + return GetGlyphMetricsArray(textBox, this.Options, wrappingLength); + } + + /// + /// Gets one per-glyph metrics collection by streaming laid-out glyphs. + /// + /// The shaped and line-broken text box. + /// The text options used for layout. + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The line index to collect, or -1 to collect every line. + /// The positioned glyph metrics. + internal static GlyphMetrics[] GetGlyphMetricsArray( + TextBox textBox, + TextOptions options, + float wrappingLength, + int lineIndex = -1) + { + int count = lineIndex < 0 ? textBox.CountGlyphLayouts() : textBox.TextLines[lineIndex].CountGlyphLayouts(); + if (count == 0) + { + return []; + } + + GlyphMetrics[] result = new GlyphMetrics[count]; + GlyphMetricsVisitor visitor = new(result, options.Dpi, lineIndex); + TextLayout.LayoutText(textBox, options, wrappingLength, ref visitor); + return result; + } + + /// + /// Measures the logical advance of an already line-broken text box. + /// + /// The shaped and line-broken text box. + /// The target DPI. + /// Whether the layout direction is horizontal. + /// The logical advance rectangle. + private static FontRectangle GetAdvance(TextBox textBox, float dpi, bool isHorizontalLayout) + { + if (textBox.TextLines.Count == 0) + { + return FontRectangle.Empty; + } + + if (isHorizontalLayout) + { + float width = 0; + float height = 0; + for (int i = 0; i < textBox.TextLines.Count; i++) + { + TextLine line = textBox.TextLines[i]; + width = MathF.Max(width, line.ScaledLineAdvance); + height += line.ScaledMaxLineHeight; + } + + return new FontRectangle(0, 0, width * dpi, height * dpi); + } + + float verticalWidth = 0; + float verticalHeight = 0; + for (int i = 0; i < textBox.TextLines.Count; i++) + { + TextLine line = textBox.TextLines[i]; + verticalWidth += line.ScaledMaxLineHeight; + verticalHeight = MathF.Max(verticalHeight, line.ScaledLineAdvance); + } + + return new FontRectangle(0, 0, verticalWidth * dpi, verticalHeight * dpi); + } +} diff --git a/src/SixLabors.Fonts/TextBox.cs b/src/SixLabors.Fonts/TextBox.cs new file mode 100644 index 00000000..e1800fae --- /dev/null +++ b/src/SixLabors.Fonts/TextBox.cs @@ -0,0 +1,85 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using System.Linq; + +namespace SixLabors.Fonts; + +/// +/// Represents a shaped and line-broken block of text. +/// +internal sealed class TextBox +{ + private readonly TextDirection textDirection; + + private float? scaledMaxAdvance; + + private float? minY; + + private int glyphLayoutCount; + + private bool hasGlyphLayoutCounts; + + /// + /// Initializes a new instance of the class. + /// + /// The shaped, line-broken lines that make up this text box. + /// The block-level text direction. + public TextBox(IReadOnlyList textLines, TextDirection textDirection) + { + this.TextLines = textLines; + this.textDirection = textDirection; + } + + /// + /// Gets the shaped and line-broken lines that make up the text. + /// + public IReadOnlyList TextLines { get; } + + /// + /// Returns the widest scaled line advance across all lines. The result is memoized. + /// + /// The widest scaled line advance. + public float ScaledMaxAdvance() + => this.scaledMaxAdvance ??= this.TextLines.Max(x => x.ScaledLineAdvance); + + /// + /// Returns the smallest (most negative) scaled Y position encountered across all lines. + /// Used to detect ink that extends above the typographic ascender (stacked marks in Tibetan etc.). + /// The result is memoized. + /// + /// The smallest scaled Y position in the text box. + public float ScaledMinY() + => this.minY ??= this.TextLines.Min(x => x.ScaledMinY); + + /// + /// Counts all glyph entries emitted from this text box. The result is memoized. + /// + /// The number of glyph entries that layout will emit. + public int CountGlyphLayouts() + => this.hasGlyphLayoutCounts ? this.glyphLayoutCount : this.CountGlyphLayoutsCore(); + + /// + /// Computes the glyph-layout count in one pass. + /// + /// The number of glyph entries that layout will emit. + private int CountGlyphLayoutsCore() + { + int count = 0; + for (int i = 0; i < this.TextLines.Count; i++) + { + count += this.TextLines[i].CountGlyphLayouts(); + } + + this.glyphLayoutCount = count; + this.hasGlyphLayoutCounts = true; + return count; + } + + /// + /// Returns the block-level text direction used for alignment calculations. + /// + /// The block-level text direction. + public TextDirection TextDirection() => this.textDirection; +} diff --git a/src/SixLabors.Fonts/TextEllipsis.cs b/src/SixLabors.Fonts/TextEllipsis.cs new file mode 100644 index 00000000..3903e40f --- /dev/null +++ b/src/SixLabors.Fonts/TextEllipsis.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Specifies ellipsis behavior when laid-out text is limited to a maximum number of lines. +/// +public enum TextEllipsis +{ + /// + /// Do not insert an ellipsis marker. + /// + None = 0, + + /// + /// Insert the standard ellipsis marker. + /// + Standard, + + /// + /// Insert the marker specified by . + /// + Custom +} diff --git a/src/SixLabors.Fonts/TextHit.cs b/src/SixLabors.Fonts/TextHit.cs new file mode 100644 index 00000000..6dfc0f50 --- /dev/null +++ b/src/SixLabors.Fonts/TextHit.cs @@ -0,0 +1,50 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Represents a hit-tested grapheme position in laid-out text. +/// +public readonly struct TextHit +{ + /// + /// Initializes a new instance of the struct. + /// + /// The zero-based line index. + /// The grapheme index in the original text. + /// The UTF-16 index in the original text. + /// Whether the hit is on the trailing side of the grapheme. + internal TextHit(int lineIndex, int graphemeIndex, int stringIndex, bool isTrailing) + { + this.LineIndex = lineIndex; + this.GraphemeIndex = graphemeIndex; + this.StringIndex = stringIndex; + this.IsTrailing = isTrailing; + } + + /// + /// Gets the zero-based line index. + /// + public int LineIndex { get; } + + /// + /// Gets the zero-based grapheme index in the original text. + /// + public int GraphemeIndex { get; } + + /// + /// Gets the zero-based UTF-16 code unit index in the original text. + /// + public int StringIndex { get; } + + /// + /// Gets the grapheme insertion index represented by this hit. + /// + public int GraphemeInsertionIndex => this.GraphemeIndex + (this.IsTrailing ? 1 : 0); + + /// + /// Gets a value indicating whether the hit is on the trailing side of the grapheme. + /// + public bool IsTrailing { get; } +} diff --git a/src/SixLabors.Fonts/TextHyphenation.cs b/src/SixLabors.Fonts/TextHyphenation.cs new file mode 100644 index 00000000..f262801b --- /dev/null +++ b/src/SixLabors.Fonts/TextHyphenation.cs @@ -0,0 +1,25 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Specifies hyphenation marker behavior when text breaks at hyphenation opportunities. +/// +public enum TextHyphenation +{ + /// + /// Do not insert a hyphenation marker. + /// + None = 0, + + /// + /// Insert the standard hyphenation marker. + /// + Standard, + + /// + /// Insert the marker specified by . + /// + Custom +} diff --git a/src/SixLabors.Fonts/TextInteraction.cs b/src/SixLabors.Fonts/TextInteraction.cs new file mode 100644 index 00000000..2e4bf74f --- /dev/null +++ b/src/SixLabors.Fonts/TextInteraction.cs @@ -0,0 +1,1445 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; + +namespace SixLabors.Fonts; + +/// +/// Provides shared helpers for text interaction metrics. +/// +/// +/// Text interaction uses grapheme advance rectangles as the logical hit target. Ink bounds can be +/// empty, overhang the advance, or exclude whitespace, which makes them unsuitable for caret +/// positioning and selection highlighting. +/// +internal static class TextInteraction +{ + /// + /// Hit tests a point against a complete laid-out text box. + /// + /// All laid-out lines ordered by their visual position. + /// The full grapheme metrics buffer flattened in visual order. + /// The text-space coordinate to resolve to a grapheme hit. + /// The orientation used to interpret the line and grapheme advances. + /// The nearest grapheme hit. + public static TextHit HitTest( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + Vector2 point, + LayoutMode layoutMode) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return new(-1, -1, -1, false); + } + + bool isHorizontal = layoutMode.IsHorizontal(); + int lineIndex = FindLine(lines, point, isHorizontal); + + // LineMetrics preserve their source line index, while grapheme metrics are emitted in + // visual line order. Locate the line slice by source range so reverse line-order modes + // pair the hit-tested line with its own graphemes. + int graphemeOffset = GetGraphemeOffset(lines[lineIndex]); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, lines[lineIndex].GraphemeCount); + + return HitTestLine(lineIndex, lineGraphemes, point, isHorizontal); + } + + /// + /// Hit tests a point against one laid-out line. + /// + /// The zero-based visual index of the line being hit tested. + /// Only the grapheme metrics belonging to the target line. + /// The coordinate to compare against the line's primary advance axis. + /// The line orientation that determines which axis is primary. + /// The nearest grapheme hit. + public static TextHit HitTestLine( + int lineIndex, + ReadOnlySpan graphemes, + Vector2 point, + LayoutMode layoutMode) + => HitTestLine(lineIndex, graphemes, point, layoutMode.IsHorizontal()); + + /// + /// Gets a caret position from a complete laid-out text box. + /// + /// All laid-out lines available for caret placement. + /// The flattened grapheme metrics that back the full text box. + /// The logical insertion position to convert into a visual caret. + /// The layout orientation used when the caret geometry was calculated. + /// The caret position in pixel units. + public static CaretPosition GetCaretPosition( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + int graphemeIndex, + LayoutMode layoutMode) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return new(-1, -1, -1, default, default, false, default, default, 0); + } + + int lineIndex = FindLineByGraphemeIndex(lines, graphemeIndex); + LineMetrics line = lines[lineIndex]; + + // See HitTest: line source indices and flattened storage offsets are deliberately + // separate because bidi reordering can make source order differ from visual order. + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + + return GetCaretPositionLine(lineIndex, line, lineGraphemes, graphemeIndex, layoutMode); + } + + /// + /// Gets a caret position from one laid-out line. + /// + /// The zero-based visual index of the supplied line. + /// The metrics for the single line that will host the caret. + /// The visual-order grapheme metrics for that one line. + /// The logical insertion position to place within the supplied line. + /// The orientation that determines the caret edge direction. + /// The caret position in pixel units. + public static CaretPosition GetCaretPositionLine( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + int graphemeIndex, + LayoutMode layoutMode) + { + if (graphemes.IsEmpty) + { + return new(lineIndex, line.GraphemeIndex, line.StringIndex, default, default, false, default, default, 0); + } + + return CreateCaret(lineIndex, line, graphemes, graphemeIndex, layoutMode.IsHorizontal()); + } + + /// + /// Gets an absolute caret position from a complete laid-out text box. + /// + /// All laid-out lines available for caret placement. + /// The flattened grapheme metrics that back the full text box. + /// The absolute placement within the text box. + /// The layout orientation used when the caret geometry was calculated. + /// The resolved text direction used to choose the visual start or end of the scope. + /// The caret position in pixel units. + public static CaretPosition GetCaret( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + CaretPlacement placement, + LayoutMode layoutMode, + TextDirection direction) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return new(-1, -1, -1, default, default, false, default, default, 0); + } + + int targetGraphemeIndex = placement == CaretPlacement.Start + ? GetSourceTextStart(graphemes) + : GetSourceTextEnd(graphemes); + + int lineIndex = FindLineByGraphemeIndex(lines, targetGraphemeIndex); + + LineMetrics line = lines[lineIndex]; + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + + return GetCaretLine(lineIndex, line, lineGraphemes, placement, layoutMode, direction); + } + + /// + /// Gets an absolute caret position from one laid-out line. + /// + /// The zero-based visual index of the supplied line. + /// The metrics for the single line that will host the caret. + /// The visual-order grapheme metrics for that one line. + /// The absolute placement within the line. + /// The orientation that determines the caret edge direction. + /// The resolved text direction used to choose the visual start or end of the scope. + /// The caret position in pixel units. + public static CaretPosition GetCaretLine( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + CaretPlacement placement, + LayoutMode layoutMode, + TextDirection direction) + { + if (graphemes.IsEmpty) + { + return new(lineIndex, line.GraphemeIndex, line.StringIndex, default, default, false, default, default, 0); + } + + return CreateCaretAtVisualLineEdge(lineIndex, line, graphemes, placement, layoutMode.IsHorizontal(), direction); + } + + /// + /// Moves a caret within a complete laid-out text box. + /// + /// The visual lines across which the caret may move. + /// The flattened grapheme metrics used to resolve movement targets. + /// The source-order word-boundary segment metrics used for word movement. + /// The starting caret location before applying the movement. + /// The requested caret navigation command. + /// The orientation rules that control horizontal versus vertical motion. + /// The resolved text direction used to choose line and text start/end. + /// The moved caret position in pixel units. + public static CaretPosition MoveCaret( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + ReadOnlySpan wordMetrics, + CaretPosition caret, + CaretMovement movement, + LayoutMode layoutMode, + TextDirection direction) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return caret; + } + + bool isHorizontal = layoutMode.IsHorizontal(); + int lineIndex = GetCaretLineIndex(lines, graphemes, caret); + LineMetrics line = lines[lineIndex]; + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + int target = caret.GraphemeIndex; + switch (movement) + { + case CaretMovement.Previous: + target = GetPreviousInsertionIndex(graphemes, caret.GraphemeIndex, GetSourceTextStart(graphemes)); + break; + + case CaretMovement.Next: + target = GetNextInsertionIndex(graphemes, caret.GraphemeIndex, GetSourceTextEnd(graphemes)); + break; + + case CaretMovement.PreviousWord: + target = GetPreviousWordBoundary(wordMetrics, caret.GraphemeIndex, GetSourceTextStart(graphemes)); + break; + + case CaretMovement.NextWord: + target = GetNextWordBoundary(wordMetrics, caret.GraphemeIndex, GetSourceTextEnd(graphemes)); + break; + + case CaretMovement.LineStart: + return GetCaretLine(lineIndex, line, lineGraphemes, CaretPlacement.Start, layoutMode, direction); + + case CaretMovement.LineEnd: + return GetCaretLine(lineIndex, line, lineGraphemes, CaretPlacement.End, layoutMode, direction); + + case CaretMovement.TextStart: + return GetCaret(lines, graphemes, CaretPlacement.Start, layoutMode, direction); + + case CaretMovement.TextEnd: + return GetCaret(lines, graphemes, CaretPlacement.End, layoutMode, direction); + + case CaretMovement.LineUp: + return MoveCaretToAdjacentLine( + lines, + graphemes, + caret, + lineIndex, + lineDown: false, + isHorizontal: isHorizontal, + layoutMode: layoutMode); + + case CaretMovement.LineDown: + return MoveCaretToAdjacentLine( + lines, + graphemes, + caret, + lineIndex, + lineDown: true, + isHorizontal: isHorizontal, + layoutMode: layoutMode); + } + + return GetCaretPosition(lines, graphemes, target, layoutMode); + } + + /// + /// Moves a caret within one laid-out line. + /// + /// The zero-based visual index of the current line. + /// The line metrics that constrain the movement. + /// The grapheme metrics available within that line. + /// The source-order word-boundary segment metrics used for word movement. + /// The caret location to move inside the line. + /// The in-line caret navigation command to execute. + /// The orientation used to choose the caret axis within the line. + /// The resolved text direction used to choose line start/end. + /// The moved caret position in pixel units. + public static CaretPosition MoveCaretLine( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + ReadOnlySpan wordMetrics, + CaretPosition caret, + CaretMovement movement, + LayoutMode layoutMode, + TextDirection direction) + { + if (graphemes.IsEmpty) + { + return caret; + } + + int lineStart = GetSourceLineStart(graphemes); + int lineEnd = GetSourceLineEnd(graphemes); + int target = caret.GraphemeIndex; + switch (movement) + { + case CaretMovement.Previous: + target = GetPreviousInsertionIndex(graphemes, caret.GraphemeIndex, lineStart); + break; + + case CaretMovement.Next: + target = GetNextInsertionIndex(graphemes, caret.GraphemeIndex, lineEnd); + break; + + case CaretMovement.PreviousWord: + target = Math.Max( + lineStart, + GetPreviousWordBoundary(wordMetrics, caret.GraphemeIndex, lineStart)); + break; + + case CaretMovement.NextWord: + target = Math.Min( + lineEnd, + GetNextWordBoundary(wordMetrics, caret.GraphemeIndex, lineEnd)); + break; + + case CaretMovement.LineStart: + case CaretMovement.TextStart: + return GetCaretLine(lineIndex, line, graphemes, CaretPlacement.Start, layoutMode, direction); + + case CaretMovement.LineEnd: + case CaretMovement.TextEnd: + return GetCaretLine(lineIndex, line, graphemes, CaretPlacement.End, layoutMode, direction); + + case CaretMovement.LineUp: + case CaretMovement.LineDown: + return caret; + } + + return GetCaretPositionLine(lineIndex, line, graphemes, target, layoutMode); + } + + /// + /// Gets the word-boundary segment metrics containing the supplied grapheme insertion index. + /// + /// The source-order word metrics to search. + /// The grapheme insertion index to locate. + /// The matching word metrics. + public static WordMetrics GetWordMetrics(ReadOnlySpan wordMetrics, int graphemeIndex) + { + if (wordMetrics.IsEmpty) + { + return default; + } + + for (int i = 0; i < wordMetrics.Length; i++) + { + WordMetrics metrics = wordMetrics[i]; + if (graphemeIndex >= metrics.GraphemeStart && graphemeIndex < metrics.GraphemeEnd) + { + return metrics; + } + + if (graphemeIndex < metrics.GraphemeStart) + { + return metrics; + } + } + + return wordMetrics[^1]; + } + + /// + /// Gets selection rectangles from a complete laid-out text box. + /// + /// The visual lines that may contribute selection rectangles. + /// The flattened grapheme metrics scanned for the selected range. + /// The first source grapheme insertion boundary in the selection. + /// The final source grapheme insertion boundary in the selection. + /// The orientation used when converting ranges into rectangles. + /// A read-only memory region containing the selection rectangles in visual order. + public static ReadOnlyMemory GetSelectionBounds( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + int graphemeStart, + int graphemeEnd, + LayoutMode layoutMode) + { + if (lines.IsEmpty || graphemes.IsEmpty || graphemeStart == graphemeEnd) + { + return ReadOnlyMemory.Empty; + } + + int selectionStart = Math.Min(graphemeStart, graphemeEnd); + int selectionEnd = Math.Max(graphemeStart, graphemeEnd); + int rectangleCount = CountSelectionBounds(lines, graphemes, selectionStart, selectionEnd); + if (rectangleCount == 0) + { + return ReadOnlyMemory.Empty; + } + + FontRectangle[] result = new FontRectangle[rectangleCount]; + int count = 0; + bool isHorizontal = layoutMode.IsHorizontal(); + + for (int i = 0; i < lines.Length; i++) + { + LineMetrics line = lines[i]; + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + if (CountSelectionBoundsLine(lineGraphemes, selectionStart, selectionEnd) == 0) + { + continue; + } + + count += FillSelectionBoundsLine(line, lineGraphemes, selectionStart, selectionEnd, isHorizontal, result.AsSpan(count)); + } + + return result; + } + + /// + /// Gets selection rectangles for one laid-out line. + /// + /// The single line for which selection rectangles are produced. + /// The line-local grapheme metrics scanned in visual order. + /// The first source grapheme insertion boundary applied to this line. + /// The final source grapheme insertion boundary applied to this line. + /// The orientation used to map the selected run onto the line box. + /// A read-only memory region containing the line selection rectangles in visual order. + public static ReadOnlyMemory GetSelectionBoundsLine( + in LineMetrics line, + ReadOnlySpan graphemes, + int graphemeStart, + int graphemeEnd, + LayoutMode layoutMode) + { + if (graphemes.IsEmpty || graphemeStart == graphemeEnd) + { + return ReadOnlyMemory.Empty; + } + + int selectionStart = Math.Min(graphemeStart, graphemeEnd); + int selectionEnd = Math.Max(graphemeStart, graphemeEnd); + int count = CountSelectionBoundsLine(graphemes, selectionStart, selectionEnd); + if (count == 0) + { + return ReadOnlyMemory.Empty; + } + + FontRectangle[] result = new FontRectangle[count]; + _ = FillSelectionBoundsLine(line, graphemes, selectionStart, selectionEnd, layoutMode.IsHorizontal(), result); + return result; + } + + /// + /// Gets selection bounds for one measured grapheme. + /// + /// The visual lines used to find the grapheme's line box. + /// The flattened grapheme metrics that back the full text box. + /// The measured grapheme to select. + /// The orientation used to map the grapheme advance onto the line box. + /// A read-only memory region containing the grapheme selection bounds. + public static ReadOnlyMemory GetSelectionBounds( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + in GraphemeMetrics grapheme, + LayoutMode layoutMode) + { + if (lines.IsEmpty || graphemes.IsEmpty) + { + return ReadOnlyMemory.Empty; + } + + int lineIndex = FindLineByGraphemeIndex(lines, grapheme.GraphemeIndex); + FontRectangle[] result = [CreateSelectionBounds(lines[lineIndex], grapheme, layoutMode.IsHorizontal())]; + return result; + } + + /// + /// Gets selection bounds for one measured grapheme within one laid-out line. + /// + /// The line that provides the cross-axis selection extent. + /// The measured grapheme to select. + /// The orientation used to map the grapheme advance onto the line box. + /// A read-only memory region containing the grapheme selection bounds. + public static ReadOnlyMemory GetSelectionBoundsLine( + in LineMetrics line, + in GraphemeMetrics grapheme, + LayoutMode layoutMode) + { + FontRectangle[] result = [CreateSelectionBounds(line, grapheme, layoutMode.IsHorizontal())]; + return result; + } + + /// + /// Finds the visual line nearest to a point. + /// + /// The candidate visual lines to compare with the point. + /// The coordinate whose cross-axis position selects the nearest line. + /// Indicates whether line advances are measured along the x-axis. + /// The nearest line index. + private static int FindLine( + ReadOnlySpan lines, + Vector2 point, + bool isHorizontal) + { + float cross = isHorizontal ? point.Y : point.X; + for (int i = 0; i < lines.Length; i++) + { + float lineStart = isHorizontal ? lines[i].Start.Y : lines[i].Start.X; + float lineEnd = isHorizontal ? lines[i].Start.Y + lines[i].Extent.Y : lines[i].Start.X + lines[i].Extent.X; + if (cross >= lineStart && cross < lineEnd) + { + return i; + } + } + + float lineFirstStart = isHorizontal ? lines[0].Start.Y : lines[0].Start.X; + return cross < lineFirstStart ? 0 : lines.Length - 1; + } + + /// + /// Finds the line that owns the supplied grapheme index. + /// + /// The visual lines whose source ranges are searched. + /// The source grapheme index to locate. + /// The nearest owning line index. + private static int FindLineByGraphemeIndex( + ReadOnlySpan lines, + int graphemeIndex) + { + for (int i = 0; i < lines.Length; i++) + { + LineMetrics line = lines[i]; + int lineStart = line.GraphemeIndex; + int lineEnd = lineStart + line.GraphemeCount; + if (graphemeIndex >= lineStart && graphemeIndex <= lineEnd) + { + return i; + } + } + + return 0; + } + + /// + /// Hit tests a point against one laid-out line after the layout mode has been normalized. + /// + /// The zero-based visual index of the normalized line. + /// The grapheme metrics already isolated for that line. + /// The coordinate to compare with each grapheme advance rectangle. + /// Indicates whether the primary hit-test axis is horizontal. + /// The nearest grapheme hit. + private static TextHit HitTestLine( + int lineIndex, + ReadOnlySpan graphemes, + Vector2 point, + bool isHorizontal) + { + int index = FindNearestGrapheme(graphemes, isHorizontal ? point.X : point.Y, isHorizontal); + GraphemeMetrics grapheme = graphemes[index]; + FontRectangle advance = grapheme.Advance; + float midpoint = isHorizontal + ? advance.Left + (advance.Width * 0.5F) + : advance.Top + (advance.Height * 0.5F); + float primary = isHorizontal ? point.X : point.Y; + bool trailing = IsRightToLeft(grapheme) + ? primary < midpoint + : primary >= midpoint; + + return new(lineIndex, grapheme.GraphemeIndex, grapheme.StringIndex, trailing); + } + + /// + /// Creates a caret line for a grapheme insertion index. + /// + /// The zero-based visual index of the caret's line. + /// The line metrics used to size the caret segment. + /// The line-local grapheme metrics searched for neighboring edges. + /// The logical insertion position to materialize as a caret. + /// Indicates whether the caret spans vertically or horizontally. + /// The caret position in pixel units. + private static CaretPosition CreateCaret( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + int graphemeIndex, + bool isHorizontal) + { + int previousIndex = FindGraphemeBySourceIndex(graphemes, graphemeIndex - 1); + int nextIndex = FindGraphemeBySourceIndex(graphemes, graphemeIndex); + + if (nextIndex < 0 && previousIndex < 0) + { + int nearestIndex = FindNearestGraphemeIndex(graphemes, graphemeIndex); + GraphemeMetrics nearest = graphemes[nearestIndex]; + bool trailing = graphemeIndex > nearest.GraphemeIndex; + CreateCaretEdge(line, nearest, trailing, isHorizontal, out Vector2 start, out Vector2 end); + + return new( + lineIndex, + graphemeIndex, + nearest.StringIndex, + start, + end, + false, + default, + default, + GetLineNavigationPosition(start, isHorizontal)); + } + + if (nextIndex >= 0) + { + GraphemeMetrics next = graphemes[nextIndex]; + CreateCaretEdge(line, next, trailing: false, isHorizontal, out Vector2 start, out Vector2 end); + + if (previousIndex >= 0) + { + GraphemeMetrics previous = graphemes[previousIndex]; + CreateCaretEdge(line, previous, trailing: true, isHorizontal, out Vector2 secondaryStart, out Vector2 secondaryEnd); + + // At a bidi boundary the same logical insertion point has one visual edge on + // each neighboring run. Return both instead of asking callers to choose affinity. + if (start != secondaryStart || end != secondaryEnd) + { + return new( + lineIndex, + graphemeIndex, + next.StringIndex, + start, + end, + true, + secondaryStart, + secondaryEnd, + GetLineNavigationPosition(start, isHorizontal)); + } + } + + return new( + lineIndex, + graphemeIndex, + next.StringIndex, + start, + end, + false, + default, + default, + GetLineNavigationPosition(start, isHorizontal)); + } + + GraphemeMetrics previousOnly = graphemes[previousIndex]; + + // Editor-mode hard breaks can create a blank visual line whose only source + // ownership is the preceding newline grapheme. A caret requested immediately + // after that grapheme should sit at the start of the blank line, not after + // the newline marker's trimmed layout box. + if (previousOnly.IsLineBreak && graphemeIndex == previousOnly.GraphemeIndex + 1) + { + Vector2 start; + Vector2 end; + if (isHorizontal) + { + float x = IsRightToLeft(previousOnly) ? line.Start.X + line.Extent.X : line.Start.X; + start = new Vector2(x, line.Start.Y); + end = new Vector2(x, line.Start.Y + line.Extent.Y); + } + else + { + float y = IsRightToLeft(previousOnly) ? line.Start.Y + line.Extent.Y : line.Start.Y; + start = new Vector2(line.Start.X, y); + end = new Vector2(line.Start.X + line.Extent.X, y); + } + + // The newline grapheme gives the blank line source ownership, but the + // editable insertion point after Enter belongs at the new line start. + return new( + lineIndex, + graphemeIndex, + previousOnly.StringIndex, + start, + end, + false, + default, + default, + GetLineNavigationPosition(start, isHorizontal)); + } + + CreateCaretEdge(line, previousOnly, trailing: true, isHorizontal, out Vector2 primaryStart, out Vector2 primaryEnd); + + return new( + lineIndex, + graphemeIndex, + previousOnly.StringIndex, + primaryStart, + primaryEnd, + false, + default, + default, + GetLineNavigationPosition(primaryStart, isHorizontal)); + } + + /// + /// Creates one visual caret edge for a grapheme. + /// + /// The containing line that defines the caret span. + /// The grapheme whose leading or trailing edge is used. + /// Specifies whether the logical trailing side should be chosen. + /// Indicates whether caret edges vary along the x-axis. + /// Receives the first endpoint of the caret segment. + /// Receives the second endpoint of the caret segment. + private static void CreateCaretEdge( + in LineMetrics line, + in GraphemeMetrics grapheme, + bool trailing, + bool isHorizontal, + out Vector2 start, + out Vector2 end) + { + FontRectangle advance = grapheme.Advance; + bool useEnd = IsRightToLeft(grapheme) ? !trailing : trailing; + + if (isHorizontal) + { + // Bidi layout can produce negative advance widths. Left/Right are + // rectangle construction edges in that case, so choose the physical + // min/max x edge after logical leading/trailing has been resolved. + float physicalStart = MathF.Min(advance.Left, advance.Right); + float physicalEnd = MathF.Max(advance.Left, advance.Right); + float x = useEnd ? physicalEnd : physicalStart; + + start = new Vector2(x, line.Start.Y); + end = new Vector2(x, line.Start.Y + line.Extent.Y); + return; + } + + float physicalTop = MathF.Min(advance.Top, advance.Bottom); + float physicalBottom = MathF.Max(advance.Top, advance.Bottom); + float y = useEnd ? physicalBottom : physicalTop; + + start = new Vector2(line.Start.X, y); + end = new Vector2(line.Start.X + line.Extent.X, y); + } + + /// + /// Creates a caret at the source start or end boundary of a laid-out line. + /// + /// The zero-based visual index of the line. + /// The line metrics used to size the caret segment. + /// The line-local grapheme metrics in visual order. + /// The source boundary to place within the line. + /// Indicates whether the caret spans vertically or horizontally. + /// The resolved text direction used to choose the visual start or end of the scope. + /// The caret position at the requested line boundary. + private static CaretPosition CreateCaretAtVisualLineEdge( + int lineIndex, + in LineMetrics line, + ReadOnlySpan graphemes, + CaretPlacement placement, + bool isHorizontal, + TextDirection direction) + { + bool isStart = placement == CaretPlacement.Start; + int insertionIndex = isStart ? GetSourceLineStart(graphemes) : GetSourceLineEnd(graphemes); + int visualIndex = FindGraphemeBySourceIndex(graphemes, isStart ? insertionIndex : insertionIndex - 1); + GraphemeMetrics grapheme = graphemes[visualIndex]; + bool isRightToLeft = direction == TextDirection.RightToLeft; + bool useEnd = isStart == isRightToLeft; + + // Start/end placement is anchored to the source boundary grapheme for + // the returned insertion index, but the visible caret sits on the line + // box edge. The resolved paragraph direction chooses which physical + // line edge represents start or end. + Vector2 start; + Vector2 end; + if (isHorizontal) + { + float x = useEnd ? line.Start.X + line.Extent.X : line.Start.X; + start = new Vector2(x, line.Start.Y); + end = new Vector2(x, line.Start.Y + line.Extent.Y); + } + else + { + float y = useEnd ? line.Start.Y + line.Extent.Y : line.Start.Y; + start = new Vector2(line.Start.X, y); + end = new Vector2(line.Start.X + line.Extent.X, y); + } + + return new( + lineIndex, + insertionIndex, + grapheme.StringIndex, + start, + end, + false, + default, + default, + GetLineNavigationPosition(start, isHorizontal)); + } + + /// + /// Moves the caret to the nearest matching position on an adjacent visual line. + /// + /// The set of visual lines available for adjacent-line navigation. + /// The flattened grapheme metrics used to resolve the new caret target. + /// The caret location before moving to the neighbor line. + /// The visual index of the line that currently contains the caret. + /// Specifies whether movement is toward the next visual line. + /// Indicates whether preserved column data uses the x-axis. + /// The orientation used when reconstructing the destination caret. + /// The moved caret position in pixel units. + private static CaretPosition MoveCaretToAdjacentLine( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + CaretPosition caret, + int lineIndex, + bool lineDown, + bool isHorizontal, + LayoutMode layoutMode) + { + int targetLineIndex = FindAdjacentLine(lines, lineIndex, lineDown, isHorizontal); + if (targetLineIndex == lineIndex) + { + return caret; + } + + LineMetrics targetLine = lines[targetLineIndex]; + int graphemeOffset = GetGraphemeOffset(targetLine); + ReadOnlySpan targetGraphemes = graphemes.Slice(graphemeOffset, targetLine.GraphemeCount); + + Vector2 hitPoint = isHorizontal + ? new(caret.LineNavigationPosition, targetLine.Start.Y + (targetLine.Extent.Y * 0.5F)) + : new(targetLine.Start.X + (targetLine.Extent.X * 0.5F), caret.LineNavigationPosition); + + TextHit hit = HitTestLineForCaretNavigation(targetLineIndex, targetGraphemes, hitPoint, isHorizontal); + CaretPosition moved = GetCaretPositionLine( + targetLineIndex, + targetLine, + targetGraphemes, + hit.GraphemeInsertionIndex, + layoutMode); + + // Preserve the original requested line position so repeated LineUp/LineDown movement + // returns to the same visual column after passing through shorter lines. + return WithLineNavigationPosition(moved, caret.LineNavigationPosition); + } + + /// + /// Hit tests a line for keyboard caret navigation. + /// + /// The zero-based visual index of the line being navigated. + /// The line-local grapheme metrics considered as navigation targets. + /// The projected point used to preserve visual column alignment. + /// Indicates whether navigation compares x coordinates first. + /// The nearest grapheme hit. + private static TextHit HitTestLineForCaretNavigation( + int lineIndex, + ReadOnlySpan graphemes, + Vector2 point, + bool isHorizontal) + { + int index = FindNearestCaretNavigationGrapheme(graphemes, isHorizontal ? point.X : point.Y, isHorizontal); + GraphemeMetrics grapheme = graphemes[index]; + FontRectangle advance = grapheme.Advance; + float midpoint = isHorizontal + ? advance.Left + (advance.Width * 0.5F) + : advance.Top + (advance.Height * 0.5F); + float primary = isHorizontal ? point.X : point.Y; + bool trailing = IsRightToLeft(grapheme) + ? primary < midpoint + : primary >= midpoint; + + return new(lineIndex, grapheme.GraphemeIndex, grapheme.StringIndex, trailing); + } + + /// + /// Finds the nearest grapheme that should participate in keyboard caret navigation. + /// + /// The visual-order graphemes filtered for caret navigation. + /// The coordinate on the primary advance axis to compare. + /// Indicates whether the primary axis maps to horizontal movement. + /// The nearest grapheme metrics index within . + private static int FindNearestCaretNavigationGrapheme( + ReadOnlySpan graphemes, + float primary, + bool isHorizontal) + { + int first = -1; + int last = -1; + for (int i = 0; i < graphemes.Length; i++) + { + first = first < 0 ? i : first; + last = i; + + FontRectangle advance = graphemes[i].Advance; + float start = isHorizontal ? advance.Left : advance.Top; + float end = isHorizontal ? advance.Right : advance.Bottom; + if (primary >= start && primary < end) + { + return i; + } + } + + FontRectangle firstAdvance = graphemes[first].Advance; + float firstStart = isHorizontal ? firstAdvance.Left : firstAdvance.Top; + return primary < firstStart ? first : last; + } + + /// + /// Finds the adjacent visual line in the requested direction. + /// + /// The visual lines among which an adjacent line is searched. + /// The current visual line index. + /// Specifies whether the search moves forward in visual order. + /// Indicates whether cross-axis distances are measured vertically. + /// The adjacent line index, or when no line exists in that direction. + private static int FindAdjacentLine( + ReadOnlySpan lines, + int lineIndex, + bool lineDown, + bool isHorizontal) + { + float currentStart = GetLineCrossStart(lines[lineIndex], isHorizontal); + float currentEnd = GetLineCrossEnd(lines[lineIndex], isHorizontal); + int targetLineIndex = lineIndex; + float bestDistance = float.MaxValue; + for (int i = 0; i < lines.Length; i++) + { + if (i == lineIndex) + { + continue; + } + + float distance = lineDown + ? GetLineCrossStart(lines[i], isHorizontal) - currentEnd + : currentStart - GetLineCrossEnd(lines[i], isHorizontal); + + if (distance >= 0 && distance < bestDistance) + { + targetLineIndex = i; + bestDistance = distance; + } + } + + return targetLineIndex; + } + + /// + /// Gets a valid line index for the supplied caret. + /// + /// The laid-out lines used to validate the caret's stored line index. + /// The flattened grapheme metrics used to resolve the caret when its line index is stale. + /// The caret whose associated visual line must be resolved. + /// The line index. + private static int GetCaretLineIndex( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + in CaretPosition caret) + { + if ((uint)caret.LineIndex < (uint)lines.Length) + { + return caret.LineIndex; + } + + return FindLineByGraphemeIndex(lines, caret.GraphemeIndex); + } + + /// + /// Gets the nearest Unicode word boundary before the supplied grapheme insertion index. + /// + /// The source-order word metrics to search. + /// The grapheme insertion index to move from. + /// The minimum grapheme insertion index that can be returned. + /// The previous word boundary. + private static int GetPreviousWordBoundary( + ReadOnlySpan wordMetrics, + int graphemeIndex, + int limit) + { + int target = limit; + for (int i = 0; i < wordMetrics.Length; i++) + { + WordMetrics metrics = wordMetrics[i]; + if (metrics.GraphemeStart >= graphemeIndex) + { + break; + } + + target = Math.Max(target, metrics.GraphemeStart); + if (metrics.GraphemeEnd < graphemeIndex) + { + target = Math.Max(target, metrics.GraphemeEnd); + } + } + + return target; + } + + /// + /// Gets the nearest Unicode word boundary after the supplied grapheme insertion index. + /// + /// The source-order word metrics to search. + /// The grapheme insertion index to move from. + /// The maximum grapheme insertion index that can be returned. + /// The next word boundary. + private static int GetNextWordBoundary( + ReadOnlySpan wordMetrics, + int graphemeIndex, + int limit) + { + for (int i = 0; i < wordMetrics.Length; i++) + { + WordMetrics metrics = wordMetrics[i]; + if (metrics.GraphemeStart > graphemeIndex) + { + return Math.Min(limit, metrics.GraphemeStart); + } + + if (metrics.GraphemeEnd > graphemeIndex) + { + return Math.Min(limit, metrics.GraphemeEnd); + } + } + + return limit; + } + + /// + /// Gets the previous measured grapheme insertion index. + /// + /// The grapheme metrics that define valid caret stops. + /// The caret insertion index to move from. + /// The minimum grapheme insertion index that can be returned. + /// The previous measured grapheme insertion index. + private static int GetPreviousInsertionIndex( + ReadOnlySpan graphemes, + int graphemeIndex, + int limit) + { + int target = limit; + for (int i = 0; i < graphemes.Length; i++) + { + int start = graphemes[i].GraphemeIndex; + if (start < graphemeIndex) + { + target = Math.Max(target, start); + } + + // The trailing boundary is derived only from an actual measured grapheme. + // This avoids walking through sparse source indices left by trimmed text. + int end = start + 1; + if (end < graphemeIndex) + { + target = Math.Max(target, end); + } + } + + return target; + } + + /// + /// Gets the next measured grapheme insertion index. + /// + /// The grapheme metrics that define valid caret stops. + /// The caret insertion index to move from. + /// The maximum grapheme insertion index that can be returned. + /// The next measured grapheme insertion index. + private static int GetNextInsertionIndex( + ReadOnlySpan graphemes, + int graphemeIndex, + int limit) + { + int target = limit; + for (int i = 0; i < graphemes.Length; i++) + { + int start = graphemes[i].GraphemeIndex; + if (start > graphemeIndex) + { + target = Math.Min(target, start); + } + + // The trailing boundary is derived only from an actual measured grapheme. + // This avoids walking through sparse source indices left by trimmed text. + int end = start + 1; + if (end > graphemeIndex) + { + target = Math.Min(target, end); + } + } + + return target; + } + + /// + /// Gets the first source grapheme insertion index in the laid-out text. + /// + /// The laid-out grapheme metrics searched for the earliest source insertion point. + /// The source text start insertion index. + private static int GetSourceTextStart(ReadOnlySpan graphemes) + { + int start = graphemes[0].GraphemeIndex; + for (int i = 1; i < graphemes.Length; i++) + { + start = Math.Min(start, graphemes[i].GraphemeIndex); + } + + return start; + } + + /// + /// Gets the final source grapheme insertion index in the laid-out text. + /// + /// The laid-out grapheme metrics searched for the final source insertion point. + /// The source text end insertion index. + private static int GetSourceTextEnd(ReadOnlySpan graphemes) + { + int end = graphemes[0].GraphemeIndex + 1; + for (int i = 1; i < graphemes.Length; i++) + { + end = Math.Max(end, graphemes[i].GraphemeIndex + 1); + } + + return end; + } + + /// + /// Gets the first source grapheme insertion index for a line. + /// + /// The line-local grapheme metrics. + /// The source line start insertion index. + private static int GetSourceLineStart(ReadOnlySpan graphemes) + { + int start = graphemes[0].GraphemeIndex; + for (int i = 1; i < graphemes.Length; i++) + { + start = Math.Min(start, graphemes[i].GraphemeIndex); + } + + return start; + } + + /// + /// Gets the final source grapheme insertion index for a line. + /// + /// The line-local grapheme metrics. + /// The source line end insertion index. + private static int GetSourceLineEnd(ReadOnlySpan graphemes) + { + int end = graphemes[0].GraphemeIndex + 1; + for (int i = 1; i < graphemes.Length; i++) + { + end = Math.Max(end, graphemes[i].GraphemeIndex + 1); + } + + return end; + } + + /// + /// Gets the cross-axis start of a line. + /// + /// The line whose cross-axis origin is requested. + /// Indicates whether the cross axis corresponds to y coordinates. + /// The cross-axis start. + private static float GetLineCrossStart(in LineMetrics line, bool isHorizontal) + => isHorizontal ? line.Start.Y : line.Start.X; + + /// + /// Gets the cross-axis end of a line. + /// + /// The line whose cross-axis limit is requested. + /// Indicates whether the cross axis corresponds to y coordinates. + /// The cross-axis end. + private static float GetLineCrossEnd(in LineMetrics line, bool isHorizontal) + => isHorizontal ? line.Start.Y + line.Extent.Y : line.Start.X + line.Extent.X; + + /// + /// Gets the coordinate to preserve for repeated visual line movement. + /// + /// The primary caret endpoint used to preserve visual column movement. + /// Indicates whether the preserved coordinate is taken from x. + /// The line navigation position. + private static float GetLineNavigationPosition(Vector2 start, bool isHorizontal) + => isHorizontal ? start.X : start.Y; + + /// + /// Creates a copy of the caret with a specific preserved line navigation position. + /// + /// The caret value to clone with updated navigation metadata. + /// The preserved visual column or row coordinate. + /// The caret position. + private static CaretPosition WithLineNavigationPosition( + in CaretPosition caret, + float lineNavigationPosition) + => new( + caret.LineIndex, + caret.GraphemeIndex, + caret.StringIndex, + caret.Start, + caret.End, + caret.HasSecondary, + caret.SecondaryStart, + caret.SecondaryEnd, + lineNavigationPosition); + + /// + /// Fills one line's selection rectangles from visually contiguous selected grapheme advances. + /// + /// The line that will receive one or more selection rectangles. + /// The line-local grapheme metrics grouped into visual runs. + /// The first source grapheme insertion boundary in the selected range. + /// The final source grapheme insertion boundary in the selected range. + /// Indicates whether rectangles expand primarily along x. + /// The destination span that receives the generated rectangles. + /// The number of selection rectangles written. + private static int FillSelectionBoundsLine( + in LineMetrics line, + ReadOnlySpan graphemes, + int selectionStart, + int selectionEnd, + bool isHorizontal, + Span result) + { + int count = 0; + bool hasSelection = false; + float start = 0; + float end = 0; + for (int i = 0; i < graphemes.Length; i++) + { + GraphemeMetrics grapheme = graphemes[i]; + + // Selections are caret boundary ranges: [start, end). A grapheme is selected + // when its source start sits inside that boundary span. + int graphemeStart = grapheme.GraphemeIndex; + bool isSelected = graphemeStart >= selectionStart && graphemeStart < selectionEnd; + if (!isSelected) + { + // A logical range can be visually discontinuous after bidi reordering. Flush at + // the first unselected visual grapheme so selection never covers that gap. + if (hasSelection) + { + result[count++] = CreateSelectionBounds(line, start, end, isHorizontal); + hasSelection = false; + } + + continue; + } + + FontRectangle advance = grapheme.Advance; + float currentStart = isHorizontal ? advance.Left : advance.Top; + float currentEnd = isHorizontal ? advance.Right : advance.Bottom; + if (!hasSelection) + { + start = currentStart; + end = currentEnd; + hasSelection = true; + continue; + } + + start = Math.Min(start, currentStart); + end = Math.Max(end, currentEnd); + } + + if (hasSelection) + { + result[count++] = CreateSelectionBounds(line, start, end, isHorizontal); + } + + return count; + } + + /// + /// Creates a selection rectangle for a contiguous visual run. + /// + /// The containing line used to fill the rectangle on the secondary axis. + /// The first selected coordinate along the primary layout axis. + /// The last selected coordinate along the primary layout axis. + /// Indicates whether the primary axis runs left to right. + /// The selection rectangle in pixel units. + private static FontRectangle CreateSelectionBounds( + in LineMetrics line, + float start, + float end, + bool isHorizontal) + => + isHorizontal + ? FontRectangle.FromLTRB(start, line.Start.Y, end, line.Start.Y + line.Extent.Y) + : FontRectangle.FromLTRB(line.Start.X, start, line.Start.X + line.Extent.X, end); + + /// + /// Creates a selection rectangle for one measured grapheme. + /// + /// The containing line used to fill the rectangle on the secondary axis. + /// The grapheme whose advance defines the primary-axis selection extent. + /// Indicates whether the primary axis runs left to right. + /// The selection rectangle in pixel units. + private static FontRectangle CreateSelectionBounds( + in LineMetrics line, + in GraphemeMetrics grapheme, + bool isHorizontal) + { + FontRectangle advance = grapheme.Advance; + float start = isHorizontal ? advance.Left : advance.Top; + float end = isHorizontal ? advance.Right : advance.Bottom; + return CreateSelectionBounds(line, start, end, isHorizontal); + } + + /// + /// Counts how many selection rectangles are required for a grapheme range. + /// + /// The visual lines searched for selected graphemes. + /// The flattened grapheme metrics used to count visual runs. + /// The first source grapheme insertion boundary used for counting. + /// The final source grapheme insertion boundary used for counting. + /// The number of selection rectangles. + private static int CountSelectionBounds( + ReadOnlySpan lines, + ReadOnlySpan graphemes, + int selectionStart, + int selectionEnd) + { + int count = 0; + for (int i = 0; i < lines.Length; i++) + { + LineMetrics line = lines[i]; + int graphemeOffset = GetGraphemeOffset(line); + ReadOnlySpan lineGraphemes = graphemes.Slice(graphemeOffset, line.GraphemeCount); + + // Source grapheme indices can have gaps because trailing whitespace is trimmed. + // Count actual measured graphemes instead of deriving a dense range from the line. + count += CountSelectionBoundsLine(lineGraphemes, selectionStart, selectionEnd); + } + + return count; + } + + /// + /// Counts visually contiguous selected grapheme runs in one line. + /// + /// The visual-order grapheme metrics for the current line. + /// The first source grapheme insertion boundary applied to that line. + /// The final source grapheme insertion boundary applied to that line. + /// The number of selected visual runs. + private static int CountSelectionBoundsLine( + ReadOnlySpan graphemes, + int selectionStart, + int selectionEnd) + { + int count = 0; + bool hasSelection = false; + for (int i = 0; i < graphemes.Length; i++) + { + GraphemeMetrics grapheme = graphemes[i]; + + // Selections are caret boundary ranges: [start, end). A grapheme is selected + // when its source start sits inside that boundary span. + int graphemeStart = grapheme.GraphemeIndex; + bool isSelected = graphemeStart >= selectionStart && graphemeStart < selectionEnd; + + if (!isSelected) + { + hasSelection = false; + continue; + } + + if (!hasSelection) + { + count++; + hasSelection = true; + } + } + + return count; + } + + /// + /// Finds the grapheme whose advance contains the primary coordinate, or the nearest edge grapheme. + /// + /// The visual-order grapheme metrics searched for a hit target. + /// The coordinate along the primary layout axis. + /// Indicates whether the primary axis is horizontal. + /// The nearest grapheme metrics index within . + private static int FindNearestGrapheme(ReadOnlySpan graphemes, float primary, bool isHorizontal) + { + for (int i = 0; i < graphemes.Length; i++) + { + FontRectangle advance = graphemes[i].Advance; + float start = isHorizontal ? advance.Left : advance.Top; + float end = isHorizontal ? advance.Right : advance.Bottom; + if (primary >= start && primary < end) + { + return i; + } + } + + FontRectangle first = graphemes[0].Advance; + float firstStart = isHorizontal ? first.Left : first.Top; + return primary < firstStart ? 0 : graphemes.Length - 1; + } + + /// + /// Finds the metrics entry for a source grapheme index within one visual line. + /// + /// The visual-order grapheme metrics belonging to one line. + /// The logical grapheme index to look up directly. + /// The grapheme metrics index, or -1 when the grapheme is not in the line. + private static int FindGraphemeBySourceIndex(ReadOnlySpan graphemes, int graphemeIndex) + { + for (int i = 0; i < graphemes.Length; i++) + { + if (graphemes[i].GraphemeIndex == graphemeIndex) + { + return i; + } + } + + return -1; + } + + /// + /// Finds the nearest metrics entry for a source grapheme index within one visual line. + /// + /// The visual-order grapheme metrics used for nearest-index matching. + /// The logical grapheme index whose closest visual entry is needed. + /// The nearest grapheme metrics index within . + private static int FindNearestGraphemeIndex(ReadOnlySpan graphemes, int graphemeIndex) + { + int nearest = 0; + int distance = Math.Abs(graphemes[0].GraphemeIndex - graphemeIndex); + for (int i = 1; i < graphemes.Length; i++) + { + int currentDistance = Math.Abs(graphemes[i].GraphemeIndex - graphemeIndex); + if (currentDistance < distance) + { + nearest = i; + distance = currentDistance; + } + } + + return nearest; + } + + /// + /// Gets a value indicating whether the grapheme advances right-to-left in source order. + /// + /// The grapheme whose resolved bidi level is inspected. + /// when the resolved bidi level is odd. + private static bool IsRightToLeft(in GraphemeMetrics grapheme) + => (grapheme.BidiLevel & 1) != 0; + + /// + /// Gets the offset of a line's graphemes within the flattened metrics array. + /// + /// The line whose stored grapheme offset identifies the desired slice. + /// The flattened grapheme metrics offset. + private static int GetGraphemeOffset(in LineMetrics line) + => line.GraphemeOffset; +} diff --git a/src/SixLabors.Fonts/TextInteractionMode.cs b/src/SixLabors.Fonts/TextInteractionMode.cs new file mode 100644 index 00000000..5f01fdd8 --- /dev/null +++ b/src/SixLabors.Fonts/TextInteractionMode.cs @@ -0,0 +1,20 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Specifies how text interaction positions are modeled for laid-out text. +/// +public enum TextInteractionMode +{ + /// + /// Uses paragraph-style interaction where trailing breaking whitespace at line ends does not create additional caret stops. + /// + Paragraph, + + /// + /// Uses editor-style interaction where ordinary trailing breaking whitespace at line ends remains addressable by caret movement and selection. + /// + Editor +} diff --git a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs index 0476f56c..b6344041 100644 --- a/src/SixLabors.Fonts/TextLayout.LineBreaking.cs +++ b/src/SixLabors.Fonts/TextLayout.LineBreaking.cs @@ -1,6 +1,7 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; @@ -10,462 +11,476 @@ namespace SixLabors.Fonts; /// internal static partial class TextLayout { + private const int SoftHyphen = 0x00AD; + private const int StandardHyphen = 0x2010; + private const int StandardEllipsis = 0x2026; + /// - /// Assembles shaped glyphs into instances, applying line-break - /// opportunities derived from and the configured - /// / settings. - /// Finalizes each line (trimming trailing whitespace and applying bidi reordering) and applies - /// justification where requested. + /// Composes the logical from shaped glyph data before width-dependent line breaking. /// + /// The width-independent shaping state. /// The original source text. /// The text shaping and layout options. - /// The resolved bidi runs covering the whole input. - /// The code point to bidi-run mapping built during shaping. - /// The GPOS positioning collection containing the shaped entries. - /// The active layout mode (drives horizontal vs vertical metrics selection). - /// The shaped, line-broken, finalized text box ready for glyph placement. - private static TextBox BreakLines( + /// The logical text line and line break opportunities before line breaking. + public static LogicalTextLine ComposeLogicalLine( + in ShapedText shapedText, ReadOnlySpan text, - TextOptions options, - BidiRun[] bidiRuns, - Dictionary bidiMap, - GlyphPositioningCollection positionings, - LayoutMode layoutMode) + TextOptions options) { - bool shouldWrap = options.WrappingLength > 0; - - // Wrapping length is always provided in pixels. Convert to inches for comparison. - float wrappingLength = shouldWrap ? options.WrappingLength / options.Dpi : float.MaxValue; - bool breakAll = options.WordBreaking == WordBreaking.BreakAll; - bool keepAll = options.WordBreaking == WordBreaking.KeepAll; - bool breakWord = options.WordBreaking == WordBreaking.BreakWord; - bool isHorizontalLayout = layoutMode.IsHorizontal(); - bool isVerticalLayout = layoutMode.IsVertical(); - bool isVerticalMixedLayout = layoutMode.IsVerticalMixed(); + bool isHorizontalLayout = shapedText.LayoutMode.IsHorizontal(); + bool isVerticalLayout = shapedText.LayoutMode.IsVertical(); + bool isVerticalMixedLayout = shapedText.LayoutMode.IsVerticalMixed(); - int graphemeIndex; + int graphemeIndex = 0; int codePointIndex = 0; int glyphSearchIndex = 0; - List textLines = []; TextLine textLine = new(); int stringIndex = 0; + List wordSegments = []; + List hyphenationMarkers = []; + CodePoint? hyphenationMarkerCodePoint = GetHyphenationMarkerCodePoint(options); // No glyph should contain more than 64 metrics. // We do a sanity check below just in case. Span decomposedAdvancesBuffer = stackalloc float[64]; - // Enumerate through each grapheme in the text. - SpanGraphemeEnumerator graphemeEnumerator = new(text); - for (graphemeIndex = 0; graphemeEnumerator.MoveNext(); graphemeIndex++) + // Word-boundary segments are prepared with the logical line, while grapheme + // and codepoint enumeration still own shaping data creation. + SpanWordEnumerator wordEnumerator = new(text); + while (wordEnumerator.MoveNext()) { - // Now enumerate through each codepoint in the grapheme. - ReadOnlySpan grapheme = graphemeEnumerator.Current.Span; - int graphemeCodePointIndex = 0; - SpanCodePointEnumerator codePointEnumerator = new(grapheme); - while (codePointEnumerator.MoveNext()) + WordSegment wordSegment = wordEnumerator.Current; + int wordSegmentGraphemeStart = graphemeIndex; + + SpanGraphemeEnumerator graphemeEnumerator = new(wordSegment.Span); + while (graphemeEnumerator.MoveNext()) { - if (!positionings.TryGetGlyphMetricsAtOffset( - codePointIndex, - ref glyphSearchIndex, - out float pointSize, - out bool isSubstituted, - out bool isVerticalSubstitution, - out bool isDecomposed, - out IReadOnlyList? metrics)) + // Now enumerate through each codepoint in the grapheme. + ReadOnlySpan grapheme = graphemeEnumerator.Current.Span; + int graphemeCodePointIndex = 0; + SpanCodePointEnumerator codePointEnumerator = new(grapheme); + while (codePointEnumerator.MoveNext()) { - // Codepoint was skipped during original enumeration. - codePointIndex++; - graphemeCodePointIndex++; - continue; - } + if (!shapedText.Positionings.TryGetGlyphMetricsAtOffset( + codePointIndex, + ref glyphSearchIndex, + out float pointSize, + out bool isSubstituted, + out bool isVerticalSubstitution, + out bool isDecomposed, + out IReadOnlyList? glyphData)) + { + // Codepoint was skipped during original enumeration. + codePointIndex++; + graphemeCodePointIndex++; + continue; + } - GlyphMetrics glyph = metrics[0]; - - // Retrieve the current codepoint from the enumerator. - // If the glyph represents a substituted codepoint and the substitution is a single codepoint substitution, - // or composite glyph, then the codepoint should be updated to the substitution value so we can read its properties. - // Substitutions that are decomposed glyphs will have multiple metrics and any layout should be based on the - // original codepoint. - // - // Note: Not all glyphs in a font will have a codepoint associated with them. e.g. most compositions, ligatures, etc. - CodePoint codePoint = codePointEnumerator.Current; - if (isSubstituted && metrics.Count == 1) - { - codePoint = glyph.CodePoint; - } + List metrics = []; + for (int i = 0; i < glyphData.Count; i++) + { + GlyphPositioningCollection.GlyphPositioningData data = glyphData[i]; + if (data.Data.IsPlaceholder) + { + textLine.AddPlaceholder( + data, + graphemeIndex, + stringIndex, + isHorizontalLayout, + isVerticalMixedLayout, + options.LineSpacing); + + continue; + } - // Determine whether the glyph advance should be calculated using vertical or horizontal metrics - // For vertical mixed layout we will rotate glyphs with the vertical orientation type R or TR - // which do not already have a vertical substitution. - bool shouldRotate = isVerticalMixedLayout && - !isVerticalSubstitution && - CodePoint.GetVerticalOrientationType(codePoint) is - VerticalOrientationType.Rotate or - VerticalOrientationType.TransformRotate; - - // Determine whether the glyph advance should be offset for vertical layout. - bool shouldOffset = isVerticalLayout && - !isVerticalSubstitution && - CodePoint.GetVerticalOrientationType(codePoint) is - VerticalOrientationType.Rotate or - VerticalOrientationType.TransformRotate; - - if (CodePoint.IsVariationSelector(codePoint)) - { - codePointIndex++; - graphemeCodePointIndex++; - continue; - } + metrics.Add(data.Metrics); + } - // Calculate the advance for the current codepoint. + if (metrics.Count == 0) + { + // This source codepoint was skipped during shaping; any placeholder + // sharing the same source offset has already been added above. + codePointIndex++; + graphemeCodePointIndex++; + continue; + } - // This should never happen, but we need to ensure that the buffer is large enough - // if, for some crazy reason, a glyph does contain more than 64 metrics. - Span decomposedAdvances = metrics.Count > decomposedAdvancesBuffer.Length - ? new float[metrics.Count] - : decomposedAdvancesBuffer[..(isDecomposed ? metrics.Count : 1)]; + FontGlyphMetrics glyph = metrics[0]; + + // Retrieve the current codepoint from the enumerator. + // If the glyph represents a substituted codepoint and the substitution is a single codepoint substitution, + // or composite glyph, then the codepoint should be updated to the substitution value so we can read its properties. + // Substitutions that are decomposed glyphs will have multiple metrics and any layout should be based on the + // original codepoint. + // + // Note: Not all glyphs in a font will have a codepoint associated with them. e.g. most compositions, ligatures, etc. + CodePoint codePoint = codePointEnumerator.Current; + if (isSubstituted && metrics.Count == 1) + { + codePoint = glyph.CodePoint; + } - float glyphAdvance; - if (isHorizontalLayout || shouldRotate) - { - glyphAdvance = glyph.AdvanceWidth; - } - else - { - glyphAdvance = glyph.AdvanceHeight; - } + // Determine whether the glyph advance should be calculated using vertical or horizontal metrics + // For vertical mixed layout we will rotate glyphs with the vertical orientation type R or TR + // which do not already have a vertical substitution. + bool shouldRotate = isVerticalMixedLayout && + !isVerticalSubstitution && + CodePoint.GetVerticalOrientationType(codePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; + + // Determine whether the glyph advance should be offset for vertical layout. + bool shouldOffset = isVerticalLayout && + !isVerticalSubstitution && + CodePoint.GetVerticalOrientationType(codePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; + + if (CodePoint.IsVariationSelector(codePoint)) + { + codePointIndex++; + graphemeCodePointIndex++; + continue; + } - decomposedAdvances[0] = glyphAdvance; + // Calculate the advance for the current codepoint. - if (CodePoint.IsTabulation(codePoint)) - { - if (options.TabWidth > -1F) + // This should never happen, but we need to ensure that the buffer is large enough + // if, for some crazy reason, a glyph does contain more than 64 metrics. + Span decomposedAdvances = metrics.Count > decomposedAdvancesBuffer.Length + ? new float[metrics.Count] + : decomposedAdvancesBuffer[..(isDecomposed ? metrics.Count : 1)]; + + float glyphAdvance; + if (isHorizontalLayout || shouldRotate) { - // Do not use the default font tab width. Instead find the advance for the space glyph - // and multiply that by the options value. - CodePoint space = new(0x0020); - if (glyph.FontMetrics.TryGetGlyphId(space, out ushort spaceGlyphId)) - { - GlyphMetrics spaceMetrics = glyph.FontMetrics.GetGlyphMetrics( - space, - spaceGlyphId, - glyph.TextAttributes, - glyph.TextDecorations, - layoutMode, - options.ColorFontSupport); - - if (isHorizontalLayout || shouldRotate) - { - glyphAdvance = spaceMetrics.AdvanceWidth * options.TabWidth; - glyph.SetAdvanceWidth((ushort)glyphAdvance); - } - else - { - glyphAdvance = spaceMetrics.AdvanceHeight * options.TabWidth; - glyph.SetAdvanceHeight((ushort)glyphAdvance); - } - } + glyphAdvance = glyph.AdvanceWidth; } - } - else if (metrics.Count == 1 && (CodePoint.IsZeroWidthJoiner(codePoint) || CodePoint.IsZeroWidthNonJoiner(codePoint))) - { - // The zero-width joiner characters should be ignored when determining word or - // line break boundaries so are safe to skip here. Any existing instances are the result of font error - // unless multiple metrics are associated with code point. In this case they are most likely the result - // of a substitution and shouldn't be ignored. - glyphAdvance = 0; - decomposedAdvances[0] = 0; - } - else if (!CodePoint.IsNewLine(codePoint)) - { - // Standard text. - // If decomposed we need to add the advance; otherwise, use the largest advance for the metrics. - if (isHorizontalLayout || shouldRotate) + else + { + glyphAdvance = glyph.AdvanceHeight; + } + + decomposedAdvances[0] = glyphAdvance; + + bool isSoftHyphen = codePoint.Value == SoftHyphen; + if (isSoftHyphen) + { + glyphAdvance = 0; + decomposedAdvances[0] = 0; + } + else if (CodePoint.IsTabulation(codePoint)) { - for (int i = 1; i < metrics.Count; i++) + if (options.TabWidth > -1F) { - float a = metrics[i].AdvanceWidth; - if (isDecomposed) - { - glyphAdvance += a; - decomposedAdvances[i] = a; - } - else if (a > glyphAdvance) + // Do not use the default font tab width. Instead find the advance for the space glyph + // and multiply that by the options value. + CodePoint space = new(0x0020); + if (glyph.FontMetrics.TryGetGlyphId(space, out ushort spaceGlyphId)) { - glyphAdvance = a; + FontGlyphMetrics spaceMetrics = glyph.FontMetrics.GetGlyphMetrics( + space, + spaceGlyphId, + glyph.TextAttributes, + glyph.TextDecorations, + shapedText.LayoutMode, + options.ColorFontSupport); + + if (isHorizontalLayout || shouldRotate) + { + glyphAdvance = spaceMetrics.AdvanceWidth * options.TabWidth; + glyph.SetAdvanceWidth((ushort)glyphAdvance); + } + else + { + glyphAdvance = spaceMetrics.AdvanceHeight * options.TabWidth; + glyph.SetAdvanceHeight((ushort)glyphAdvance); + } } } } - else + else if (metrics.Count == 1 && (CodePoint.IsZeroWidthJoiner(codePoint) || CodePoint.IsZeroWidthNonJoiner(codePoint))) + { + // The zero-width joiner characters should be ignored when determining word or + // line break boundaries so are safe to skip here. Any existing instances are the result of font error + // unless multiple metrics are associated with code point. In this case they are most likely the result + // of a substitution and shouldn't be ignored. + glyphAdvance = 0; + decomposedAdvances[0] = 0; + } + else if (!CodePoint.IsNewLine(codePoint)) { - for (int i = 1; i < metrics.Count; i++) + // Standard text. + // If decomposed we need to add the advance; otherwise, use the largest advance for the metrics. + if (isHorizontalLayout || shouldRotate) { - float a = metrics[i].AdvanceHeight; - if (isDecomposed) + for (int i = 1; i < metrics.Count; i++) { - glyphAdvance += a; - decomposedAdvances[i] = a; + float a = metrics[i].AdvanceWidth; + if (isDecomposed) + { + glyphAdvance += a; + decomposedAdvances[i] = a; + } + else if (a > glyphAdvance) + { + glyphAdvance = a; + } } - else if (a > glyphAdvance) + } + else + { + for (int i = 1; i < metrics.Count; i++) { - glyphAdvance = a; + float a = metrics[i].AdvanceHeight; + if (isDecomposed) + { + glyphAdvance += a; + decomposedAdvances[i] = a; + } + else if (a > glyphAdvance) + { + glyphAdvance = a; + } } } } - } - // Now scale the advance. We use inches for comparison. - if (isHorizontalLayout || shouldRotate) - { - float scaleAX = pointSize / glyph.ScaleFactor.X; - glyphAdvance *= scaleAX; - for (int i = 0; i < decomposedAdvances.Length; i++) + // Now scale the advance. We use inches for comparison. + if (isHorizontalLayout || shouldRotate) { - decomposedAdvances[i] *= scaleAX; + float scaleAX = pointSize / glyph.ScaleFactor.X; + glyphAdvance *= scaleAX; + for (int i = 0; i < decomposedAdvances.Length; i++) + { + decomposedAdvances[i] *= scaleAX; + } } - } - else - { - float scaleAY = pointSize / glyph.ScaleFactor.Y; - glyphAdvance *= scaleAY; - for (int i = 0; i < decomposedAdvances.Length; i++) + else { - decomposedAdvances[i] *= scaleAY; + float scaleAY = pointSize / glyph.ScaleFactor.Y; + glyphAdvance *= scaleAY; + for (int i = 0; i < decomposedAdvances.Length; i++) + { + decomposedAdvances[i] *= scaleAY; + } } - } - int graphemeCodePointMax = CodePoint.GetCodePointCount(grapheme) - 1; + int graphemeCodePointMax = CodePoint.GetCodePointCount(grapheme) - 1; - // For non-decomposed glyphs the length is always 1. - for (int i = 0; i < decomposedAdvances.Length; i++) - { - // Determine if this is the last codepoint in the grapheme. - bool isLastInGrapheme = graphemeCodePointIndex == graphemeCodePointMax && i == decomposedAdvances.Length - 1; + // For non-decomposed glyphs the length is always 1. + for (int i = 0; i < decomposedAdvances.Length; i++) + { + // Determine if this is the last codepoint in the grapheme. + bool isLastInGrapheme = graphemeCodePointIndex == graphemeCodePointMax && i == decomposedAdvances.Length - 1; - float decomposedAdvance = decomposedAdvances[i]; + float decomposedAdvance = decomposedAdvances[i]; - // Work out the scaled metrics for the glyph. - GlyphMetrics metric = metrics[i]; + // Work out the scaled metrics for the glyph. + FontGlyphMetrics metric = metrics[i]; - // Adjust the advance for the last decomposed glyph to add tracking if applicable. - // Tracking should only be added once per grapheme, so only on the last codepoint of the grapheme. - if (isLastInGrapheme && options.Tracking != 0 && i == decomposedAdvances.Length - 1) - { - // Tracking should not be applied to tab characters or non-rendered codepoints. - if (!CodePoint.IsTabulation(codePoint) && !UnicodeUtility.ShouldNotBeRendered(codePoint)) + // Adjust the advance for the last decomposed glyph to add tracking if applicable. + // Tracking should only be added once per grapheme, so only on the last codepoint of the grapheme. + if (isLastInGrapheme && options.Tracking != 0 && i == decomposedAdvances.Length - 1) { - if (isHorizontalLayout || shouldRotate) - { - float scaleAX = pointSize / glyph.ScaleFactor.X; - decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAX; - } - else + // Tracking should not be applied to tab characters or non-rendered codepoints. + if (!CodePoint.IsTabulation(codePoint) && !UnicodeUtility.ShouldNotBeRendered(codePoint)) { - float scaleAY = pointSize / glyph.ScaleFactor.Y; - decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAY; + if (isHorizontalLayout || shouldRotate) + { + float scaleAX = pointSize / glyph.ScaleFactor.X; + decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAX; + } + else + { + float scaleAY = pointSize / glyph.ScaleFactor.Y; + decomposedAdvance += options.Tracking * metric.FontMetrics.UnitsPerEm * scaleAY; + } } } - } - // Convert design-space units to pixels based on the target point size. - // ScaleFactor.Y represents the vertical UPEM scaling factor for this glyph. - float scaleY = pointSize / metric.ScaleFactor.Y; + // Convert design-space units to pixels based on the target point size. + // ScaleFactor.Y represents the vertical UPEM scaling factor for this glyph. + float scaleY = pointSize / metric.ScaleFactor.Y; - // Choose which metrics table to use based on layout orientation. - // Horizontal is the default; vertical fonts use VMTX if available. - IMetricsHeader metricsHeader = isHorizontalLayout || shouldRotate - ? metric.FontMetrics.HorizontalMetrics - : metric.FontMetrics.VerticalMetrics; + // Choose which metrics table to use based on layout orientation. + // Horizontal is the default; vertical fonts use VMTX if available. + IMetricsHeader metricsHeader = isHorizontalLayout || shouldRotate + ? metric.FontMetrics.HorizontalMetrics + : metric.FontMetrics.VerticalMetrics; - // Ascender and descender are stored in font design units, so scale them to pixels. - float ascender = metricsHeader.Ascender * scaleY; + // Ascender and descender are stored in font design units, so scale them to pixels. + float ascender = metricsHeader.Ascender * scaleY; - // Match browser line-height calculation logic. - // Reference: https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height - // The line height in CSS is based on a multiple of the font-size (pointSize), - // but fonts may define a custom LineHeight in their metrics that differs from UPEM. - float descender = Math.Abs(metricsHeader.Descender * scaleY); - float lineHeight = metric.UnitsPerEm * scaleY; + // Match browser line-height calculation logic. + // Reference: https://www.w3.org/TR/CSS2/visudet.html#propdef-line-height + // The line height in CSS is based on a multiple of the font-size (pointSize), + // but fonts may define a custom LineHeight in their metrics that differs from UPEM. + float descender = Math.Abs(metricsHeader.Descender * scaleY); + float lineHeight = metric.UnitsPerEm * scaleY; - // The delta centers the font's line box within the CSS line box when - // LineHeight differs from the nominal font size. - float delta = ((metricsHeader.LineHeight * scaleY) - lineHeight) * 0.5F; + // The delta centers the font's line box within the CSS line box when + // LineHeight differs from the nominal font size. + float delta = ((metricsHeader.LineHeight * scaleY) - lineHeight) * 0.5F; - // Adjust ascender and descender symmetrically by delta to preserve visual balance. - ascender -= delta; - descender -= delta; + // Adjust ascender and descender symmetrically by delta to preserve visual balance. + ascender -= delta; + descender -= delta; - GlyphLayoutMode mode = GlyphLayoutMode.Horizontal; - if (isVerticalLayout) - { - mode = GlyphLayoutMode.Vertical; - } - else if (isVerticalMixedLayout) - { - mode = shouldRotate ? GlyphLayoutMode.VerticalRotated : GlyphLayoutMode.Vertical; + GlyphLayoutMode mode = GlyphLayoutMode.Horizontal; + if (isVerticalLayout) + { + mode = GlyphLayoutMode.Vertical; + } + else if (isVerticalMixedLayout) + { + mode = shouldRotate ? GlyphLayoutMode.VerticalRotated : GlyphLayoutMode.Vertical; + } + + int hyphenationMarkerIndex = -1; + if (isSoftHyphen && hyphenationMarkerCodePoint.HasValue) + { + // U+00AD is shaped as an invisible source entry, but if this exact + // discretionary break is later selected we need a visible marker with + // the same run, font attributes, bidi mapping, and source mapping. Build + // that marker here while those values are already in hand; BreakLines can + // then account for its advance without rescanning or reshaping the line. + hyphenationMarkerIndex = hyphenationMarkers.Count; + hyphenationMarkers.Add(CreateGeneratedMarker( + glyph, + pointSize, + shapedText.BidiRuns[shapedText.BidiMap[codePointIndex]], + graphemeIndex, + isLastInGrapheme, + codePointIndex, + graphemeCodePointIndex, + stringIndex, + hyphenationMarkerCodePoint.Value, + shapedText.LayoutMode, + options)); + } + + // Add our metrics to the line. + textLine.Add( + isDecomposed ? new FontGlyphMetrics[] { metric } : metrics, + pointSize, + decomposedAdvance, + lineHeight, + ascender, + descender, + delta, + shapedText.BidiRuns[shapedText.BidiMap[codePointIndex]], + graphemeIndex, + isLastInGrapheme, + codePointIndex, + graphemeCodePointIndex, + shouldRotate || shouldOffset, + isDecomposed, + stringIndex, + mode, + options.LineSpacing, + hyphenationMarkerIndex); } - // Add our metrics to the line. - textLine.Add( - isDecomposed ? new GlyphMetrics[] { metric } : metrics, - pointSize, - decomposedAdvance, - lineHeight, - ascender, - descender, - delta, - bidiRuns[bidiMap[codePointIndex]], - graphemeIndex, - isLastInGrapheme, - codePointIndex, - graphemeCodePointIndex, - shouldRotate || shouldOffset, - isDecomposed, - stringIndex, - mode, - options.LineSpacing); + codePointIndex++; + graphemeCodePointIndex++; } - codePointIndex++; - graphemeCodePointIndex++; + stringIndex += grapheme.Length; + graphemeIndex++; } - stringIndex += grapheme.Length; + wordSegments.Add(new WordSegmentRun( + wordSegmentGraphemeStart, + graphemeIndex, + wordSegment.Utf16Offset, + wordSegment.Utf16Offset + wordSegment.Utf16Length)); } - // Calculate the break opportunities once. The wrapping loop below may scan them - // repeatedly as each finalized line is split off from the remaining text. - List lineBreaks = CollectLineBreaks(text); - - int processed = 0; - while (textLine.Count > 0) + // Placeholders do not consume source text. A placeholder inserted at + // the final source position has no following codepoint to visit in + // the main loop, so we add those trailing placeholder entries here. + if (shapedText.Positionings.TryGetGlyphMetricsAtOffset( + codePointIndex, + ref glyphSearchIndex, + out _, + out _, + out _, + out _, + out IReadOnlyList? endGlyphData)) { - LineBreak? bestBreak = null; - foreach (LineBreak lineBreak in lineBreaks) + for (int i = 0; i < endGlyphData.Count; i++) { - // Skip breaks that are already behind the processed portion - if (lineBreak.PositionWrap <= processed) - { - continue; - } - - // Measure the text up to the adjusted break point - float advance = textLine.MeasureAt(lineBreak.PositionMeasure - processed); - if (advance >= wrappingLength) - { - bestBreak ??= lineBreak; - break; - } - - // If it's a mandatory break, stop immediately - if (lineBreak.Required) + GlyphPositioningCollection.GlyphPositioningData data = endGlyphData[i]; + if (data.Data.IsPlaceholder) { - bestBreak = lineBreak; - break; + textLine.AddPlaceholder( + data, + graphemeIndex, + stringIndex, + isHorizontalLayout, + isVerticalMixedLayout, + options.LineSpacing); } - - // Update the best break - bestBreak = lineBreak; } + } - if (bestBreak != null) - { - LineBreak breakAt = bestBreak.Value; - if (breakAll) - { - // Break-all works differently to the other modes. - // It will break at any character so we simply toggle the breaking operation depending - // on whether the break is required. - TextLine? remaining; - if (bestBreak.Value.Required) - { - if (textLine.TrySplitAt(breakAt, keepAll, out remaining)) - { - processed = breakAt.PositionWrap; - textLines.Add(textLine.Finalize(true)); - textLine = remaining; - } - } - else if (textLine.TrySplitAt(wrappingLength, out remaining)) - { - processed += textLine.Count; - textLines.Add(textLine.Finalize()); - textLine = remaining; - } - else - { - processed += textLine.Count; - } - } - else - { - // Split the current line at the adjusted break index - if (textLine.TrySplitAt(breakAt, keepAll, out TextLine? remaining)) - { - // If 'keepAll' is true then the break could be later than expected. - processed = keepAll - ? processed + Math.Max(textLine.Count, breakAt.PositionWrap - processed) - : breakAt.PositionWrap; + // Line break candidates are width-independent and belong with the composed logical line. + List lineBreaks = CollectLineBreaks(text, hyphenationMarkerCodePoint.HasValue); - if (breakWord) - { - // A break was found, but we need to check if the line is too long - // and break if required. - if (textLine.ScaledLineAdvance > wrappingLength && - textLine.TrySplitAt(wrappingLength, out TextLine? overflow)) - { - // Reinsert the overflow at the beginning of the remaining line - processed -= overflow.Count; - remaining.InsertAt(0, overflow); - } - } + return new LogicalTextLine(textLine, lineBreaks, wordSegments, hyphenationMarkers); + } - // Add the split part to the list and continue processing. - textLines.Add(textLine.Finalize(breakAt.Required)); - textLine = remaining; - } - else - { - processed += textLine.Count; - } - } - } - else - { - // We're at the last line break which should be at the end of the - // text. We can break here and finalize the line. - if (breakWord || breakAll) - { - while (textLine.ScaledLineAdvance > wrappingLength) - { - if (!textLine.TrySplitAt(wrappingLength, out TextLine? overflow)) - { - break; - } + /// + /// Applies line-break opportunities to a shaped using the configured + /// behavior and supplied wrapping length. + /// Finalizes each line (trimming trailing whitespace and applying bidi reordering) and applies + /// justification where requested. + /// + /// The logical text line and line break opportunities to break. + /// The text shaping and layout options. + /// The wrapping length in pixels. + /// The shaped, line-broken, finalized text box ready for glyph placement. + public static TextBox BreakLines( + in LogicalTextLine logicalLine, + TextOptions options, + float wrappingLength) + { + int maxLines = options.MaxLines; - textLines.Add(textLine.Finalize()); - textLine = overflow; - } - } + if (maxLines == 0) + { + TextDirection emptyTextDirection = options.TextDirection == TextDirection.RightToLeft + ? TextDirection.RightToLeft + : TextDirection.LeftToRight; - textLines.Add(textLine.Finalize(true)); - break; - } + return new TextBox([], emptyTextDirection); } - // Finally we justify each line that does not end a paragraph. - for (int i = 0; i < textLines.Count; i++) + TextDirection textDirection = GetTextDirection(logicalLine, options); + + List textLines = []; + TextLineBreakEnumerator lineEnumerator = new(logicalLine, options); + + while (lineEnumerator.MoveNext(wrappingLength)) { - TextLine line = textLines[i]; - if (!line.SkipJustification) - { - line.Justify(options); - } + textLines.Add(lineEnumerator.Current); } - return new TextBox(textLines); + return new TextBox(textLines, textDirection); } + /// + /// Gets the block-level text direction for a prepared logical line. + /// + /// The prepared logical line. + /// The text options used for layout. + /// The block-level text direction. + public static TextDirection GetTextDirection(in LogicalTextLine logicalLine, TextOptions options) + => options.TextDirection == TextDirection.Auto && logicalLine.TextLine.Count > 0 + ? logicalLine.TextLine[0].TextDirection + : options.TextDirection; + /// /// Collects the line break opportunities used by the wrapping loop. /// @@ -492,16 +507,150 @@ VerticalOrientationType.Rotate or /// /// /// The original source text being laid out. + /// Whether soft-hyphen break opportunities should be included. /// The ordered line break opportunities after layout-level tailoring. - private static List CollectLineBreaks(ReadOnlySpan text) + private static List CollectLineBreaks(ReadOnlySpan text, bool includeHyphenationBreaks) { LineBreakEnumerator lineBreakEnumerator = new(text, tailorUrls: true); List lineBreaks = []; while (lineBreakEnumerator.MoveNext()) { - lineBreaks.Add(lineBreakEnumerator.Current); + LineBreak lineBreak = lineBreakEnumerator.Current; + if (lineBreak.IsHyphenationBreak && !includeHyphenationBreaks) + { + continue; + } + + lineBreaks.Add(lineBreak); } return lineBreaks; } + + private static CodePoint? GetHyphenationMarkerCodePoint(TextOptions options) + => options.TextHyphenation switch + { + TextHyphenation.Standard => new CodePoint(StandardHyphen), + TextHyphenation.Custom => options.CustomHyphen, + _ => null + }; + + /// + /// Creates a visible generated marker that matches the layout style of the anchor entry. + /// + /// The glyph metric that supplies font, run, attributes, and decorations. + /// The point size at which the marker is rendered. + /// The bidi run that the marker belongs to. + /// The source grapheme index to map the marker to. + /// Whether the marker maps to the last entry in its grapheme. + /// The source codepoint index to map the marker to. + /// The source codepoint-in-grapheme index to map the marker to. + /// The UTF-16 source index to map the marker to. + /// The marker codepoint to create. + /// The layout mode used to calculate marker orientation. + /// The text options used for layout. + /// The generated marker entry. + internal static GlyphLayoutData CreateGeneratedMarker( + FontGlyphMetrics anchorMetric, + float pointSize, + BidiRun bidiRun, + int graphemeIndex, + bool isLastInGrapheme, + int codePointIndex, + int graphemeCodePointIndex, + int stringIndex, + CodePoint markerCodePoint, + LayoutMode layoutMode, + TextOptions options) + { + anchorMetric.FontMetrics.TryGetGlyphId(markerCodePoint, out ushort markerGlyphId); + + FontGlyphMetrics markerMetric = anchorMetric.FontMetrics.GetGlyphMetrics( + markerCodePoint, + markerGlyphId, + anchorMetric.TextAttributes, + anchorMetric.TextDecorations, + layoutMode, + options.ColorFontSupport); + + markerMetric = markerMetric.CloneForRendering(anchorMetric.TextRun); + + bool isHorizontalLayout = layoutMode.IsHorizontal(); + bool isVerticalLayout = layoutMode.IsVertical(); + bool isVerticalMixedLayout = layoutMode.IsVerticalMixed(); + bool shouldRotate = isVerticalMixedLayout && + CodePoint.GetVerticalOrientationType(markerCodePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; + + bool shouldOffset = isVerticalLayout && + CodePoint.GetVerticalOrientationType(markerCodePoint) is + VerticalOrientationType.Rotate or + VerticalOrientationType.TransformRotate; + + GlyphLayoutMode markerMode = GlyphLayoutMode.Horizontal; + if (isVerticalLayout) + { + markerMode = GlyphLayoutMode.Vertical; + } + else if (isVerticalMixedLayout) + { + markerMode = shouldRotate ? GlyphLayoutMode.VerticalRotated : GlyphLayoutMode.Vertical; + } + + float markerAdvance = isHorizontalLayout || shouldRotate + ? markerMetric.AdvanceWidth * (pointSize / markerMetric.ScaleFactor.X) + : markerMetric.AdvanceHeight * (pointSize / markerMetric.ScaleFactor.Y); + + // Generated markers must reserve the same CSS line box as ordinary glyphs + // from the same run so truncation and discretionary hyphens do not collapse + // or expand line spacing. + float markerScaleY = pointSize / markerMetric.ScaleFactor.Y; + IMetricsHeader markerMetricsHeader = isHorizontalLayout || shouldRotate + ? markerMetric.FontMetrics.HorizontalMetrics + : markerMetric.FontMetrics.VerticalMetrics; + + float markerAscender = markerMetricsHeader.Ascender * markerScaleY; + float markerDescender = Math.Abs(markerMetricsHeader.Descender * markerScaleY); + float markerLineHeight = markerMetric.UnitsPerEm * markerScaleY; + float markerDelta = ((markerMetricsHeader.LineHeight * markerScaleY) - markerLineHeight) * 0.5F; + + markerAscender -= markerDelta; + markerDescender -= markerDelta; + + FontRectangle markerBox = FontGlyphMetrics.ShouldSkipGlyphRendering(markerMetric.CodePoint) + ? FontRectangle.Empty + : markerMetric.GetBoundingBox(markerMode, Vector2.Zero, pointSize); + + return new GlyphLayoutData( + new FontGlyphMetrics[] { markerMetric }, + pointSize, + markerAdvance, + markerLineHeight * options.LineSpacing, + markerAscender, + markerDescender, + markerDelta, + MathF.Min(0, markerBox.Y), + bidiRun, + graphemeIndex, + isLastInGrapheme, + codePointIndex, + graphemeCodePointIndex, + shouldRotate || shouldOffset, + false, + stringIndex); + } + + /// + /// Gets the configured ellipsis marker codepoint. + /// + /// The text options used for layout. + /// The configured ellipsis marker codepoint, or when ellipsis is disabled. + public static CodePoint? GetEllipsisMarkerCodePoint(TextOptions options) + => options.TextEllipsis switch + { + TextEllipsis.Standard => new CodePoint(StandardEllipsis), + TextEllipsis.Custom => options.CustomEllipsis, + _ => null + }; } diff --git a/src/SixLabors.Fonts/TextLayout.Visitors.cs b/src/SixLabors.Fonts/TextLayout.Visitors.cs index f2678587..749c8ff7 100644 --- a/src/SixLabors.Fonts/TextLayout.Visitors.cs +++ b/src/SixLabors.Fonts/TextLayout.Visitors.cs @@ -4,104 +4,32 @@ namespace SixLabors.Fonts; /// -/// Visitor types for streaming laid-out glyphs through . +/// Visitor types for streaming laid-out glyphs through the layout pipeline. /// internal static partial class TextLayout { /// - /// Receives laid-out glyphs streamed from . + /// Receives laid-out glyphs streamed from the layout pipeline. /// Implementations are value types so the generic dispatch is specialized by the JIT and no boxing or /// delegate allocation is required. /// internal interface IGlyphLayoutVisitor { /// - /// Invoked once for each laid-out glyph in layout order. - /// - /// The laid-out glyph. - public void Visit(in GlyphLayout glyph); - } - - /// - /// Collects streamed glyphs into a . - /// - internal readonly struct GlyphLayoutCollector : IGlyphLayoutVisitor - { - /// - /// Initializes a new instance of the struct. + /// Invoked before glyphs are streamed for a laid-out line. /// - /// The list to collect streamed glyphs into. - public GlyphLayoutCollector(List glyphs) => this.Glyphs = glyphs; - - /// - /// Gets the accumulated glyphs. - /// - public List Glyphs { get; } - - /// - public readonly void Visit(in GlyphLayout glyph) => this.Glyphs.Add(glyph); - } - - /// - /// Accumulates the union of glyph ink bounds as glyphs are streamed, avoiding the allocation - /// of a and a second iteration pass. - /// - internal struct GlyphBoundsAccumulator : IGlyphLayoutVisitor - { - private readonly float dpi; - private float left; - private float top; - private float right; - private float bottom; - private bool any; + /// The zero-based index of the line in the line-broken text box. + public void BeginLine(int lineIndex); /// - /// Initializes a new instance of the struct. + /// Invoked once for each laid-out glyph in layout order. /// - /// The device-independent pixels per unit for the containing . - public GlyphBoundsAccumulator(float dpi) - { - this.dpi = dpi; - this.left = float.MaxValue; - this.top = float.MaxValue; - this.right = float.MinValue; - this.bottom = float.MinValue; - this.any = false; - } - - /// - public void Visit(in GlyphLayout glyph) - { - FontRectangle box = glyph.BoundingBox(this.dpi); - - if (box.Left < this.left) - { - this.left = box.Left; - } - - if (box.Top < this.top) - { - this.top = box.Top; - } - - if (box.Right > this.right) - { - this.right = box.Right; - } - - if (box.Bottom > this.bottom) - { - this.bottom = box.Bottom; - } - - this.any = true; - } + /// The laid-out glyph. + public void Visit(in GlyphLayout glyph); /// - /// Returns the accumulated ink bounds, or if no glyphs were visited. + /// Invoked after glyphs have been streamed for a laid-out line. /// - /// The union of the ink bounds of all visited glyphs. - public readonly FontRectangle Result() - => this.any ? FontRectangle.FromLTRB(this.left, this.top, this.right, this.bottom) : FontRectangle.Empty; + public void EndLine(); } } diff --git a/src/SixLabors.Fonts/TextLayout.cs b/src/SixLabors.Fonts/TextLayout.cs index ff6a131b..f6bf442c 100644 --- a/src/SixLabors.Fonts/TextLayout.cs +++ b/src/SixLabors.Fonts/TextLayout.cs @@ -1,8 +1,6 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using System.Diagnostics; -using System.Diagnostics.CodeAnalysis; using System.Numerics; using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Unicode; @@ -10,32 +8,10 @@ namespace SixLabors.Fonts; /// -/// Encapsulated logic or laying out text. +/// Encapsulates logic for laying out text. /// internal static partial class TextLayout { - /// - /// Shapes the supplied text and returns every laid-out glyph in layout order. - /// - /// - /// Equivalent to followed by . - /// Prefer for callers that only need aggregate ink bounds, - /// or the streaming overload to avoid materializing the glyph list. - /// - /// The text to lay out. - /// The text shaping and layout options. - /// The laid-out glyphs in layout order, or an empty list when is empty. - public static IReadOnlyList GenerateLayout(ReadOnlySpan text, TextOptions options) - { - if (text.IsEmpty) - { - return Array.Empty(); - } - - TextBox textBox = ProcessText(text, options); - return LayoutText(textBox, options); - } - /// /// Resolves the ordered sequence of instances that cover . /// @@ -45,10 +21,17 @@ public static IReadOnlyList GenerateLayout(ReadOnlySpan text, /// supplied runs are ordered, gaps are filled with default-font runs, and overlapping ranges are trimmed. /// /// The text to partition into runs. - /// The text shaping options supplying the default font and optional user-defined runs. + /// The text options supplying the default font and optional user-defined runs. /// The resolved runs that together cover the entire grapheme range of . public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, TextOptions options) { + int start = 0; + int end = text.GetGraphemeCount(); + if (end == 0) + { + return []; + } + if (options.TextRuns is null || options.TextRuns.Count == 0) { return new TextRun[] @@ -62,8 +45,6 @@ public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Text }; } - int start = 0; - int end = text.GetGraphemeCount(); List textRuns = []; foreach (TextRun textRun in options.TextRuns.OrderBy(x => x.Start)) { @@ -81,6 +62,11 @@ public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Text // Add the current run, ensuring the font is not null. textRun.Font ??= options.Font; + if (textRun.Placeholder.HasValue && textRun.End != textRun.Start) + { + throw new ArgumentException("Placeholder text runs must be zero-length insertion runs.", nameof(options)); + } + // Ensure that the previous run does not overlap the current. if (textRuns.Count > 0) { @@ -108,18 +94,17 @@ public static IReadOnlyList BuildTextRuns(ReadOnlySpan text, Text } /// - /// Shapes and line-breaks into a ready for layout. + /// Shapes into shaping state that is independent of the wrapping length. /// /// /// Performs the font-run build, bidi analysis, GSUB/GPOS shaping (including fallback font - /// resolution for unmapped codepoints), and line breaking. The result is a sequence of - /// entries with resolved glyph metrics but no pen positioning — positioning - /// is applied later by . + /// resolution for unmapped codepoints). The result contains the positioned glyph collection + /// and bidi state used by logical line composition. /// /// The text to process. - /// The text shaping options. - /// The shaped, line-broken text ready for glyph positioning. - internal static TextBox ProcessText(ReadOnlySpan text, TextOptions options) + /// The text options used while shaping. + /// The wrapping-independent shaping state. + public static ShapedText ShapeText(ReadOnlySpan text, TextOptions options) { // Gather the font and fallbacks. Font[] fallbackFonts = (options.FallbackFontFamilies?.Count > 0) @@ -135,13 +120,33 @@ internal static TextBox ProcessText(ReadOnlySpan text, TextOptions options BidiData bidiData = new(); bidiData.Init(text, (sbyte)options.TextDirection); - // If we have embedded directional overrides then change those - // ranges to neutral. - if (options.TextDirection != TextDirection.Auto) + if (options.TextBidiMode == TextBidiMode.Override) { - bidiData.SaveTypes(); - bidiData.Types.Span.Fill(BidiCharacterType.OtherNeutral); - bidiData.PairedBracketTypes.Span.Clear(); + BidiCharacterType overrideType = options.TextDirection == TextDirection.Auto + ? (bidi.ResolveEmbeddingLevel(bidiData.Types) == 1 ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight) + : (options.TextDirection == TextDirection.RightToLeft ? BidiCharacterType.RightToLeft : BidiCharacterType.LeftToRight); + + for (int i = 0; i < bidiData.Types.Length; i++) + { + // Bidi override is a higher-level protocol override: real text behaves as the requested + // strong direction, while separators and explicit bidi controls keep their structural role. + bidiData.Types[i] = bidiData.Types[i] switch + { + BidiCharacterType.ParagraphSeparator + or BidiCharacterType.SegmentSeparator + or BidiCharacterType.BoundaryNeutral + or BidiCharacterType.LeftToRightEmbedding + or BidiCharacterType.RightToLeftEmbedding + or BidiCharacterType.LeftToRightOverride + or BidiCharacterType.RightToLeftOverride + or BidiCharacterType.PopDirectionalFormat + or BidiCharacterType.LeftToRightIsolate + or BidiCharacterType.RightToLeftIsolate + or BidiCharacterType.FirstStrongIsolate + or BidiCharacterType.PopDirectionalIsolate => bidiData.Types[i], + _ => overrideType, + }; + } } bidi.Process(bidiData); @@ -160,6 +165,35 @@ internal static TextBox ProcessText(ReadOnlySpan text, TextOptions options int bidiRunIndex = 0; foreach (TextRun textRun in textRuns) { + if (textRun.Placeholder.HasValue) + { + substitutions.Clear(); + + while (bidiRunIndex < bidiRuns.Length && codePointIndex == bidiRuns[bidiRunIndex].End) + { + bidiRunIndex++; + } + + // Placeholder direction comes from the bidi region at the insertion + // point. If the insertion point is after all source text, use the + // default even/LTR embedding level. + BidiRun placeholderBidiRun = bidiRunIndex < bidiRuns.Length + ? bidiRuns[bidiRunIndex] + : new(BidiCharacterType.LeftToRight, 2, codePointIndex, 0); + + // Placeholder runs are inserted into the layout stream and do not consume + // source graphemes, source codepoints, or bidi runs. + substitutions.AddPlaceholder( + CodePoint.ObjectReplacementChar, + placeholderBidiRun, + textRun, + codePointIndex); + + complete &= positionings.TryAdd(textRun.Font!, substitutions); + textRunIndex++; + continue; + } + if (!DoFontRun( textRun.Slice(text), textRun.Start, @@ -228,52 +262,12 @@ internal static TextBox ProcessText(ReadOnlySpan text, TextOptions options font.FontMetrics.UpdatePositions(positionings); } - return BreakLines(text, options, bidiRuns, bidiMap, positionings, layoutMode); - } - - /// - /// Lays out the supplied and materializes every glyph into a - /// . - /// - /// - /// Prefer the streaming overload when the caller only needs - /// aggregated state (for example ink bounds), to avoid allocating the list. - /// - /// The shaped and line-broken text. - /// The text shaping options used to shape . - /// The laid-out glyphs in layout order. - internal static List LayoutText(TextBox textBox, TextOptions options) - { - GlyphLayoutCollector visitor = new([]); - LayoutText(textBox, options, ref visitor); - return visitor.Glyphs; - } - - /// - /// Lays out the supplied and returns the union of the ink bounds of - /// every emitted glyph in a single streaming pass. - /// - /// - /// Equivalent to iterating and unioning each - /// glyph's , but avoids materializing the glyph list and - /// the second iteration pass. - /// - /// The shaped and line-broken text. - /// The text shaping options used to shape . - /// - /// The union of the ink bounds of every laid-out glyph, or - /// if no glyphs were emitted. - /// - internal static FontRectangle GetBounds(TextBox textBox, TextOptions options) - { - GlyphBoundsAccumulator visitor = new(options.Dpi); - LayoutText(textBox, options, ref visitor); - return visitor.Result(); + return new ShapedText(positionings, bidiRuns, bidiMap, layoutMode); } /// /// Lays out the supplied , streaming each laid-out glyph through the - /// supplied in layout order. + /// supplied in layout order using the supplied wrapping length for alignment. /// /// /// The visitor type is constrained to a struct implementing @@ -281,22 +275,32 @@ internal static FontRectangle GetBounds(TextBox textBox, TextOptions options) /// /// The concrete visitor struct type. /// The shaped and line-broken text. - /// The text shaping options used to shape . - /// The visitor that receives each laid-out glyph. - internal static void LayoutText(TextBox textBox, TextOptions options, ref TVisitor visitor) + /// The text options used to lay out . + /// The wrapping length in pixels. Use -1 to disable wrapping. + /// The visitor that receives each positioned glyph. + public static void LayoutText( + TextBox textBox, + TextOptions options, + float wrappingLength, + ref TVisitor visitor) where TVisitor : struct, IGlyphLayoutVisitor { + if (textBox.TextLines.Count == 0) + { + return; + } + LayoutMode layoutMode = options.LayoutMode; Vector2 boxLocation = options.Origin / options.Dpi; Vector2 penLocation = boxLocation; - // If a wrapping length is specified that should be used to determine the - // box size to align text within. + // When wrapping is enabled, the wrapping length defines the minimum line-box + // extent used by alignment. float maxScaledAdvance = textBox.ScaledMaxAdvance(); - if (options.TextAlignment != TextAlignment.Start && options.WrappingLength > 0) + if (options.TextAlignment != TextAlignment.Start && wrappingLength > 0) { - maxScaledAdvance = Math.Max(options.WrappingLength / options.Dpi, maxScaledAdvance); + maxScaledAdvance = Math.Max(wrappingLength / options.Dpi, maxScaledAdvance); } TextDirection direction = textBox.TextDirection(); @@ -305,6 +309,7 @@ internal static void LayoutText(TextBox textBox, TextOptions options, { for (int i = 0; i < textBox.TextLines.Count; i++) { + visitor.BeginLine(i); LayoutLineHorizontal( textBox, textBox.TextLines[i], @@ -315,6 +320,8 @@ internal static void LayoutText(TextBox textBox, TextOptions options, ref boxLocation, ref penLocation, ref visitor); + + visitor.EndLine(); } } else if (layoutMode == LayoutMode.HorizontalBottomTop) @@ -322,6 +329,7 @@ internal static void LayoutText(TextBox textBox, TextOptions options, int index = 0; for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { + visitor.BeginLine(i); LayoutLineHorizontal( textBox, textBox.TextLines[i], @@ -332,12 +340,15 @@ internal static void LayoutText(TextBox textBox, TextOptions options, ref boxLocation, ref penLocation, ref visitor); + + visitor.EndLine(); } } else if (layoutMode is LayoutMode.VerticalLeftRight) { for (int i = 0; i < textBox.TextLines.Count; i++) { + visitor.BeginLine(i); LayoutLineVertical( textBox, textBox.TextLines[i], @@ -348,6 +359,8 @@ internal static void LayoutText(TextBox textBox, TextOptions options, ref boxLocation, ref penLocation, ref visitor); + + visitor.EndLine(); } } else if (layoutMode is LayoutMode.VerticalRightLeft) @@ -355,6 +368,7 @@ internal static void LayoutText(TextBox textBox, TextOptions options, int index = 0; for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { + visitor.BeginLine(i); LayoutLineVertical( textBox, textBox.TextLines[i], @@ -365,12 +379,15 @@ internal static void LayoutText(TextBox textBox, TextOptions options, ref boxLocation, ref penLocation, ref visitor); + + visitor.EndLine(); } } else if (layoutMode is LayoutMode.VerticalMixedLeftRight) { for (int i = 0; i < textBox.TextLines.Count; i++) { + visitor.BeginLine(i); LayoutLineVerticalMixed( textBox, textBox.TextLines[i], @@ -381,6 +398,8 @@ internal static void LayoutText(TextBox textBox, TextOptions options, ref boxLocation, ref penLocation, ref visitor); + + visitor.EndLine(); } } else @@ -388,6 +407,7 @@ internal static void LayoutText(TextBox textBox, TextOptions options, int index = 0; for (int i = textBox.TextLines.Count - 1; i >= 0; i--) { + visitor.BeginLine(i); LayoutLineVerticalMixed( textBox, textBox.TextLines[i], @@ -398,6 +418,8 @@ internal static void LayoutText(TextBox textBox, TextOptions options, ref boxLocation, ref penLocation, ref visitor); + + visitor.EndLine(); } } } @@ -412,7 +434,7 @@ internal static void LayoutText(TextBox textBox, TextOptions options, /// The line being laid out. /// The resolved text direction for this line. /// The widest scaled line advance in the block (or wrapping length). - /// The text shaping and layout options. + /// The text options used to position the line. /// The zero-based visual index of this line within the block. /// The running top-left position of the glyph boxes; advanced by this method. /// The running pen position used for glyph placement; advanced by this method. @@ -521,21 +543,32 @@ private static void LayoutLineHorizontal( } penLocation.X += offsetX; + Vector2 boundsLocation = boxLocation; bool emitted = false; for (int i = 0; i < textLine.Count; i++) { - TextLine.GlyphLayoutData data = textLine[i]; + GlyphLayoutData data = textLine[i]; + float layoutAdvance = data.ScaledAdvance; + if (data.IsNewLine) { - visitor.Visit(new GlyphLayout( - new Glyph(data.Metrics[0], data.PointSize), - boxLocation, + FontGlyphMetrics metric = data.Metrics[0]; + + // Hard breaks bypass the normal glyph loop, but still need the + // current pen position plus the same baseline origin used by glyphs. + Vector2 hardBreakGlyphOrigin = penLocation + new Vector2(0, textLine.ScaledMaxAscender); + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + boundsLocation, + hardBreakGlyphOrigin, penLocation, - Vector2.Zero, data.ScaledAdvance, yLineAdvance, GlyphLayoutMode.Horizontal, + data.BidiRun.Level, true, data.GraphemeIndex, data.StringIndex)); @@ -544,20 +577,26 @@ private static void LayoutLineHorizontal( penLocation.Y += yLineAdvance; boxLocation.X = originX; boxLocation.Y += advanceY; + boundsLocation.X = originX; + boundsLocation.Y += advanceY; return; } int j = 0; - foreach (GlyphMetrics metric in data.Metrics) + foreach (FontGlyphMetrics metric in data.Metrics) { - visitor.Visit(new GlyphLayout( + Vector2 glyphOrigin = penLocation + new Vector2(0, textLine.ScaledMaxAscender); + + visitor.Visit( + new GlyphLayout( new Glyph(metric, data.PointSize), - boxLocation, - penLocation + new Vector2(0, textLine.ScaledMaxAscender), - Vector2.Zero, + boundsLocation, + glyphOrigin, + glyphOrigin, data.ScaledAdvance, advanceY, GlyphLayoutMode.Horizontal, + data.BidiRun.Level, i == 0 && j == 0, data.GraphemeIndex, data.StringIndex)); @@ -566,8 +605,9 @@ private static void LayoutLineHorizontal( j++; } - boxLocation.X += data.ScaledAdvance; - penLocation.X += data.ScaledAdvance; + boxLocation.X += layoutAdvance; + penLocation.X += layoutAdvance; + boundsLocation.X += data.ScaledAdvance; } boxLocation.X = originX; @@ -590,7 +630,7 @@ private static void LayoutLineHorizontal( /// The line being laid out. /// The resolved text direction for this line. /// The longest scaled line advance in the block (or wrapping length). - /// The text shaping and layout options. + /// The text options used to position the line. /// The zero-based visual index of this line within the block. /// The running top-left position of the glyph boxes; advanced by this method. /// The running pen position used for glyph placement; advanced by this method. @@ -607,7 +647,6 @@ private static void LayoutLineVertical( ref TVisitor visitor) where TVisitor : struct, IGlyphLayoutVisitor { - float originX = penLocation.X; float originY = penLocation.Y; float offsetY = 0; @@ -693,12 +732,14 @@ private static void LayoutLineVertical( penLocation.X += offsetX; float lineOriginX = penLocation.X; + Vector2 boundsLocation = boxLocation; + float boundsLineOriginX = boundsLocation.X; bool emitted = false; // Grapheme-scoped state for transformed glyph alignment. // - // IMPORTANT: TextLine.GlyphLayoutData is per-codepoint, not per-grapheme. + // IMPORTANT: GlyphLayoutData is per-codepoint, not per-grapheme. // Complex scripts can therefore produce multiple entries for a single grapheme. // For example Devanagari "र्कि" can end up as two entries ("र्" and "कि") even though it // visually shapes as a single cluster. @@ -712,17 +753,30 @@ private static void LayoutLineVertical( for (int i = 0; i < textLine.Count; i++) { - TextLine.GlyphLayoutData data = textLine[i]; + GlyphLayoutData data = textLine[i]; + float layoutAdvance = data.ScaledAdvance; + float scaledLineHeight = data.ScaledLineHeight / options.LineSpacing; + if (data.IsNewLine) { - visitor.Visit(new GlyphLayout( - new Glyph(data.Metrics[0], data.PointSize), - boxLocation, - penLocation, - Vector2.Zero, + FontGlyphMetrics metric = data.Metrics[0]; + Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + + // Hard breaks bypass the normal glyph loop, but still need the + // current pen position plus the same vertical glyph origin adjustment. + Vector2 hardBreakDecorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); + Vector2 hardBreakGlyphOrigin = hardBreakDecorationOrigin + new Vector2(0, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + boundsLocation, + hardBreakGlyphOrigin, + hardBreakDecorationOrigin, xLineAdvance, data.ScaledAdvance, GlyphLayoutMode.Vertical, + data.BidiRun.Level, true, data.GraphemeIndex, data.StringIndex)); @@ -731,6 +785,8 @@ private static void LayoutLineVertical( boxLocation.Y = originY; penLocation.X += xLineAdvance; penLocation.Y = originY; + boundsLocation.X += advanceX; + boundsLocation.Y = originY; return; } @@ -752,7 +808,7 @@ private static void LayoutLineVertical( for (int k = i; k < textLine.Count; k++) { - TextLine.GlyphLayoutData g = textLine[k]; + GlyphLayoutData g = textLine[k]; if (g.GraphemeIndex != graphemeIndex) { @@ -782,14 +838,14 @@ private static void LayoutLineVertical( for (int k = i; k < textLine.Count; k++) { - TextLine.GlyphLayoutData g = textLine[k]; + GlyphLayoutData g = textLine[k]; if (g.GraphemeIndex != graphemeIndex) { break; } - foreach (GlyphMetrics m in g.Metrics) + foreach (FontGlyphMetrics m in g.Metrics) { Vector2 s = new Vector2(g.PointSize) / m.ScaleFactor; @@ -810,10 +866,13 @@ private static void LayoutLineVertical( float inkWidth = maxX - minX; - // Normalize ink minX to 0 and center within the column width. + // Normalize ink minX to 0 and center within the entry's own line box. + // The decoration origin has already centered that entry line box within + // the widest line box, so using the widest line box here would apply the + // mixed-size offset twice. // This is grapheme-correct and avoids centering based only on the "first" entry, // which is not representative for marks like reph in Devanagari. - currentGraphemeAlignX = -minX + ((unscaledLineHeight - inkWidth) * .5F); + currentGraphemeAlignX = -minX + ((scaledLineHeight - inkWidth) * .5F); } } @@ -826,20 +885,31 @@ private static void LayoutLineVertical( // Transformed glyphs are still positioned using horizontal metrics (`AdvanceWidth`) even though // they participate in a vertical flow. `AdvanceWidth` gives us the horizontal pen advance we must // apply between entries inside the transformed grapheme. - foreach (GlyphMetrics m in data.Metrics) + foreach (FontGlyphMetrics m in data.Metrics) { Vector2 s = new Vector2(data.PointSize) / m.ScaleFactor; entryScaledAdvanceWidth += m.AdvanceWidth * s.X; } } - foreach (GlyphMetrics metric in data.Metrics) + foreach (FontGlyphMetrics metric in data.Metrics) { // Align the glyph horizontally and vertically centering vertically around the baseline. Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + float glyphAlignX = alignX; + + if (!currentGraphemeIsTransformed) + { + // Vertical origin fallback places the vertical origin at half the + // horizontal advance. The decoration origin has already centered this + // entry's line box in the column, so center the glyph advance inside it. + glyphAlignX = (scaledLineHeight - (metric.AdvanceWidth * scale.X)) * .5F; + } - // Offset our in both directions to account for horizontal ink centering and vertical baseline centering. - Vector2 offset = new(alignX, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + // Move the glyph origin without changing the advance or decoration origin. + Vector2 glyphOffset = new(glyphAlignX, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + Vector2 decorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); + Vector2 glyphOrigin = decorationOrigin + glyphOffset; float advanceW = advanceX; @@ -851,14 +921,16 @@ private static void LayoutLineVertical( advanceW = scale.X * metric.AdvanceWidth; } - visitor.Visit(new GlyphLayout( + visitor.Visit( + new GlyphLayout( new Glyph(metric, data.PointSize), - boxLocation, - penLocation + new Vector2((unscaledLineHeight - (data.ScaledLineHeight / options.LineSpacing)) * .5F, 0), - offset, + boundsLocation, + glyphOrigin, + decorationOrigin, advanceW, data.ScaledAdvance, GlyphLayoutMode.Vertical, + data.BidiRun.Level, i == 0 && j == 0, data.GraphemeIndex, data.StringIndex)); @@ -874,11 +946,18 @@ private static void LayoutLineVertical( penLocation.X += entryScaledAdvanceWidth; } + if (currentGraphemeIsTransformed) + { + boundsLocation.X += entryScaledAdvanceWidth; + } + if (data.IsLastInGrapheme) { - penLocation.Y += data.ScaledAdvance; + penLocation.Y += layoutAdvance; boxLocation.X = lineOriginX; penLocation.X = lineOriginX; + boundsLocation.Y += data.ScaledAdvance; + boundsLocation.X = boundsLineOriginX; } } @@ -902,7 +981,7 @@ private static void LayoutLineVertical( /// The line being laid out. /// The resolved text direction for this line. /// The longest scaled line advance in the block (or wrapping length). - /// The text shaping and layout options. + /// The text options used to position the line. /// The zero-based visual index of this line within the block. /// The running top-left position of the glyph boxes; advanced by this method. /// The running pen position used for glyph placement; advanced by this method. @@ -1002,21 +1081,35 @@ private static void LayoutLineVerticalMixed( penLocation.Y += offsetY; penLocation.X += offsetX; + Vector2 boundsLocation = boxLocation; bool emitted = false; for (int i = 0; i < textLine.Count; i++) { - TextLine.GlyphLayoutData data = textLine[i]; + GlyphLayoutData data = textLine[i]; + float layoutAdvance = data.ScaledAdvance; + float scaledLineHeight = data.ScaledLineHeight / options.LineSpacing; + if (data.IsNewLine) { - visitor.Visit(new GlyphLayout( - new Glyph(data.Metrics[0], data.PointSize), - boxLocation, - penLocation, - Vector2.Zero, + FontGlyphMetrics metric = data.Metrics[0]; + Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; + + // Hard breaks bypass the normal glyph loop, but still need the + // current pen position plus the same vertical glyph origin adjustment. + Vector2 hardBreakDecorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); + Vector2 hardBreakGlyphOrigin = hardBreakDecorationOrigin + new Vector2(0, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + + visitor.Visit( + new GlyphLayout( + new Glyph(metric, data.PointSize), + boundsLocation, + hardBreakGlyphOrigin, + hardBreakDecorationOrigin, xLineAdvance, data.ScaledAdvance, GlyphLayoutMode.Vertical, + data.BidiRun.Level, true, data.GraphemeIndex, data.StringIndex)); @@ -1025,13 +1118,15 @@ private static void LayoutLineVerticalMixed( boxLocation.Y = originY; penLocation.X += xLineAdvance; penLocation.Y = originY; + boundsLocation.X += advanceX; + boundsLocation.Y = originY; return; } if (data.IsTransformed) { int j = 0; - foreach (GlyphMetrics metric in data.Metrics) + foreach (FontGlyphMetrics metric in data.Metrics) { // The glyph will be rotated 90 degrees for vertical mixed layout. // We still advance along Y, but the glyphs are laid out sideways in X. @@ -1040,7 +1135,7 @@ private static void LayoutLineVerticalMixed( // - Take half the difference between the max line height (scaledMaxLineHeight) // and the current glyph's line height (data.ScaledLineHeight). // - The line height includes both ascender and descender metrics. - float baselineDelta = (unscaledLineHeight - (data.ScaledLineHeight / options.LineSpacing)) * .5F; + float baselineDelta = (unscaledLineHeight - scaledLineHeight) * .5F; // Adjust the horizontal offset further by considering the descender differences: // - Subtract the current glyph's descender (data.ScaledDescender) to align it properly. @@ -1048,15 +1143,18 @@ private static void LayoutLineVerticalMixed( float descenderDelta = (Math.Abs(textLine.ScaledMaxDescender) - descenderAbs) * .5F; float centerOffsetX = baselineDelta + descenderAbs + descenderDelta; + Vector2 glyphOrigin = penLocation + new Vector2(centerOffsetX, 0); - visitor.Visit(new GlyphLayout( + visitor.Visit( + new GlyphLayout( new Glyph(metric, data.PointSize), - boxLocation, - penLocation + new Vector2(centerOffsetX, 0), - Vector2.Zero, + boundsLocation, + glyphOrigin, + glyphOrigin, advanceX, data.ScaledAdvance, GlyphLayoutMode.VerticalRotated, + data.BidiRun.Level, i == 0 && j == 0, data.GraphemeIndex, data.StringIndex)); @@ -1068,20 +1166,29 @@ private static void LayoutLineVerticalMixed( else { int j = 0; - foreach (GlyphMetrics metric in data.Metrics) + foreach (FontGlyphMetrics metric in data.Metrics) { // Align the glyph horizontally and vertically centering vertically around the baseline. Vector2 scale = new Vector2(data.PointSize) / metric.ScaleFactor; - Vector2 offset = new(0, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); - visitor.Visit(new GlyphLayout( + // Vertical origin fallback places the vertical origin at half the + // horizontal advance. The decoration origin has already centered this + // entry's line box in the column, so center the glyph advance inside it. + float glyphAlignX = (scaledLineHeight - (metric.AdvanceWidth * scale.X)) * .5F; + Vector2 glyphOffset = new(glyphAlignX, (metric.Bounds.Max.Y + metric.TopSideBearing) * scale.Y); + Vector2 decorationOrigin = penLocation + new Vector2((unscaledLineHeight - scaledLineHeight) * .5F, 0); + Vector2 glyphOrigin = decorationOrigin + glyphOffset; + + visitor.Visit( + new GlyphLayout( new Glyph(metric, data.PointSize), - boxLocation, - penLocation + new Vector2((unscaledLineHeight - (data.ScaledLineHeight / options.LineSpacing)) * .5F, 0), - offset, + boundsLocation, + glyphOrigin, + decorationOrigin, advanceX, data.ScaledAdvance, GlyphLayoutMode.Vertical, + data.BidiRun.Level, i == 0 && j == 0, data.GraphemeIndex, data.StringIndex)); @@ -1091,7 +1198,8 @@ private static void LayoutLineVerticalMixed( } } - penLocation.Y += data.ScaledAdvance; + penLocation.Y += layoutAdvance; + boundsLocation.Y += data.ScaledAdvance; } boxLocation.Y = originY; @@ -1155,7 +1263,7 @@ private static bool DoFontRun( int graphemeCodePointIndex = 0; int charIndex = 0; - if (graphemeIndex == textRuns[textRunIndex].End) + while (textRunIndex < textRuns.Count - 1 && graphemeIndex == textRuns[textRunIndex].End) { textRunIndex++; } @@ -1409,937 +1517,4 @@ internal static float CalculateLineOffsetY( return offsetY; } - - /// - /// A shaped and line-broken block of text produced by and consumed - /// by . - /// - internal sealed class TextBox - { - private float? scaledMaxAdvance; - - private float? minY; - - /// - /// Initializes a new instance of the class. - /// - /// The shaped, line-broken lines that make up this text box. - public TextBox(IReadOnlyList textLines) - => this.TextLines = textLines; - - /// - /// Gets the shaped and line-broken lines that make up the text. - /// - public IReadOnlyList TextLines { get; } - - /// - /// Returns the widest scaled line advance across all lines. The result is memoized. - /// - /// The widest scaled line advance. - public float ScaledMaxAdvance() - => this.scaledMaxAdvance ??= this.TextLines.Max(x => x.ScaledLineAdvance); - - /// - /// Returns the smallest (most negative) scaled Y position encountered across all lines. - /// Used to detect ink that extends above the typographic ascender (stacked marks in Tibetan etc.). - /// The result is memoized. - /// - /// The smallest scaled Y position in the text box. - public float ScaledMinY() - => this.minY ??= this.TextLines.Min(x => x.ScaledMinY); - - /// - /// Returns the resolved text direction of the first glyph in the first line. Used as the - /// block-level direction for alignment calculations. - /// - /// The block-level text direction. - public TextDirection TextDirection() => this.TextLines[0][0].TextDirection; - } - - /// - /// A shaped line of text — an ordered sequence of entries plus - /// per-line aggregate metrics (advance, ascender, descender, etc.) used to position the line - /// during layout. - /// - internal sealed class TextLine - { - private readonly List data; - private readonly Dictionary advances = []; - - /// - /// Initializes a new instance of the class with a small default capacity. - /// - public TextLine() => this.data = new(16); - - /// - /// Initializes a new instance of the class with the specified initial - /// entry capacity. - /// - /// Initial capacity for the internal entry list. - public TextLine(int capacity) => this.data = new(capacity); - - /// - /// Gets the number of entries in this line. - /// - public int Count => this.data.Count; - - /// - /// Gets a value indicating whether this line should be skipped during text justification. - /// Set by for lines that end a paragraph. - /// - public bool SkipJustification { get; private set; } - - /// - /// Gets the sum of scaled advances across all entries in this line. - /// - public float ScaledLineAdvance { get; private set; } - - /// - /// Gets the greatest scaled line height across all entries, multiplied by the configured - /// line-spacing factor. - /// - public float ScaledMaxLineHeight { get; private set; } = -1; - - /// - /// Gets the greatest scaled ascender across all entries in this line. - /// - public float ScaledMaxAscender { get; private set; } = -1; - - /// - /// Gets the greatest scaled descender across all entries in this line. - /// - public float ScaledMaxDescender { get; private set; } = -1; - - /// - /// Gets the greatest scaled symmetric-metrics delta across all entries in this line. - /// Browsers adjust ascender/descender symmetrically for baseline alignment; this captures - /// that adjustment. - /// - public float ScaledMaxDelta { get; private set; } = float.MinValue; - - /// - /// Gets the smallest (most negative) scaled Y position across all entries in this line. - /// Used to detect ink that extends above the typographic ascender (for example stacked - /// marks in Tibetan) so the layout engine can reserve extra ascent. - /// - public float ScaledMinY { get; private set; } - - /// - /// Gets the entry at the given index. - /// - /// The zero-based index into this line. - /// The entry at the given index. - public GlyphLayoutData this[int index] => this.data[index]; - - /// - /// Appends a shaped entry to this line, updating the aggregated line-level metrics. - /// - /// The glyph metrics produced by shaping this entry's codepoint. - /// The point size at which the entry is rendered. - /// The scaled advance contributed by this entry. - /// The scaled line height contributed by this entry (before line-spacing). - /// The scaled typographic ascender. - /// The scaled typographic descender. - /// The symmetric metrics delta applied during line-box construction. - /// The bidi run this entry belongs to. - /// The grapheme index in the source text. - /// Whether this entry is the last codepoint in its grapheme cluster. - /// The codepoint index in the source text. - /// The index of the codepoint within its grapheme cluster. - /// Whether the entry participates in a transformed (rotated) vertical layout. - /// Whether the entry was produced by Unicode decomposition. - /// The character index in the source string. - /// The glyph-level layout mode to use for ink bounds computation. - /// The line-spacing factor to apply to . - public void Add( - IReadOnlyList metrics, - float pointSize, - float scaledAdvance, - float scaledLineHeight, - float scaledAscender, - float scaledDescender, - float scaledDelta, - BidiRun bidiRun, - int graphemeIndex, - bool isLastInGrapheme, - int codePointIndex, - int graphemeCodePointIndex, - bool isTransformed, - bool isDecomposed, - int stringIndex, - GlyphLayoutMode layoutMode, - float lineSpacing) - { - // Apply LineSpacing to scaledLineHeight before storing - scaledLineHeight *= lineSpacing; - - // Reset metrics. - // We track the maximum metrics for each line to ensure glyphs can be aligned. - if (graphemeCodePointIndex == 0) - { - // TODO: Check this logic is correct. - this.ScaledLineAdvance += scaledAdvance; - } - - this.ScaledMaxLineHeight = MathF.Max(this.ScaledMaxLineHeight, scaledLineHeight); - this.ScaledMaxAscender = MathF.Max(this.ScaledMaxAscender, scaledAscender); - this.ScaledMaxDescender = MathF.Max(this.ScaledMaxDescender, scaledDescender); - this.ScaledMaxDelta = MathF.Max(this.ScaledMaxDelta, scaledDelta); - - // Track the true top of the ink in device space (Y down, baseline at 0). - // For scripts with stacked marks (Tibetan, etc) this can be significantly - // above the typographic ascender, so we cannot trust ascender alone. - float scaledMinY = 0; - for (int i = 0; i < metrics.Count; i++) - { - GlyphMetrics metric = metrics[i]; - if (GlyphMetrics.ShouldSkipGlyphRendering(metric.CodePoint)) - { - continue; - } - - FontRectangle bbox = metric.GetBoundingBox(layoutMode, Vector2.Zero, pointSize); - scaledMinY = MathF.Min(scaledMinY, bbox.Y); - } - - // ScaledMinY is the minimum ink Y over all glyphs in this line, in Y down. - // It is usually <= 0; more negative means more ink above the baseline. - if (this.data.Count == 0) - { - this.ScaledMinY = scaledMinY; - } - else - { - this.ScaledMinY = MathF.Min(this.ScaledMinY, scaledMinY); - } - - this.data.Add(new( - metrics, - pointSize, - scaledAdvance, - scaledLineHeight, - scaledAscender, - scaledDescender, - scaledDelta, - scaledMinY, - bidiRun, - graphemeIndex, - isLastInGrapheme, - codePointIndex, - graphemeCodePointIndex, - isTransformed, - isDecomposed, - stringIndex)); - } - - /// - /// Inserts all entries from into this line at the given index - /// and recomputes aggregated metrics. - /// - /// The zero-based index at which to insert. - /// The line whose entries should be inserted. - public void InsertAt(int index, TextLine textLine) - { - this.data.InsertRange(index, textLine.data); - RecalculateLineMetrics(this); - } - - /// - /// Returns the cumulative scaled advance up to and including the glyph at the given index. - /// Whitespace entries at or after are skipped so the returned value - /// represents the advance at the last non-whitespace glyph before a potential line break. - /// - /// Results are memoized by index. - /// The zero-based index to measure up to. - /// The cumulative scaled advance. - public float MeasureAt(int index) - { - if (this.advances.TryGetValue(index, out float advance)) - { - return advance; - } - - if (index >= this.data.Count) - { - index = this.data.Count - 1; - } - - while (index >= 0 && CodePoint.IsWhiteSpace(this.data[index].CodePoint)) - { - // If the index is whitespace, we need to measure at the previous - // non-whitespace glyph to ensure we don't break too early. - index--; - } - - advance = 0; - for (int i = 0; i <= index; i++) - { - advance += this.data[i].ScaledAdvance; - } - - this.advances[index] = advance; - return advance; - } - - /// - /// Splits this line at the first non-whitespace glyph whose cumulative advance meets or - /// exceeds . On success, the split-off tail is returned as a new - /// line and removed from this one; both lines have their aggregated metrics recomputed. - /// - /// The scaled advance threshold at which to split. - /// The trailing portion of the split, or if no split was performed. - /// if a split occurred; otherwise . - public bool TrySplitAt(float length, [NotNullWhen(true)] out TextLine? result) - { - float advance = this.data[0].ScaledAdvance; - - // Ensure at least one glyph is in the line. - // trailing whitespace should be ignored as it is trimmed - // on finalization. - for (int i = 1; i < this.data.Count; i++) - { - GlyphLayoutData glyph = this.data[i]; - advance += glyph.ScaledAdvance; - if (CodePoint.IsWhiteSpace(glyph.CodePoint)) - { - continue; - } - - if (advance >= length) - { - int count = this.data.Count - i; - result = new(count); - result.data.AddRange(this.data.GetRange(i, count)); - RecalculateLineMetrics(result); - - this.data.RemoveRange(i, count); - RecalculateLineMetrics(this); - return true; - } - } - - result = null; - return false; - } - - /// - /// Splits this line at the glyph immediately preceding the supplied - /// wrap position. When is set, the split is delayed until - /// the nearest boundary outside a CSS keep-all word unit sequence. - /// - /// The resolved line-break opportunity. - /// When , avoid breaking within keep-all word unit sequences. - /// The trailing portion of the split, or if no split was performed. - /// if a split occurred; otherwise . - public bool TrySplitAt(LineBreak lineBreak, bool keepAll, [NotNullWhen(true)] out TextLine? result) - { - int index = this.data.Count; - while (index > 0) - { - if (this.data[--index].CodePointIndex == lineBreak.PositionWrap) - { - break; - } - } - - // CSS word-break: keep-all suppresses implicit breaks between typographic letter units. - if (index > 0 - && !lineBreak.Required - && keepAll - && this.IsKeepAllSuppressedBreak(index)) - { - while (index > 0 && this.IsKeepAllSuppressedBreak(index)) - { - index--; - } - } - - if (index == 0) - { - result = null; - return false; - } - - // Create a new line ensuring we capture the initial metrics. - int count = this.data.Count - index; - result = new(count); - result.data.AddRange(this.data.GetRange(index, count)); - RecalculateLineMetrics(result); - - // Remove those items from this line. - this.data.RemoveRange(index, count); - RecalculateLineMetrics(this); - - return true; - } - - /// - /// Returns whether CSS word-break: keep-all suppresses the candidate break before - /// the entry at . - /// - /// - /// See CSS Text Module Level 4, word-break. - /// - /// The entry index immediately after the candidate break. - /// if the candidate break is within a keep-all word unit sequence. - private bool IsKeepAllSuppressedBreak(int index) - { - if (index <= 0 || index >= this.data.Count) - { - return false; - } - - return IsKeepAllWordUnit(this.data[index - 1].CodePoint) - && IsKeepAllWordUnit(this.data[index].CodePoint); - } - - /// - /// Returns whether participates in a CSS keep-all word unit - /// sequence. - /// - /// - /// CSS keep-all uses typographic letter units and the Unicode line-breaking - /// classes NU, AL, AI, and ID. - /// See Unicode Standard Annex #14, Line Breaking Classes. - /// - /// The code point to classify. - /// if the code point participates in a keep-all word unit sequence. - private static bool IsKeepAllWordUnit(CodePoint codePoint) - => CodePoint.IsLetter(codePoint) - || CodePoint.IsNumber(codePoint) - || CodePoint.GetLineBreakClass(codePoint) is - LineBreakClass.Numeric - or LineBreakClass.Alphabetic - or LineBreakClass.Ambiguous - or LineBreakClass.Ideographic; - - /// - /// Removes trailing breaking-whitespace entries from this line. Non-breaking spaces are - /// preserved and the first entry is always kept even when whitespace. - /// - private void TrimTrailingWhitespace() - { - int count = this.data.Count; - int index = count; - while (index > 1) - { - // Trim trailing breaking whitespace. - CodePoint point = this.data[index - 1].CodePoint; - if (!CodePoint.IsWhiteSpace(point) || CodePoint.IsNonBreakingSpace(point)) - { - break; - } - - index--; - } - - if (index < count) - { - this.data.RemoveRange(index, count - index); - } - } - - /// - /// Finalizes this line after line-breaking: trims trailing breaking whitespace, applies - /// bidi reordering so entries are in visual order, and recomputes aggregated metrics. - /// - /// - /// When , marks the line so becomes a no-op - /// (used for paragraph-final lines). - /// - /// This line, for fluent chaining. - public TextLine Finalize(bool skipJustification = false) - { - this.SkipJustification = skipJustification; - this.TrimTrailingWhitespace(); - this.BidiReOrder(); - RecalculateLineMetrics(this); - return this; - } - - /// - /// Distributes the remaining space between the line advance and the wrapping length across - /// either inter-character or inter-word gaps, as configured by - /// . - /// - /// - /// No-op when the line was finalized with skipJustification, when wrapping is - /// disabled, when no justification style is selected, or when the line is already at or - /// beyond the wrapping length. - /// - /// The text shaping options supplying the wrapping length and justification style. - public void Justify(TextOptions options) - { - if (options.WrappingLength == -1F || options.TextJustification == TextJustification.None) - { - return; - } - - if (this.ScaledLineAdvance == 0) - { - return; - } - - float delta = (options.WrappingLength / options.Dpi) - this.ScaledLineAdvance; - if (delta <= 0) - { - return; - } - - // Increase the advance for all non zero-width glyphs but the last. - if (options.TextJustification == TextJustification.InterCharacter) - { - int nonZeroCount = 0; - for (int i = 0; i < this.data.Count - 1; i++) - { - GlyphLayoutData glyph = this.data[i]; - if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint)) - { - nonZeroCount++; - } - } - - if (nonZeroCount == 0) - { - return; - } - - float padding = delta / nonZeroCount; - for (int i = 0; i < this.data.Count - 1; i++) - { - GlyphLayoutData glyph = this.data[i]; - if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint)) - { - glyph.ScaledAdvance += padding; - this.data[i] = glyph; - } - } - - RecalculateLineMetrics(this); - return; - } - - // Increase the advance for all spaces but the last. - if (options.TextJustification == TextJustification.InterWord) - { - // Count all the whitespace characters. - int whiteSpaceCount = 0; - for (int i = 0; i < this.data.Count - 1; i++) - { - GlyphLayoutData glyph = this.data[i]; - if (CodePoint.IsWhiteSpace(glyph.CodePoint)) - { - whiteSpaceCount++; - } - } - - if (whiteSpaceCount == 0) - { - return; - } - - float padding = delta / whiteSpaceCount; - for (int i = 0; i < this.data.Count - 1; i++) - { - GlyphLayoutData glyph = this.data[i]; - if (CodePoint.IsWhiteSpace(glyph.CodePoint)) - { - glyph.ScaledAdvance += padding; - this.data[i] = glyph; - } - } - } - - RecalculateLineMetrics(this); - } - - /// - /// Re-orders the entries in this line from logical to visual order according to the - /// Unicode Bidirectional Algorithm (, rules L1 and L2). - /// - public void BidiReOrder() - { - // Build up the collection of ordered runs. - BidiRun run = this.data[0].BidiRun; - OrderedBidiRun orderedRun = new(run.Level); - OrderedBidiRun? current = orderedRun; - for (int i = 0; i < this.data.Count; i++) - { - GlyphLayoutData g = this.data[i]; - if (run != g.BidiRun) - { - run = g.BidiRun; - current.Next = new(run.Level); - current = current.Next; - } - - current.Add(g); - } - - // Reorder them into visual order. - orderedRun = LinearReOrder(orderedRun); - - // Now perform a recursive reversal of each run. - // From the highest level found in the text to the lowest odd level on each line, including intermediate levels - // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. - // https://unicode.org/reports/tr9/#L2 - int max = 0; - int min = int.MaxValue; - for (int i = 0; i < this.data.Count; i++) - { - int level = this.data[i].BidiRun.Level; - if (level > max) - { - max = level; - } - - if ((level & 1) != 0 && level < min) - { - min = level; - } - } - - if (min > max) - { - min = max; - } - - if (max == 0 || (min == max && (max & 1) == 0)) - { - // Nothing to reverse. - return; - } - - // Now apply the reversal and replace the original contents. - int minLevelToReverse = max; - while (minLevelToReverse >= min) - { - current = orderedRun; - while (current != null) - { - if (current.Level >= minLevelToReverse) - { - current.Reverse(); - } - - current = current.Next; - } - - minLevelToReverse--; - } - - this.data.Clear(); - current = orderedRun; - while (current != null) - { - this.data.AddRange(current.AsSlice()); - current = current.Next; - } - } - - /// - /// Recomputes the aggregated per-line metrics (advance, max line height, ascender, - /// descender, delta, min-Y) from the current entries. Called after any mutation that - /// can affect these — split, insert, trim, justify. - /// - /// The line to recompute metrics for. - private static void RecalculateLineMetrics(TextLine textLine) - { - // Lastly recalculate this line metrics. - float advance = 0; - float ascender = 0; - float descender = 0; - float delta = 0; - float lineHeight = 0; - float minY = 0; - for (int i = 0; i < textLine.Count; i++) - { - GlyphLayoutData glyph = textLine[i]; - advance += glyph.ScaledAdvance; - ascender = MathF.Max(ascender, glyph.ScaledAscender); - descender = MathF.Max(descender, glyph.ScaledDescender); - delta = MathF.Max(delta, glyph.ScaledDelta); - lineHeight = MathF.Max(lineHeight, glyph.ScaledLineHeight); - minY = MathF.Min(minY, glyph.ScaledMinY); - } - - textLine.ScaledLineAdvance = advance; - textLine.ScaledMaxAscender = ascender; - textLine.ScaledMaxDescender = descender; - textLine.ScaledMaxDelta = delta; - textLine.ScaledMaxLineHeight = lineHeight; - textLine.ScaledMinY = minY; - - textLine.advances.Clear(); - } - - /// - /// Reorders a series of runs from logical to visual order, returning the left most run. - /// - /// - /// The ordered bidi run. - /// The . - private static OrderedBidiRun LinearReOrder(OrderedBidiRun? line) - { - BidiRange? range = null; - OrderedBidiRun? run = line; - - while (run != null) - { - OrderedBidiRun? next = run.Next; - - while (range != null && range.Level > run.Level - && range.Previous != null && range.Previous.Level >= run.Level) - { - range = BidiRange.MergeWithPrevious(range); - } - - if (range != null && range.Level >= run.Level) - { - // Attach run to the range. - if ((run.Level & 1) != 0) - { - // Odd, range goes to the right of run. - run.Next = range.Left; - range.Left = run; - } - else - { - // Even, range goes to the left of run. - range.Right!.Next = run; - range.Right = run; - } - - range.Level = run.Level; - } - else - { - BidiRange r = new(); - r.Left = r.Right = run; - r.Level = run.Level; - r.Previous = range; - range = r; - } - - run = next; - } - - while (range?.Previous != null) - { - range = BidiRange.MergeWithPrevious(range); - } - - // Terminate. - range!.Right!.Next = null; - return range!.Left!; - } - - /// - /// Per-codepoint shaping data stored inside a . - /// Each entry corresponds to a single codepoint — complex scripts may map one grapheme to - /// multiple entries (tracked via ). - /// - [DebuggerDisplay("{DebuggerDisplay,nq}")] - internal struct GlyphLayoutData - { - /// - /// Initializes a new instance of the struct. - /// - /// The shaped glyph metrics for this codepoint. - /// The point size at which the glyph is rendered. - /// The scaled advance of this entry. - /// The scaled line height contributed by this entry. - /// The scaled typographic ascender. - /// The scaled typographic descender. - /// The symmetric metrics delta applied during line-box construction. - /// The minimum scaled Y (topmost ink) across . - /// The resolved bidi run this entry belongs to. - /// The grapheme index in the source text. - /// Whether this is the last codepoint in its grapheme cluster. - /// The codepoint index in the source text. - /// The index of this codepoint within its grapheme cluster. - /// Whether the entry participates in a transformed vertical layout. - /// Whether the entry was produced by Unicode decomposition. - /// The UTF-16 character index in the source string. - public GlyphLayoutData( - IReadOnlyList metrics, - float pointSize, - float scaledAdvance, - float scaledLineHeight, - float scaledAscender, - float scaledDescender, - float scaledDelta, - float scaledMinY, - BidiRun bidiRun, - int graphemeIndex, - bool isLastInGrapheme, - int codePointIndex, - int graphemeCodePointIndex, - bool isTransformed, - bool isDecomposed, - int stringIndex) - { - this.Metrics = metrics; - this.PointSize = pointSize; - this.ScaledAdvance = scaledAdvance; - this.ScaledLineHeight = scaledLineHeight; - this.ScaledAscender = scaledAscender; - this.ScaledDescender = scaledDescender; - this.ScaledDelta = scaledDelta; - this.ScaledMinY = scaledMinY; - this.BidiRun = bidiRun; - this.GraphemeIndex = graphemeIndex; - this.IsLastInGrapheme = isLastInGrapheme; - this.CodePointIndex = codePointIndex; - this.GraphemeCodePointIndex = graphemeCodePointIndex; - this.IsTransformed = isTransformed; - this.IsDecomposed = isDecomposed; - this.StringIndex = stringIndex; - } - - /// Gets the source codepoint for this entry. - public readonly CodePoint CodePoint => this.Metrics[0].CodePoint; - - /// Gets the shaped glyph metrics produced for this codepoint (one codepoint may map to several glyphs). - public IReadOnlyList Metrics { get; } - - /// Gets the point size at which this entry is rendered. - public float PointSize { get; } - - /// Gets or sets the scaled advance of this entry (mutated by justification). - public float ScaledAdvance { get; set; } - - /// Gets the scaled line height contributed by this entry, before line-spacing is applied. - public float ScaledLineHeight { get; } - - /// Gets the scaled typographic ascender. - public float ScaledAscender { get; } - - /// Gets the scaled typographic descender. - public float ScaledDescender { get; } - - /// Gets the symmetric ascender/descender delta applied during line-box construction. - public float ScaledDelta { get; } - - /// Gets the smallest (most negative) scaled Y across . - public float ScaledMinY { get; } - - /// Gets the resolved bidi run this entry belongs to. - public BidiRun BidiRun { get; } - - /// Gets the text direction derived from . - public readonly TextDirection TextDirection => (TextDirection)this.BidiRun.Direction; - - /// Gets the grapheme index in the source text. - public int GraphemeIndex { get; } - - /// Gets a value indicating whether this is the last codepoint in its grapheme cluster. - public bool IsLastInGrapheme { get; } - - /// Gets the index of this codepoint within its grapheme cluster (0-based). - public int GraphemeCodePointIndex { get; } - - /// Gets the codepoint index in the source text. - public int CodePointIndex { get; } - - /// Gets a value indicating whether the entry participates in a transformed vertical layout. - public bool IsTransformed { get; } - - /// Gets a value indicating whether the entry was produced by Unicode decomposition. - public bool IsDecomposed { get; } - - /// Gets the UTF-16 character index in the source string. - public int StringIndex { get; } - - /// Gets a value indicating whether the codepoint is a line-break character. - public readonly bool IsNewLine => CodePoint.IsNewLine(this.CodePoint); - - private readonly string DebuggerDisplay => FormattableString - .Invariant($"{this.CodePoint.ToDebuggerDisplay()} : {this.TextDirection} : {this.CodePointIndex}, level: {this.BidiRun.Level}"); - } - - /// - /// A node in the linked list of contiguous same-level bidi runs used by . - /// Each node owns the glyph entries at its bidi embedding level and can be reversed in place. - /// - private sealed class OrderedBidiRun - { - private ArrayBuilder info; - - /// - /// Initializes a new instance of the class. - /// - /// The bidi embedding level for this run. - public OrderedBidiRun(int level) => this.Level = level; - - /// Gets the bidi embedding level of this run. - public int Level { get; } - - /// Gets or sets the next run in visual order. - public OrderedBidiRun? Next { get; set; } - - /// Appends an entry to this run. - /// The entry to append. - public void Add(GlyphLayoutData info) => this.info.Add(info); - - /// Returns a slice view over this run's entries. - /// A slice over the entries. - public ArraySlice AsSlice() => this.info.AsSlice(); - - /// Reverses the entries in this run in place (for rule L2). - public void Reverse() => this.AsSlice().Span.Reverse(); - } - - /// - /// An intermediate grouping of links used by the linear-reorder - /// algorithm to stitch pairs of same-level ranges together. - /// - private sealed class BidiRange - { - /// Gets or sets the shared bidi embedding level for this range. - public int Level { get; set; } - - /// Gets or sets the leftmost run in the range. - public OrderedBidiRun? Left { get; set; } - - /// Gets or sets the rightmost run in the range. - public OrderedBidiRun? Right { get; set; } - - /// Gets or sets the previous range in the processing stack. - public BidiRange? Previous { get; set; } - - /// - /// Stitches the current range with its predecessor, producing a single merged range - /// whose internal orientation depends on the predecessor's embedding level parity. - /// - /// The current range whose will be merged. - /// The merged range (always the predecessor instance, reused in place). - public static BidiRange MergeWithPrevious(BidiRange? range) - { - BidiRange previous = range!.Previous!; - BidiRange left; - BidiRange right; - - if ((previous.Level & 1) != 0) - { - // Odd, previous goes to the right of range. - left = range; - right = previous; - } - else - { - // Even, previous goes to the left of range. - left = previous; - right = range; - } - - // Stitch them - left.Right!.Next = right.Left; - previous.Left = left.Left; - previous.Right = right.Right; - - return previous; - } - } - } } diff --git a/src/SixLabors.Fonts/TextLine.cs b/src/SixLabors.Fonts/TextLine.cs new file mode 100644 index 00000000..5b4dc535 --- /dev/null +++ b/src/SixLabors.Fonts/TextLine.cs @@ -0,0 +1,1090 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Diagnostics.CodeAnalysis; +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// A shaped line of text — an ordered sequence of entries plus +/// per-line aggregate metrics (advance, ascender, descender, etc.) used to position the line +/// during layout. +/// +internal sealed class TextLine +{ + private readonly List data; + private readonly Dictionary advances = []; + + /// + /// Initializes a new instance of the class with a small default capacity. + /// + public TextLine() => this.data = new(16); + + /// + /// Initializes a new instance of the class with the specified initial + /// entry capacity. + /// + /// Initial capacity for the internal entry list. + public TextLine(int capacity) => this.data = new(capacity); + + /// + /// Initializes a new instance of the class by copying another line. + /// + /// The line to copy. + public TextLine(TextLine source) + { + this.data = [.. source.data]; + this.SkipJustification = source.SkipJustification; + this.ScaledLineAdvance = source.ScaledLineAdvance; + this.ScaledMaxLineHeight = source.ScaledMaxLineHeight; + this.ScaledMaxAscender = source.ScaledMaxAscender; + this.ScaledMaxDescender = source.ScaledMaxDescender; + this.ScaledMaxDelta = source.ScaledMaxDelta; + this.ScaledMinY = source.ScaledMinY; + } + + /// + /// Gets the number of entries in this line. + /// + public int Count => this.data.Count; + + /// + /// Gets the number of graphemes in this line. + /// + public int GraphemeCount + { + get + { + int count = 0; + int lastGraphemeIndex = -1; + for (int i = 0; i < this.data.Count; i++) + { + int graphemeIndex = this.data[i].GraphemeIndex; + if (graphemeIndex == lastGraphemeIndex) + { + continue; + } + + count++; + lastGraphemeIndex = graphemeIndex; + } + + return count; + } + } + + /// + /// Gets a value indicating whether this line should be skipped during text justification. + /// Set by for lines that end a paragraph. + /// + public bool SkipJustification { get; private set; } + + /// + /// Gets the scaled advance contributed to line layout and measurement. + /// + public float ScaledLineAdvance { get; private set; } + + /// + /// Gets the greatest scaled line height across all entries, multiplied by the configured + /// line-spacing factor. + /// + public float ScaledMaxLineHeight { get; private set; } = -1; + + /// + /// Gets the greatest scaled ascender across all entries in this line. + /// + public float ScaledMaxAscender { get; private set; } = -1; + + /// + /// Gets the greatest scaled descender across all entries in this line. + /// + public float ScaledMaxDescender { get; private set; } = -1; + + /// + /// Gets the greatest scaled symmetric-metrics delta across all entries in this line. + /// Browsers adjust ascender/descender symmetrically for baseline alignment; this captures + /// that adjustment. + /// + public float ScaledMaxDelta { get; private set; } = float.MinValue; + + /// + /// Gets the smallest (most negative) scaled Y position across all entries in this line. + /// Used to detect ink that extends above the typographic ascender (for example stacked + /// marks in Tibetan) so the layout engine can reserve extra ascent. + /// + public float ScaledMinY { get; private set; } + + /// + /// Gets the entry at the given index. + /// + /// The zero-based index into this line. + /// The entry at the given index. + public GlyphLayoutData this[int index] => this.data[index]; + + /// + /// Counts the glyph entries emitted from this line. + /// + /// The number of glyph entries that layout will emit for this line. + public int CountGlyphLayouts() + { + int count = 0; + for (int i = 0; i < this.data.Count; i++) + { + count += this.data[i].Metrics.Count; + } + + return count; + } + + /// + /// Appends a shaped entry to this line, updating the aggregated line-level metrics. + /// + /// The glyph metrics produced by shaping this entry's codepoint. + /// The point size at which the entry is rendered. + /// The scaled advance contributed by this entry. + /// The scaled line height contributed by this entry (before line-spacing). + /// The scaled typographic ascender. + /// The scaled typographic descender. + /// The symmetric metrics delta applied during line-box construction. + /// The bidi run this entry belongs to. + /// The grapheme index in the source text. + /// Whether this entry is the last codepoint in its grapheme cluster. + /// The codepoint index in the source text. + /// The index of the codepoint within its grapheme cluster. + /// Whether the entry participates in a transformed (rotated) vertical layout. + /// Whether the entry was produced by Unicode decomposition. + /// The character index in the source string. + /// The glyph-level layout mode to use for ink bounds computation. + /// The line-spacing factor to apply to . + /// The marker index to use if this entry becomes a selected soft-hyphen break. + public void Add( + IReadOnlyList metrics, + float pointSize, + float scaledAdvance, + float scaledLineHeight, + float scaledAscender, + float scaledDescender, + float scaledDelta, + BidiRun bidiRun, + int graphemeIndex, + bool isLastInGrapheme, + int codePointIndex, + int graphemeCodePointIndex, + bool isTransformed, + bool isDecomposed, + int stringIndex, + GlyphLayoutMode layoutMode, + float lineSpacing, + int hyphenationMarkerIndex = GlyphLayoutData.NoHyphenationMarker) + { + // Apply LineSpacing to scaledLineHeight before storing + scaledLineHeight *= lineSpacing; + + // Reset metrics. + // We track the maximum metrics for each line to ensure glyphs can be aligned. + if (graphemeCodePointIndex == 0) + { + // TODO: Check this logic is correct. + this.ScaledLineAdvance += scaledAdvance; + } + + this.ScaledMaxLineHeight = MathF.Max(this.ScaledMaxLineHeight, scaledLineHeight); + this.ScaledMaxAscender = MathF.Max(this.ScaledMaxAscender, scaledAscender); + this.ScaledMaxDescender = MathF.Max(this.ScaledMaxDescender, scaledDescender); + this.ScaledMaxDelta = MathF.Max(this.ScaledMaxDelta, scaledDelta); + + // Track the true top of the ink in device space (Y down, baseline at 0). + // For scripts with stacked marks (Tibetan, etc) this can be significantly + // above the typographic ascender, so we cannot trust ascender alone. + float scaledMinY = 0; + for (int i = 0; i < metrics.Count; i++) + { + FontGlyphMetrics metric = metrics[i]; + if (FontGlyphMetrics.ShouldSkipGlyphRendering(metric.CodePoint)) + { + continue; + } + + FontRectangle bbox = metric.GetBoundingBox(layoutMode, Vector2.Zero, pointSize); + scaledMinY = MathF.Min(scaledMinY, bbox.Y); + } + + // ScaledMinY is the minimum ink Y over all glyphs in this line, in Y down. + // It is usually <= 0; more negative means more ink above the baseline. + if (this.data.Count == 0) + { + this.ScaledMinY = scaledMinY; + } + else + { + this.ScaledMinY = MathF.Min(this.ScaledMinY, scaledMinY); + } + + this.data.Add(new( + metrics, + pointSize, + scaledAdvance, + scaledLineHeight, + scaledAscender, + scaledDescender, + scaledDelta, + scaledMinY, + bidiRun, + graphemeIndex, + isLastInGrapheme, + codePointIndex, + graphemeCodePointIndex, + isTransformed, + isDecomposed, + stringIndex, + hyphenationMarkerIndex)); + } + + /// + /// Adds an inline placeholder entry at an existing source codepoint position without consuming source text. + /// + /// The positioned placeholder glyph data. + /// The source grapheme index at the placeholder insertion point. + /// The source UTF-16 index at the placeholder insertion point. + /// when the current layout advances horizontally. + /// when the current layout is vertical mixed. + /// The line-spacing factor to apply to placeholder line height. + public void AddPlaceholder( + GlyphPositioningCollection.GlyphPositioningData placeholder, + int graphemeIndex, + int stringIndex, + bool isHorizontalLayout, + bool isVerticalMixedLayout, + float lineSpacing) + { + FontGlyphMetrics placeholderGlyph = placeholder.Metrics; + bool isPlaceholderHorizontal = isHorizontalLayout || isVerticalMixedLayout; + float placeholderAdvance = isPlaceholderHorizontal + ? placeholderGlyph.AdvanceWidth + : placeholderGlyph.AdvanceHeight; + + Vector2 placeholderScale = new( + placeholder.PointSize / placeholderGlyph.ScaleFactor.X, + placeholder.PointSize / placeholderGlyph.ScaleFactor.Y); + + placeholderAdvance *= isPlaceholderHorizontal ? placeholderScale.X : placeholderScale.Y; + + GlyphLayoutMode placeholderMode = isHorizontalLayout + ? GlyphLayoutMode.Horizontal + : GlyphLayoutMode.Vertical; + + FontRectangle placeholderBox = placeholderGlyph.GetBoundingBox(placeholderMode, Vector2.Zero, placeholder.PointSize); + + IMetricsHeader metricsHeader = isPlaceholderHorizontal + ? placeholderGlyph.FontMetrics.HorizontalMetrics + : placeholderGlyph.FontMetrics.VerticalMetrics; + + // Placeholder bounds can extend beyond the surrounding run font's + // normal ascender/descender band. Keep the run font line-box model as + // the baseline contribution, then expand only the side the placeholder + // actually overhangs so following lines reserve enough space. + float placeholderScaleY = placeholder.PointSize / placeholderGlyph.ScaleFactor.Y; + float placeholderLineHeight = placeholderGlyph.UnitsPerEm * placeholderScaleY; + float placeholderDelta = ((metricsHeader.LineHeight * placeholderScaleY) - placeholderLineHeight) * .5F; + float placeholderAscender = (metricsHeader.Ascender * placeholderScaleY) - placeholderDelta; + float placeholderDescender = Math.Abs(metricsHeader.Descender * placeholderScaleY) - placeholderDelta; + placeholderAscender = MathF.Max(placeholderAscender, -placeholderBox.Top); + placeholderDescender = MathF.Max(placeholderDescender, placeholderBox.Bottom); + placeholderLineHeight = MathF.Max( + placeholderLineHeight, + placeholderAscender + placeholderDescender + (2 * placeholderDelta)); + + // Placeholders share the source codepoint offset at their insertion point, + // but they do not consume source grapheme, codepoint, or UTF-16 indexes. + this.Add( + new FontGlyphMetrics[] { placeholderGlyph }, + placeholder.PointSize, + placeholderAdvance, + placeholderLineHeight, + placeholderAscender, + placeholderDescender, + placeholderDelta, + placeholder.Data.BidiRun, + graphemeIndex, + true, + placeholder.Offset, + 0, + false, + false, + stringIndex, + placeholderMode, + lineSpacing); + } + + /// + /// Inserts all entries from into this line at the given index + /// and recomputes aggregated metrics. + /// + /// The zero-based index at which to insert. + /// The line whose entries should be inserted. + public void InsertAt(int index, TextLine textLine) + { + this.data.InsertRange(index, textLine.data); + RecalculateLineMetrics(this); + } + + /// + /// Returns the cumulative scaled advance up to and including the glyph at the given index. + /// Whitespace entries at or after are skipped so the returned value + /// represents the advance at the last non-whitespace glyph before a potential line break. + /// + /// Results are memoized by index. + /// The zero-based index to measure up to. + /// The cumulative scaled advance. + public float MeasureAt(int index) + { + if (this.advances.TryGetValue(index, out float advance)) + { + return advance; + } + + if (index >= this.data.Count) + { + index = this.data.Count - 1; + } + + while (index >= 0 && CodePoint.IsWhiteSpace(this.data[index].CodePoint)) + { + // If the index is whitespace, we need to measure at the previous + // non-whitespace glyph to ensure we don't break too early. + index--; + } + + advance = 0; + for (int i = 0; i <= index; i++) + { + advance += this.data[i].ScaledAdvance; + } + + this.advances[index] = advance; + return advance; + } + + /// + /// Gets the marker advance for a selected soft-hyphen entry. + /// + /// The soft-hyphen entry index in this line. + /// The markers prepared with the logical line. + /// The scaled advance of the visible hyphenation marker. + public float GetHyphenationMarkerAdvance( + int index, + List hyphenationMarkers) + => hyphenationMarkers[this.data[index].HyphenationMarkerIndex].ScaledAdvance; + + /// + /// Replaces a selected soft-hyphen entry with its prepared visible marker. + /// + /// The soft-hyphen entry index in this line. + /// The markers prepared with the logical line. + public void ApplyHyphenationMarker( + int index, + List hyphenationMarkers) + { + this.data[index] = hyphenationMarkers[this.data[index].HyphenationMarkerIndex]; + RecalculateLineMetrics(this); + } + + /// + /// Applies an ellipsis marker to the end of this line. + /// + /// The marker codepoint to append. + /// The wrapping length in inches. + /// The text options used for layout. + public void ApplyEllipsisMarker( + CodePoint markerCodePoint, + float scaledWrappingLength, + TextOptions options) + { + // The marker replaces the hidden tail, so breakable whitespace at the + // truncation edge is removed before we choose the marker style or decide + // how many graphemes fit. + this.RemoveTrailingBreakingWhitespace(); + + GlyphLayoutData anchor = this.data[^1]; + GlyphLayoutData marker = TextLayout.CreateGeneratedMarker( + anchor.Metrics[0], + anchor.PointSize, + anchor.BidiRun, + anchor.GraphemeIndex, + anchor.IsLastInGrapheme, + anchor.CodePointIndex, + anchor.GraphemeCodePointIndex, + anchor.StringIndex, + markerCodePoint, + options.LayoutMode, + options); + + while (this.data.Count > 0 && + this.ScaledLineAdvance + marker.ScaledAdvance > scaledWrappingLength) + { + // Remove a whole grapheme at a time. Truncating through a decomposed + // cluster would corrupt the same source unit that selection and caret + // metrics expose as indivisible. + this.RemoveLastGrapheme(); + } + + // CSS block ellipsis allows the marker to displace the whole final line. + // That means an overflowing line can become marker-only, but only because + // hidden text exists after the clamp point. + this.data.Add(marker); + RecalculateLineMetrics(this); + } + + /// + /// Removes trailing breakable whitespace from the line. + /// + /// + /// When , keeps ordinary trailing breaking whitespace for editor interaction. + /// + private void RemoveTrailingBreakingWhitespace(bool preserveTrailingBreakingWhitespace = false) + { + int index = this.data.Count; + while (index > 1) + { + CodePoint point = this.data[index - 1].CodePoint; + if (!CodePoint.IsWhiteSpace(point) || CodePoint.IsNonBreakingSpace(point)) + { + break; + } + + if (preserveTrailingBreakingWhitespace && !CodePoint.IsNewLine(point)) + { + break; + } + + index--; + } + + if (index < this.data.Count) + { + this.data.RemoveRange(index, this.data.Count - index); + RecalculateLineMetrics(this); + } + } + + /// + /// Removes the last complete grapheme from the line. + /// + private void RemoveLastGrapheme() + { + int end = this.data.Count - 1; + int graphemeIndex = this.data[end].GraphemeIndex; + int start = end; + while (start > 0 && this.data[start - 1].GraphemeIndex == graphemeIndex) + { + start--; + } + + this.data.RemoveRange(start, end - start + 1); + RecalculateLineMetrics(this); + } + + /// + /// Splits this line at the first non-whitespace glyph whose cumulative advance meets or + /// exceeds . On success, the split-off tail is returned as a new + /// line and removed from this one; both lines have their aggregated metrics recomputed. + /// + /// The scaled advance threshold at which to split. + /// The trailing portion of the split, or if no split was performed. + /// if a split occurred; otherwise . + public bool TrySplitAt(float length, [NotNullWhen(true)] out TextLine? result) + { + float advance = this.data[0].ScaledAdvance; + + // Ensure at least one glyph is in the line. + // trailing whitespace should be ignored as it is trimmed + // on finalization. + for (int i = 1; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + advance += glyph.ScaledAdvance; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + continue; + } + + if (advance >= length) + { + int count = this.data.Count - i; + result = new(count); + result.data.AddRange(this.data.GetRange(i, count)); + RecalculateLineMetrics(result); + + this.data.RemoveRange(i, count); + RecalculateLineMetrics(this); + return true; + } + } + + result = null; + return false; + } + + /// + /// Splits this line at the glyph immediately preceding the supplied + /// wrap position. When is set, the split is delayed until + /// the nearest boundary outside a CSS keep-all word unit sequence. + /// + /// The resolved line-break opportunity. + /// When , avoid breaking within keep-all word unit sequences. + /// The trailing portion of the split, or if no split was performed. + /// if a split occurred; otherwise . + public bool TrySplitAt(LineBreak lineBreak, bool keepAll, [NotNullWhen(true)] out TextLine? result) + { + int index = this.data.Count; + while (index > 0) + { + if (this.data[--index].CodePointIndex == lineBreak.PositionWrap) + { + break; + } + } + + // CSS word-break: keep-all suppresses implicit breaks between typographic letter units. + if (index > 0 + && !lineBreak.Required + && keepAll + && this.IsKeepAllSuppressedBreak(index)) + { + while (index > 0 && this.IsKeepAllSuppressedBreak(index)) + { + index--; + } + } + + if (index == 0) + { + result = null; + return false; + } + + // Create a new line ensuring we capture the initial metrics. + int count = this.data.Count - index; + result = new(count); + result.data.AddRange(this.data.GetRange(index, count)); + RecalculateLineMetrics(result); + + // Remove those items from this line. + this.data.RemoveRange(index, count); + RecalculateLineMetrics(this); + + return true; + } + + /// + /// Splits a terminal hard-break grapheme into its own line. + /// + /// The terminal hard-break line, or if no split was performed. + /// if a terminal hard break was split; otherwise . + public bool TrySplitTerminalHardBreak([NotNullWhen(true)] out TextLine? result) + { + int end = this.data.Count - 1; + if (end <= 0 || !this.data[end].IsNewLine) + { + result = null; + return false; + } + + int graphemeIndex = this.data[end].GraphemeIndex; + int start = end; + while (start > 0 && this.data[start - 1].GraphemeIndex == graphemeIndex) + { + start--; + } + + int count = this.data.Count - start; + result = new(count); + result.data.AddRange(this.data.GetRange(start, count)); + RecalculateLineMetrics(result); + + this.data.RemoveRange(start, count); + RecalculateLineMetrics(this); + return true; + } + + /// + /// Returns whether CSS word-break: keep-all suppresses the candidate break before + /// the entry at . + /// + /// + /// See CSS Text Module Level 4, word-break. + /// + /// The entry index immediately after the candidate break. + /// if the candidate break is within a keep-all word unit sequence. + private bool IsKeepAllSuppressedBreak(int index) + { + if (index <= 0 || index >= this.data.Count) + { + return false; + } + + return IsKeepAllWordUnit(this.data[index - 1].CodePoint) + && IsKeepAllWordUnit(this.data[index].CodePoint); + } + + /// + /// Returns whether participates in a CSS keep-all word unit + /// sequence. + /// + /// + /// CSS keep-all uses typographic letter units and the Unicode line-breaking + /// classes NU, AL, AI, and ID. + /// See Unicode Standard Annex #14, Line Breaking Classes. + /// + /// The code point to classify. + /// if the code point participates in a keep-all word unit sequence. + private static bool IsKeepAllWordUnit(CodePoint codePoint) + => CodePoint.IsLetter(codePoint) + || CodePoint.IsNumber(codePoint) + || CodePoint.GetLineBreakClass(codePoint) is + LineBreakClass.Numeric + or LineBreakClass.Alphabetic + or LineBreakClass.Ambiguous + or LineBreakClass.Ideographic; + + /// + /// Finalizes this line after line-breaking: trims trailing breaking whitespace when requested, + /// applies bidi reordering so entries are in visual order, and recomputes aggregated metrics. + /// + /// + /// When , marks the line so becomes a no-op + /// (used for paragraph-final lines). + /// + /// + /// When , moves decomposed grapheme advances to the final visual entry. + /// + /// + /// When , keeps ordinary trailing breaking whitespace in the finalized line. + /// + /// This line, for fluent chaining. + public TextLine Finalize( + bool skipJustification = false, + bool normalizeDecomposedAdvances = false, + bool preserveTrailingBreakingWhitespace = false) + { + this.SkipJustification = skipJustification; + this.RemoveTrailingBreakingWhitespace(preserveTrailingBreakingWhitespace); + this.BidiReOrder(); + + if (normalizeDecomposedAdvances) + { + this.NormalizeDecomposedAdvances(); + } + + RecalculateLineMetrics(this); + return this; + } + + /// + /// Moves decomposed grapheme advances when bidi reordering moved the grapheme boundary marker. + /// + private void NormalizeDecomposedAdvances() + { + int start = 0; + while (start < this.data.Count) + { + int graphemeIndex = this.data[start].GraphemeIndex; + int end = start + 1; + bool hasDecomposedEntry = this.data[start].IsDecomposed; + + while (end < this.data.Count && this.data[end].GraphemeIndex == graphemeIndex) + { + hasDecomposedEntry |= this.data[end].IsDecomposed; + end++; + } + + if (hasDecomposedEntry && end - start > 1 && !this.data[end - 1].IsLastInGrapheme) + { + float advance = 0; + for (int i = start; i < end; i++) + { + GlyphLayoutData glyph = this.data[i]; + advance += glyph.ScaledAdvance; + glyph.ScaledAdvance = 0; + glyph.IsLastInGrapheme = false; + this.data[i] = glyph; + } + + GlyphLayoutData last = this.data[end - 1]; + last.ScaledAdvance = advance; + last.IsLastInGrapheme = true; + this.data[end - 1] = last; + } + + start = end; + } + } + + /// + /// Distributes the remaining space between the line advance and the wrapping length across + /// either inter-character or inter-word gaps, as configured by + /// . + /// + /// + /// No-op when the line was finalized with skipJustification, when wrapping is + /// disabled, when no justification style is selected, or when the line is already at or + /// beyond the wrapping length. + /// + /// The text options supplying the wrapping length and justification style. + public void Justify(TextOptions options) + { + if (options.WrappingLength == -1F || options.TextJustification == TextJustification.None) + { + return; + } + + if (this.ScaledLineAdvance == 0) + { + return; + } + + float delta = (options.WrappingLength / options.Dpi) - this.ScaledLineAdvance; + if (delta <= 0) + { + return; + } + + // Increase the advance for all non zero-width glyphs but the last. + if (options.TextJustification == TextJustification.InterCharacter) + { + int nonZeroCount = 0; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) + && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint)) + { + nonZeroCount++; + } + } + + int opportunityCount = nonZeroCount - 1; + if (opportunityCount == 0) + { + return; + } + + float padding = delta / opportunityCount; + int remainingOpportunities = opportunityCount; + for (int i = 0; i < this.data.Count && remainingOpportunities > 0; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (!CodePoint.IsZeroWidthJoiner(glyph.CodePoint) + && !CodePoint.IsZeroWidthNonJoiner(glyph.CodePoint)) + { + glyph.ScaledAdvance += padding; + this.data[i] = glyph; + remainingOpportunities--; + } + } + + RecalculateLineMetrics(this); + return; + } + + // Increase the advance for all spaces but the last. + if (options.TextJustification == TextJustification.InterWord) + { + // Count all the whitespace characters. + int whiteSpaceCount = 0; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + whiteSpaceCount++; + } + } + + if (whiteSpaceCount == 0) + { + return; + } + + float padding = delta / whiteSpaceCount; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData glyph = this.data[i]; + if (CodePoint.IsWhiteSpace(glyph.CodePoint)) + { + glyph.ScaledAdvance += padding; + this.data[i] = glyph; + } + } + } + + RecalculateLineMetrics(this); + } + + /// + /// Re-orders the entries in this line from logical to visual order according to the + /// Unicode Bidirectional Algorithm (, rules L1 and L2). + /// + public void BidiReOrder() + { + // Build up the collection of ordered runs. + BidiRun run = this.data[0].BidiRun; + OrderedBidiRun orderedRun = new(run.Level); + OrderedBidiRun? current = orderedRun; + for (int i = 0; i < this.data.Count; i++) + { + GlyphLayoutData g = this.data[i]; + if (run != g.BidiRun) + { + run = g.BidiRun; + current.Next = new(run.Level); + current = current.Next; + } + + current.Add(g); + } + + // Reorder them into visual order. + orderedRun = LinearReOrder(orderedRun); + + // Now perform a recursive reversal of each run. + // From the highest level found in the text to the lowest odd level on each line, including intermediate levels + // not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher. + // https://unicode.org/reports/tr9/#L2 + int max = 0; + int min = int.MaxValue; + for (int i = 0; i < this.data.Count; i++) + { + int level = this.data[i].BidiRun.Level; + if (level > max) + { + max = level; + } + + if ((level & 1) != 0 && level < min) + { + min = level; + } + } + + if (min > max) + { + min = max; + } + + if (max == 0 || (min == max && (max & 1) == 0)) + { + // Nothing to reverse. + return; + } + + // Now apply the reversal and replace the original contents. + int minLevelToReverse = max; + while (minLevelToReverse >= min) + { + current = orderedRun; + while (current != null) + { + if (current.Level >= minLevelToReverse) + { + current.Reverse(); + } + + current = current.Next; + } + + minLevelToReverse--; + } + + this.data.Clear(); + current = orderedRun; + while (current != null) + { + this.data.AddRange(current.AsSlice()); + current = current.Next; + } + } + + /// + /// Recomputes the aggregated per-line metrics (advance, max line height, ascender, + /// descender, delta, min-Y) from the current entries. Called after any mutation that + /// can affect these — split, insert, trim, justify. + /// + /// The line to recompute metrics for. + private static void RecalculateLineMetrics(TextLine textLine) + { + // Lastly recalculate this line metrics. + float advance = 0; + float ascender = 0; + float descender = 0; + float delta = 0; + float lineHeight = 0; + float minY = 0; + for (int i = 0; i < textLine.Count; i++) + { + GlyphLayoutData glyph = textLine[i]; + advance += glyph.ScaledAdvance; + ascender = MathF.Max(ascender, glyph.ScaledAscender); + descender = MathF.Max(descender, glyph.ScaledDescender); + delta = MathF.Max(delta, glyph.ScaledDelta); + lineHeight = MathF.Max(lineHeight, glyph.ScaledLineHeight); + minY = MathF.Min(minY, glyph.ScaledMinY); + } + + textLine.ScaledLineAdvance = advance; + textLine.ScaledMaxAscender = ascender; + textLine.ScaledMaxDescender = descender; + textLine.ScaledMaxDelta = delta; + textLine.ScaledMaxLineHeight = lineHeight; + textLine.ScaledMinY = minY; + + textLine.advances.Clear(); + } + + /// + /// Reorders a series of runs from logical to visual order, returning the left most run. + /// + /// + /// The ordered bidi run. + /// The . + private static OrderedBidiRun LinearReOrder(OrderedBidiRun? line) + { + BidiRange? range = null; + OrderedBidiRun? run = line; + + while (run != null) + { + OrderedBidiRun? next = run.Next; + + while (range != null && range.Level > run.Level + && range.Previous != null && range.Previous.Level >= run.Level) + { + range = BidiRange.MergeWithPrevious(range); + } + + if (range != null && range.Level >= run.Level) + { + // Attach run to the range. + if ((run.Level & 1) != 0) + { + // Odd, range goes to the right of run. + run.Next = range.Left; + range.Left = run; + } + else + { + // Even, range goes to the left of run. + range.Right!.Next = run; + range.Right = run; + } + + range.Level = run.Level; + } + else + { + BidiRange r = new(); + r.Left = r.Right = run; + r.Level = run.Level; + r.Previous = range; + range = r; + } + + run = next; + } + + while (range?.Previous != null) + { + range = BidiRange.MergeWithPrevious(range); + } + + // Terminate. + range!.Right!.Next = null; + return range!.Left!; + } + + /// + /// A node in the linked list of contiguous same-level bidi runs used by . + /// Each node owns the glyph entries at its bidi embedding level and can be reversed in place. + /// + private sealed class OrderedBidiRun + { + private ArrayBuilder info; + + /// + /// Initializes a new instance of the class. + /// + /// The bidi embedding level for this run. + public OrderedBidiRun(int level) => this.Level = level; + + /// Gets the bidi embedding level of this run. + public int Level { get; } + + /// Gets or sets the next run in visual order. + public OrderedBidiRun? Next { get; set; } + + /// Appends an entry to this run. + /// The entry to append. + public void Add(GlyphLayoutData info) => this.info.Add(info); + + /// Returns a slice view over this run's entries. + /// A slice over the entries. + public ArraySlice AsSlice() => this.info.AsSlice(); + + /// Reverses the entries in this run in place (for rule L2). + public void Reverse() => this.AsSlice().Span.Reverse(); + } + + /// + /// An intermediate grouping of links used by the linear-reorder + /// algorithm to stitch pairs of same-level ranges together. + /// + private sealed class BidiRange + { + /// Gets or sets the shared bidi embedding level for this range. + public int Level { get; set; } + + /// Gets or sets the leftmost run in the range. + public OrderedBidiRun? Left { get; set; } + + /// Gets or sets the rightmost run in the range. + public OrderedBidiRun? Right { get; set; } + + /// Gets or sets the previous range in the processing stack. + public BidiRange? Previous { get; set; } + + /// + /// Stitches the current range with its predecessor, producing a single merged range + /// whose internal orientation depends on the predecessor's embedding level parity. + /// + /// The current range whose will be merged. + /// The merged range (always the predecessor instance, reused in place). + public static BidiRange MergeWithPrevious(BidiRange? range) + { + BidiRange previous = range!.Previous!; + BidiRange left; + BidiRange right; + + if ((previous.Level & 1) != 0) + { + // Odd, previous goes to the right of range. + left = range; + right = previous; + } + else + { + // Even, previous goes to the left of range. + left = previous; + right = range; + } + + // Stitch them + left.Right!.Next = right.Left; + previous.Left = left.Left; + previous.Right = right.Right; + + return previous; + } + } +} diff --git a/src/SixLabors.Fonts/TextLineBreakEnumerator.cs b/src/SixLabors.Fonts/TextLineBreakEnumerator.cs new file mode 100644 index 00000000..c85ad37c --- /dev/null +++ b/src/SixLabors.Fonts/TextLineBreakEnumerator.cs @@ -0,0 +1,340 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Collections.Generic; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts; + +/// +/// Breaks a prepared logical line one visual line at a time. +/// +internal sealed class TextLineBreakEnumerator +{ + private readonly LogicalTextLine logicalLine; + private readonly TextOptions options; + private readonly bool breakAll; + private readonly bool keepAll; + private readonly bool breakWord; + private readonly bool normalizeDecomposedAdvances; + private readonly int maxLines; + private readonly CodePoint? ellipsisMarkerCodePoint; + private readonly IReadOnlyList lineBreaks; + private TextLine textLine; + private int processed; + private int lineCount; + private TextLine? current; + + /// + /// Initializes a new instance of the class. + /// + /// The logical line to break. + /// The text options used for layout. + public TextLineBreakEnumerator(in LogicalTextLine logicalLine, TextOptions options) + { + this.logicalLine = logicalLine; + this.options = options; + this.breakAll = options.WordBreaking == WordBreaking.BreakAll; + this.keepAll = options.WordBreaking == WordBreaking.KeepAll; + this.breakWord = options.WordBreaking == WordBreaking.BreakWord; + this.normalizeDecomposedAdvances = options.LayoutMode.IsVertical(); + this.maxLines = options.MaxLines; + this.ellipsisMarkerCodePoint = TextLayout.GetEllipsisMarkerCodePoint(options); + this.lineBreaks = logicalLine.LineBreaks; + + // The breaker mutates the remaining line as it advances, so each cursor owns + // a clone of the immutable prepared line held by TextBlock. + this.textLine = new(logicalLine.TextLine); + } + + /// + /// Gets the current finalized visual line. + /// + public TextLine Current => this.current!; + + /// + /// Advances to the next visual line using the supplied wrapping length. + /// + /// The wrapping length in pixels. + /// when a line was produced. + public bool MoveNext(float wrappingLength) + { + if (this.textLine.Count == 0) + { + return false; + } + + bool shouldWrap = wrappingLength > 0; + + // Wrapping length is always provided in pixels. Convert to inches for comparison. + float scaledWrappingLength = shouldWrap ? wrappingLength / this.options.Dpi : float.MaxValue; + + while (this.textLine.Count > 0) + { + LineBreak? bestBreak = null; + foreach (LineBreak lineBreak in this.lineBreaks) + { + // Skip breaks that are already behind the processed portion. + if (lineBreak.PositionWrap <= this.processed) + { + continue; + } + + // Measure the text up to the adjusted break point. + int measureIndex = lineBreak.PositionMeasure - this.processed; + float advance = this.textLine.MeasureAt(measureIndex); + if (lineBreak.IsHyphenationBreak) + { + advance += this.textLine.GetHyphenationMarkerAdvance( + measureIndex - 1, + this.logicalLine.HyphenationMarkers); + } + + if (advance >= scaledWrappingLength) + { + bestBreak ??= lineBreak; + break; + } + + // If it's a mandatory break, stop immediately. + if (lineBreak.Required) + { + bestBreak = lineBreak; + break; + } + + // Update the best break. + bestBreak = lineBreak; + } + + if (bestBreak != null) + { + if (this.BreakAt(bestBreak.Value, scaledWrappingLength)) + { + return true; + } + + continue; + } + + return this.BreakLastLine(scaledWrappingLength); + } + + return false; + } + + /// + /// Breaks the current remaining line at the supplied break opportunity. + /// + /// The selected line break opportunity. + /// The wrapping length in inches. + /// when a visual line was produced. + private bool BreakAt(LineBreak breakAt, float scaledWrappingLength) + { + if (this.breakAll) + { + return this.BreakAtAnyGlyph(breakAt, scaledWrappingLength); + } + + int hyphenationMarkerIndex = breakAt.PositionMeasure - this.processed - 1; + + // Split the current line at the adjusted break index. + if (this.textLine.TrySplitAt(breakAt, this.keepAll, out TextLine? remaining)) + { + if (breakAt.IsHyphenationBreak) + { + this.textLine.ApplyHyphenationMarker( + hyphenationMarkerIndex, + this.logicalLine.HyphenationMarkers); + } + + if (breakAt.Required + && this.options.TextInteractionMode == TextInteractionMode.Editor + && remaining.Count > 0 + && remaining[0].IsNewLine + && this.textLine.TrySplitTerminalHardBreak(out TextLine? blankLine)) + { + // Consecutive hard breaks need an editable blank line for the break that + // ended this segment, plus the next break still waiting in the remainder. + remaining.InsertAt(0, blankLine); + } + + // If 'keepAll' is true then the break could be later than expected. + this.processed = this.keepAll + ? this.processed + Math.Max(this.textLine.Count, breakAt.PositionWrap - this.processed) + : breakAt.PositionWrap; + + if (this.breakWord) + { + // A break was found, but we need to check if the line is too long + // and break if required. + if (this.textLine.ScaledLineAdvance > scaledWrappingLength && + this.textLine.TrySplitAt(scaledWrappingLength, out TextLine? overflow)) + { + // Reinsert the overflow at the beginning of the remaining line. + this.processed -= overflow.Count; + remaining.InsertAt(0, overflow); + } + } + + bool stopLayout = this.SetCurrent( + this.textLine, + breakAt.Required, + remaining.Count > 0, + scaledWrappingLength); + + this.textLine = stopLayout ? new TextLine() : remaining; + return true; + } + + this.processed += this.textLine.Count; + return false; + } + + /// + /// Breaks the current remaining line using CSS behavior. + /// + /// The selected line break opportunity. + /// The wrapping length in inches. + /// when a visual line was produced. + private bool BreakAtAnyGlyph(LineBreak breakAt, float scaledWrappingLength) + { + TextLine? remaining; + if (breakAt.Required) + { + if (this.textLine.TrySplitAt(breakAt, this.keepAll, out remaining)) + { + this.processed = breakAt.PositionWrap; + + bool stopLayout = this.SetCurrent( + this.textLine, + true, + remaining.Count > 0, + scaledWrappingLength); + + this.textLine = stopLayout ? new TextLine() : remaining; + return true; + } + } + else if (this.textLine.TrySplitAt(scaledWrappingLength, out remaining)) + { + this.processed += this.textLine.Count; + + bool stopLayout = this.SetCurrent( + this.textLine, + false, + remaining.Count > 0, + scaledWrappingLength); + + this.textLine = stopLayout ? new TextLine() : remaining; + return true; + } + else + { + this.processed += this.textLine.Count; + } + + return false; + } + + /// + /// Breaks and finalizes the last remaining line. + /// + /// The wrapping length in inches. + /// when a visual line was produced. + private bool BreakLastLine(float scaledWrappingLength) + { + if (this.breakWord || this.breakAll) + { + while (this.textLine.ScaledLineAdvance > scaledWrappingLength) + { + if (!this.textLine.TrySplitAt(scaledWrappingLength, out TextLine? overflow)) + { + break; + } + + bool stopLayout = this.SetCurrent( + this.textLine, + false, + overflow.Count > 0, + scaledWrappingLength); + + // Width-based overflow splits do not come from a stored LineBreak, so the + // cursor advances by the consumed entries before the next MoveNext scan. + this.processed += this.textLine.Count; + this.textLine = stopLayout ? new TextLine() : overflow; + return true; + } + } + + if (this.options.TextInteractionMode == TextInteractionMode.Editor + && this.textLine.TrySplitTerminalHardBreak(out TextLine? hardBreakLine)) + { + // A terminal Enter has no following glyph for the normal required-break split. + // Editor interaction still needs the next blank line as a caret target. + this.SetCurrent( + this.textLine, + true, + true, + scaledWrappingLength); + + this.textLine = hardBreakLine; + return true; + } + + this.SetCurrent( + this.textLine, + true, + false, + scaledWrappingLength); + + this.textLine = new TextLine(); + return true; + } + + /// + /// Finalizes the current line and stores it as the enumerator result. + /// + /// The line to finalize. + /// Whether the line should skip justification. + /// Whether source text remains after this line. + /// The wrapping length in inches. + /// when no further lines should be produced. + private bool SetCurrent( + TextLine line, + bool skipJustification, + bool hasOverflow, + float scaledWrappingLength) + { + bool isLimitedFinalLine = this.maxLines > -1 && this.lineCount + 1 >= this.maxLines; + if (isLimitedFinalLine && hasOverflow) + { + // A max-lines ellipsis is a final-line transformation: wrapping has already + // chosen the visible line, so the marker replaces the tail of that line and + // the line must behave like a paragraph-final line for justification. + if (this.ellipsisMarkerCodePoint.HasValue) + { + line.ApplyEllipsisMarker(this.ellipsisMarkerCodePoint.Value, scaledWrappingLength, this.options); + } + + skipJustification = true; + } + + bool preserveTrailingBreakingWhitespace = this.options.TextInteractionMode == TextInteractionMode.Editor; + + // Paragraph layout trims trailing breaking whitespace. Editor interaction keeps + // ordinary trailing whitespace addressable so typed spaces can advance the caret. + this.current = line.Finalize( + skipJustification, + this.normalizeDecomposedAdvances, + preserveTrailingBreakingWhitespace); + + if (!this.current.SkipJustification) + { + this.current.Justify(this.options); + } + + this.lineCount++; + return isLimitedFinalLine; + } +} diff --git a/src/SixLabors.Fonts/TextMeasurer.cs b/src/SixLabors.Fonts/TextMeasurer.cs index 9b5e4e04..13e27df3 100644 --- a/src/SixLabors.Fonts/TextMeasurer.cs +++ b/src/SixLabors.Fonts/TextMeasurer.cs @@ -1,26 +1,14 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. -using SixLabors.Fonts.Unicode; - namespace SixLabors.Fonts; /// -/// Encapsulated logic for laying out and then measuring text properties. +/// Encapsulates logic for laying out and then measuring text properties. /// public static class TextMeasurer { - /// - /// Measures the full set of layout metrics for the supplied text in a single pass. - /// - /// The text. - /// The text shaping options. - /// A value containing every measurement for the laid-out text. - /// - /// This method is cheaper than calling multiple granular overloads back-to-back because the text is - /// shaped and laid out only once. Prefer the granular overloads (for example ) - /// when only one or two values are required, because they avoid materializing the per-character and per-line arrays. - /// + /// public static TextMetrics Measure(string text, TextOptions options) => Measure(text.AsSpan(), options); @@ -28,112 +16,15 @@ public static TextMetrics Measure(string text, TextOptions options) /// Measures the full set of layout metrics for the supplied text in a single pass. /// /// The text. - /// The text shaping options. - /// A value containing every measurement for the laid-out text. - /// - /// This method is cheaper than calling multiple granular overloads back-to-back because the text is - /// shaped and laid out only once. Prefer the granular overloads (for example ) - /// when only one or two values are required, because they avoid materializing the per-character and per-line arrays. - /// + /// The text options. controls wrapping; use -1 to disable wrapping. + /// A instance containing every measurement for the laid-out text. public static TextMetrics Measure(ReadOnlySpan text, TextOptions options) { - if (text.IsEmpty) - { - return TextMetrics.Empty; - } - - TextLayout.TextBox textBox = TextLayout.ProcessText(text, options); - List glyphLayouts = TextLayout.LayoutText(textBox, options); - float dpi = options.Dpi; - bool isHorizontal = options.LayoutMode.IsHorizontal(); - - FontRectangle advance = GetAdvance(textBox, dpi, isHorizontal); - - int count = glyphLayouts.Count; - GlyphBounds[] characterAdvances = new GlyphBounds[count]; - GlyphBounds[] characterSizes = new GlyphBounds[count]; - GlyphBounds[] characterBounds = new GlyphBounds[count]; - GlyphBounds[] characterRenderableBounds = new GlyphBounds[count]; - - float left = float.MaxValue; - float top = float.MaxValue; - float right = float.MinValue; - float bottom = float.MinValue; - - for (int i = 0; i < count; i++) - { - GlyphLayout g = glyphLayouts[i]; - FontRectangle glyphBox = g.BoundingBox(dpi); - FontRectangle advanceRect = new(g.BoxLocation.X * dpi, g.BoxLocation.Y * dpi, g.AdvanceX * dpi, g.AdvanceY * dpi); - FontRectangle renderableRect = FontRectangle.Union(advanceRect, glyphBox); - - CodePoint codePoint = g.Glyph.GlyphMetrics.CodePoint; - int graphemeIndex = g.GraphemeIndex; - int stringIndex = g.StringIndex; - - FontRectangle advanceBox = new(0, 0, g.AdvanceX * dpi, g.AdvanceY * dpi); - FontRectangle sizeBox = new(0, 0, glyphBox.Width, glyphBox.Height); - - characterAdvances[i] = new GlyphBounds(codePoint, in advanceBox, graphemeIndex, stringIndex); - characterSizes[i] = new GlyphBounds(codePoint, in sizeBox, graphemeIndex, stringIndex); - characterBounds[i] = new GlyphBounds(codePoint, in glyphBox, graphemeIndex, stringIndex); - characterRenderableBounds[i] = new GlyphBounds(codePoint, in renderableRect, graphemeIndex, stringIndex); - - if (glyphBox.Left < left) - { - left = glyphBox.Left; - } - - if (glyphBox.Top < top) - { - top = glyphBox.Top; - } - - if (glyphBox.Right > right) - { - right = glyphBox.Right; - } - - if (glyphBox.Bottom > bottom) - { - bottom = glyphBox.Bottom; - } - } - - FontRectangle bounds = count == 0 - ? FontRectangle.Empty - : FontRectangle.FromLTRB(left, top, right, bottom); - FontRectangle size = new(0, 0, bounds.Width, bounds.Height); - FontRectangle absoluteAdvance = new(options.Origin.X, options.Origin.Y, advance.Width, advance.Height); - FontRectangle renderableBounds = FontRectangle.Union(absoluteAdvance, bounds); - - LineMetrics[] lineMetrics = GetLineMetrics(textBox, options); - - return new TextMetrics( - advance, - bounds, - size, - renderableBounds, - textBox.TextLines.Count, - characterAdvances, - characterSizes, - characterBounds, - characterRenderableBounds, - lineMetrics); + TextBlock block = new(text, options); + return block.Measure(options.WrappingLength); } - /// - /// Measures the logical advance of the text in pixel units. - /// - /// The text. - /// The text shaping options. - /// The logical advance rectangle of the text if it was to be rendered. - /// - /// This measurement reflects line-box height and horizontal or vertical text advance from the layout model. - /// It does not guarantee that all rendered glyph pixels fit within the returned rectangle. - /// Use for glyph ink bounds or - /// for the union of logical advance and rendered bounds. - /// + /// public static FontRectangle MeasureAdvance(string text, TextOptions options) => MeasureAdvance(text.AsSpan(), options); @@ -141,14 +32,8 @@ public static FontRectangle MeasureAdvance(string text, TextOptions options) /// Measures the logical advance of the text in pixel units. /// /// The text. - /// The text shaping options. + /// The text options. controls wrapping; use -1 to disable wrapping. /// The logical advance rectangle of the text if it was to be rendered. - /// - /// This measurement reflects line-box height and horizontal or vertical text advance from the layout model. - /// It does not guarantee that all rendered glyph pixels fit within the returned rectangle. - /// Use for glyph ink bounds or - /// for the union of logical advance and rendered bounds. - /// public static FontRectangle MeasureAdvance(ReadOnlySpan text, TextOptions options) { if (text.IsEmpty) @@ -156,66 +41,15 @@ public static FontRectangle MeasureAdvance(ReadOnlySpan text, TextOptions return FontRectangle.Empty; } - return GetAdvance(TextLayout.ProcessText(text, options), options.Dpi, options.LayoutMode.IsHorizontal()); + TextBlock block = new(text, options); + return block.MeasureAdvance(options.WrappingLength); } - /// - /// Measures the normalized rendered size of the text in pixel units. - /// - /// The text. - /// The text shaping options. - /// The rendered size of the text with the origin normalized to (0, 0). - /// - /// This is equivalent to measuring the rendered bounds and returning only the width and height. - /// Use when the returned X and Y offset are also required. - /// - public static FontRectangle MeasureSize(string text, TextOptions options) - => MeasureSize(text.AsSpan(), options); - - /// - /// Measures the normalized rendered size of the text in pixel units. - /// - /// The text. - /// The text shaping options. - /// The rendered size of the text with the origin normalized to (0, 0). - /// - /// This is equivalent to measuring the rendered bounds and returning only the width and height. - /// Use when the returned X and Y offset are also required. - /// - public static FontRectangle MeasureSize(ReadOnlySpan text, TextOptions options) - { - FontRectangle bounds = MeasureBounds(text, options); - return new FontRectangle(0, 0, bounds.Width, bounds.Height); - } - - /// - /// Measures the rendered glyph bounds of the text in pixel units. - /// - /// The text. - /// The text shaping options. - /// The rendered glyph bounds of the text if it was to be rendered. - /// - /// This measures the tight ink bounds enclosing all rendered glyphs. The returned rectangle - /// may be smaller or larger than the logical advance and may have a non-zero origin. - /// Use for the logical layout box or - /// for the union of both. - /// + /// public static FontRectangle MeasureBounds(string text, TextOptions options) => MeasureBounds(text.AsSpan(), options); - /// - /// Measures the full renderable bounds of the text in pixel units. - /// - /// The text. - /// The text shaping options. - /// - /// The union of the logical advance rectangle and the rendered glyph bounds if the text was to be rendered. - /// - /// - /// The returned rectangle is in absolute coordinates and is large enough to contain both the logical advance - /// rectangle and the rendered glyph bounds. - /// Use this method when both typographic advance and rendered glyph overshoot must fit within the same rectangle. - /// + /// public static FontRectangle MeasureRenderableBounds(string text, TextOptions options) => MeasureRenderableBounds(text.AsSpan(), options); @@ -223,14 +57,8 @@ public static FontRectangle MeasureRenderableBounds(string text, TextOptions opt /// Measures the rendered glyph bounds of the text in pixel units. /// /// The text. - /// The text shaping options. + /// The text options. controls wrapping; use -1 to disable wrapping. /// The rendered glyph bounds of the text if it was to be rendered. - /// - /// This measures the tight ink bounds enclosing all rendered glyphs. The returned rectangle - /// may be smaller or larger than the logical advance and may have a non-zero origin. - /// Use for the logical layout box or - /// for the union of both. - /// public static FontRectangle MeasureBounds(ReadOnlySpan text, TextOptions options) { if (text.IsEmpty) @@ -238,22 +66,18 @@ public static FontRectangle MeasureBounds(ReadOnlySpan text, TextOptions o return FontRectangle.Empty; } - return TextLayout.GetBounds(TextLayout.ProcessText(text, options), options); + TextBlock block = new(text, options); + return block.MeasureBounds(options.WrappingLength); } /// /// Measures the full renderable bounds of the text in pixel units. /// /// The text. - /// The text shaping options. + /// The text options. controls wrapping; use -1 to disable wrapping. /// /// The union of the logical advance rectangle and the rendered glyph bounds if the text was to be rendered. /// - /// - /// The returned rectangle is in absolute coordinates and is large enough to contain both the logical advance - /// rectangle and the rendered glyph bounds. - /// Use this method when both typographic advance and rendered glyph overshoot must fit within the same rectangle. - /// public static FontRectangle MeasureRenderableBounds(ReadOnlySpan text, TextOptions options) { if (text.IsEmpty) @@ -261,127 +85,74 @@ public static FontRectangle MeasureRenderableBounds(ReadOnlySpan text, Tex return FontRectangle.Empty; } - TextLayout.TextBox textBox = TextLayout.ProcessText(text, options); - FontRectangle advance = GetAdvance(textBox, options.Dpi, options.LayoutMode.IsHorizontal()); - FontRectangle absoluteAdvance = new(options.Origin.X, options.Origin.Y, advance.Width, advance.Height); - FontRectangle bounds = TextLayout.GetBounds(textBox, options); - return FontRectangle.Union(absoluteAdvance, bounds); + TextBlock block = new(text, options); + return block.MeasureRenderableBounds(options.WrappingLength); } - /// - /// Measures the logical advance of each laid-out character entry in pixel units. - /// - /// The text. - /// The text shaping options. - /// The list of per-entry logical advances of the text if it was to be rendered. - /// Whether any of the entries had non-empty advances. - /// - /// Each entry reflects the typographic advance width and height for one character. - /// Use for per-character ink bounds or - /// for the union of both. - /// - public static bool TryMeasureCharacterAdvances(string text, TextOptions options, out ReadOnlySpan advances) - => TryMeasureCharacterAdvances(text.AsSpan(), options, out advances); + /// + public static ReadOnlyMemory GetGlyphMetrics(string text, TextOptions options) + => GetGlyphMetrics(text.AsSpan(), options); /// - /// Measures the logical advance of each laid-out character entry in pixel units. + /// Gets the positioned metrics of each laid-out glyph entry in pixel units. /// /// The text. - /// The text shaping options. - /// The list of per-entry logical advances of the text if it was to be rendered. - /// Whether any of the entries had non-empty advances. - /// - /// Each entry reflects the typographic advance width and height for one character. - /// Use for per-character ink bounds or - /// for the union of both. - /// - public static bool TryMeasureCharacterAdvances(ReadOnlySpan text, TextOptions options, out ReadOnlySpan advances) - => TryGetCharacterAdvances(TextLayout.GenerateLayout(text, options), options.Dpi, out advances); + /// The text options. controls wrapping; use -1 to disable wrapping. + /// A read-only memory region containing the per-glyph metrics entries of the text if it was to be rendered. + public static ReadOnlyMemory GetGlyphMetrics(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return ReadOnlyMemory.Empty; + } - /// - /// Measures the normalized rendered size of each laid-out character entry in pixel units. - /// - /// The text. - /// The text shaping options. - /// The list of per-entry rendered sizes with the origin normalized to (0, 0). - /// Whether any of the entries had non-empty dimensions. - public static bool TryMeasureCharacterSizes(string text, TextOptions options, out ReadOnlySpan sizes) - => TryMeasureCharacterSizes(text.AsSpan(), options, out sizes); + TextBlock block = new(text, options); + return block.GetGlyphMetrics(options.WrappingLength); + } - /// - /// Measures the normalized rendered size of each laid-out character entry in pixel units. - /// - /// The text. - /// The text shaping options. - /// The list of per-entry rendered sizes with the origin normalized to (0, 0). - /// Whether any of the entries had non-empty dimensions. - public static bool TryMeasureCharacterSizes(ReadOnlySpan text, TextOptions options, out ReadOnlySpan sizes) - => TryGetCharacterSizes(TextLayout.GenerateLayout(text, options), options.Dpi, out sizes); + /// + public static ReadOnlyMemory GetGraphemeMetrics(string text, TextOptions options) + => GetGraphemeMetrics(text.AsSpan(), options); /// - /// Measures the rendered glyph bounds of each laid-out character entry in pixel units. + /// Gets the positioned metrics of each laid-out grapheme in pixel units. /// /// The text. - /// The text shaping options. - /// The list of per-entry rendered glyph bounds of the text if it was to be rendered. - /// Whether any of the entries had non-empty bounds. - /// - /// Each entry reflects the tight ink bounds of one rendered glyph. - /// Use for per-character logical advances or - /// for the union of both. - /// - public static bool TryMeasureCharacterBounds(string text, TextOptions options, out ReadOnlySpan bounds) - => TryMeasureCharacterBounds(text.AsSpan(), options, out bounds); + /// The text options. controls wrapping; use -1 to disable wrapping. + /// A read-only memory region containing the per-grapheme metrics entries of the text if it was to be rendered. + public static ReadOnlyMemory GetGraphemeMetrics(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return ReadOnlyMemory.Empty; + } - /// - /// Measures the full renderable bounds of each laid-out character entry in pixel units. - /// - /// The text. - /// The text shaping options. - /// The list of per-entry renderable bounds of the text if it was to be rendered. - /// Whether any of the entries had non-empty bounds. - /// - /// Each returned rectangle is in absolute coordinates and is large enough to contain both the logical advance - /// rectangle and the rendered glyph bounds for the corresponding laid-out entry. - /// - public static bool TryMeasureCharacterRenderableBounds(string text, TextOptions options, out ReadOnlySpan bounds) - => TryMeasureCharacterRenderableBounds(text.AsSpan(), options, out bounds); + TextBlock block = new(text, options); + return block.GetGraphemeMetrics(options.WrappingLength); + } - /// - /// Measures the rendered glyph bounds of each laid-out character entry in pixel units. - /// - /// The text. - /// The text shaping options. - /// The list of per-entry rendered glyph bounds of the text if it was to be rendered. - /// Whether any of the entries had non-empty bounds. - /// - /// Each entry reflects the tight ink bounds of one rendered glyph. - /// Use for per-character logical advances or - /// for the union of both. - /// - public static bool TryMeasureCharacterBounds(ReadOnlySpan text, TextOptions options, out ReadOnlySpan bounds) - => TryGetCharacterBounds(TextLayout.GenerateLayout(text, options), options.Dpi, out bounds); + /// + public static ReadOnlyMemory GetWordMetrics(string text, TextOptions options) + => GetWordMetrics(text.AsSpan(), options); /// - /// Measures the full renderable bounds of each laid-out character entry in pixel units. + /// Gets the positioned metrics of each Unicode word-boundary segment in pixel units. /// /// The text. - /// The text shaping options. - /// The list of per-entry renderable bounds of the text if it was to be rendered. - /// Whether any of the entries had non-empty bounds. - /// - /// Each returned rectangle is in absolute coordinates and is large enough to contain both the logical advance - /// rectangle and the rendered glyph bounds for the corresponding laid-out entry. - /// - public static bool TryMeasureCharacterRenderableBounds(ReadOnlySpan text, TextOptions options, out ReadOnlySpan bounds) - => TryGetCharacterRenderableBounds(TextLayout.GenerateLayout(text, options), options.Dpi, out bounds); + /// The text options. controls wrapping; use -1 to disable wrapping. + /// A read-only memory region containing the per-word-boundary segment metrics entries of the text if it was to be rendered. + public static ReadOnlyMemory GetWordMetrics(ReadOnlySpan text, TextOptions options) + { + if (text.IsEmpty) + { + return ReadOnlyMemory.Empty; + } - /// - /// Gets the number of laid-out lines contained within the text. - /// - /// The text. - /// The text shaping options. - /// The laid-out line count. + TextBlock block = new(text, options); + return block.GetWordMetrics(options.WrappingLength); + } + + /// public static int CountLines(string text, TextOptions options) => CountLines(text.AsSpan(), options); @@ -389,7 +160,7 @@ public static int CountLines(string text, TextOptions options) /// Gets the number of laid-out lines contained within the text. /// /// The text. - /// The text shaping options. + /// The text options. controls wrapping; use -1 to disable wrapping. /// The laid-out line count. public static int CountLines(ReadOnlySpan text, TextOptions options) { @@ -398,300 +169,30 @@ public static int CountLines(ReadOnlySpan text, TextOptions options) return 0; } - return TextLayout.ProcessText(text, options).TextLines.Count; + TextBlock block = new(text, options); + return block.CountLines(options.WrappingLength); } - /// - /// Gets per-line layout metrics for the supplied text. - /// - /// The text to measure. - /// The text shaping and layout options. - /// - /// An array of in pixel units, one entry per laid-out line. - /// - /// - /// - /// The returned and are expressed - /// in the primary flow direction for the active layout mode. - /// - /// - /// , , and - /// are line-box positions relative to the current line origin and are suitable for drawing guide lines. - /// - /// - /// Horizontal layouts: Start = X position, Extent = width. - /// Vertical layouts: Start = Y position, Extent = height. - /// - /// - public static LineMetrics[] GetLineMetrics(string text, TextOptions options) + /// + public static ReadOnlyMemory GetLineMetrics(string text, TextOptions options) => GetLineMetrics(text.AsSpan(), options); /// /// Gets per-line layout metrics for the supplied text. /// /// The text to measure. - /// The text shaping and layout options. + /// The text options. controls wrapping; use -1 to disable wrapping. /// - /// An array of in pixel units, one entry per laid-out line. + /// A read-only memory region containing in pixel units, one entry per laid-out line. /// - /// - /// - /// The returned and are expressed - /// in the primary flow direction for the active layout mode. - /// - /// - /// , , and - /// are line-box positions relative to the current line origin and are suitable for drawing guide lines. - /// - /// - /// Horizontal layouts: Start = X position, Extent = width. - /// Vertical layouts: Start = Y position, Extent = height. - /// - /// - public static LineMetrics[] GetLineMetrics(ReadOnlySpan text, TextOptions options) + public static ReadOnlyMemory GetLineMetrics(ReadOnlySpan text, TextOptions options) { if (text.IsEmpty) { - return []; - } - - return GetLineMetrics(TextLayout.ProcessText(text, options), options); - } - - private static LineMetrics[] GetLineMetrics(TextLayout.TextBox textBox, TextOptions options) - { - LineMetrics[] metrics = new LineMetrics[textBox.TextLines.Count]; - - // Determine the line-box extent used for alignment within the flow direction. - float maxScaledAdvance = textBox.ScaledMaxAdvance(); - if (options.TextAlignment != TextAlignment.Start && options.WrappingLength > 0) - { - maxScaledAdvance = MathF.Max(options.WrappingLength / options.Dpi, maxScaledAdvance); - } - - TextDirection direction = textBox.TextDirection(); - LayoutMode layoutMode = options.LayoutMode; - bool isHorizontalLayout = layoutMode.IsHorizontal(); - - for (int i = 0; i < textBox.TextLines.Count; i++) - { - TextLayout.TextLine line = textBox.TextLines[i]; - - // Calculate the line start position in the current flow direction. - float offset = isHorizontalLayout - ? TextLayout.CalculateLineOffsetX( - line.ScaledLineAdvance, - maxScaledAdvance, - options.HorizontalAlignment, - options.TextAlignment, - direction) - : TextLayout.CalculateLineOffsetY( - line.ScaledLineAdvance, - maxScaledAdvance, - options.VerticalAlignment, - options.TextAlignment, - direction); - - // Delta captured during layout when ascender/descender were symmetrically - // adjusted to match browser-like line-box behavior. - float delta = line.ScaledMaxDelta; - - // Core typographic region within the line box. - // We add back 2*delta to recover the pre-adjustment ascender+descender span - // used for deriving guide positions. - float coreHeight = line.ScaledMaxAscender + line.ScaledMaxDescender + (2 * delta); - - // Additional leading in the line box (for example from line spacing). - float extra = line.ScaledMaxLineHeight - coreHeight; - - // Baseline position within the line box. - float baseline = (extra * 0.5f) + line.ScaledMaxAscender + delta; - - // Ascender line position relative to the same origin. - float ascender = baseline - line.ScaledMaxAscender + delta; - - // Descender line position relative to the same origin. - float descender = baseline + line.ScaledMaxDescender + delta; - - metrics[i] = new LineMetrics( - ascender * options.Dpi, - baseline * options.Dpi, - descender * options.Dpi, - line.ScaledMaxLineHeight * options.Dpi, - offset * options.Dpi, - line.ScaledLineAdvance * options.Dpi); - } - - return metrics; - } - - internal static FontRectangle GetAdvance(TextLayout.TextBox textBox, float dpi, bool isHorizontalLayout) - { - if (textBox.TextLines.Count == 0) - { - return FontRectangle.Empty; - } - - if (isHorizontalLayout) - { - float width = 0; - float height = 0; - for (int i = 0; i < textBox.TextLines.Count; i++) - { - TextLayout.TextLine line = textBox.TextLines[i]; - width = MathF.Max(width, line.ScaledLineAdvance); - height += line.ScaledMaxLineHeight; - } - - return new FontRectangle(0, 0, width * dpi, height * dpi); - } - - float verticalWidth = 0; - float verticalHeight = 0; - for (int i = 0; i < textBox.TextLines.Count; i++) - { - TextLayout.TextLine line = textBox.TextLines[i]; - verticalWidth += line.ScaledMaxLineHeight; - verticalHeight = MathF.Max(verticalHeight, line.ScaledLineAdvance); - } - - return new FontRectangle(0, 0, verticalWidth * dpi, verticalHeight * dpi); - } - - internal static FontRectangle GetSize(IReadOnlyList glyphLayouts, float dpi) - { - FontRectangle bounds = GetBounds(glyphLayouts, dpi); - return new FontRectangle(0, 0, bounds.Width, bounds.Height); - } - - internal static FontRectangle GetBounds(IReadOnlyList glyphLayouts, float dpi) - { - if (glyphLayouts.Count == 0) - { - return FontRectangle.Empty; - } - - float left = float.MaxValue; - float top = float.MaxValue; - float bottom = float.MinValue; - float right = float.MinValue; - for (int i = 0; i < glyphLayouts.Count; i++) - { - FontRectangle box = glyphLayouts[i].BoundingBox(dpi); - if (left > box.Left) - { - left = box.Left; - } - - if (top > box.Top) - { - top = box.Top; - } - - if (bottom < box.Bottom) - { - bottom = box.Bottom; - } - - if (right < box.Right) - { - right = box.Right; - } - } - - return FontRectangle.FromLTRB(left, top, right, bottom); - } - - internal static bool TryGetCharacterAdvances(IReadOnlyList glyphLayouts, float dpi, out ReadOnlySpan characterBounds) - { - bool hasSize = false; - if (glyphLayouts.Count == 0) - { - characterBounds = []; - return hasSize; - } - - GlyphBounds[] characterBoundsList = new GlyphBounds[glyphLayouts.Count]; - for (int i = 0; i < glyphLayouts.Count; i++) - { - GlyphLayout glyph = glyphLayouts[i]; - FontRectangle bounds = new(0, 0, glyph.AdvanceX * dpi, glyph.AdvanceY * dpi); - hasSize |= bounds.Width > 0 || bounds.Height > 0; - characterBoundsList[i] = new GlyphBounds(glyph.Glyph.GlyphMetrics.CodePoint, in bounds, glyph.GraphemeIndex, glyph.StringIndex); - } - - characterBounds = characterBoundsList; - return hasSize; - } - - internal static bool TryGetCharacterSizes(IReadOnlyList glyphLayouts, float dpi, out ReadOnlySpan characterBounds) - { - bool hasSize = false; - if (glyphLayouts.Count == 0) - { - characterBounds = []; - return hasSize; - } - - GlyphBounds[] characterBoundsList = new GlyphBounds[glyphLayouts.Count]; - - for (int i = 0; i < glyphLayouts.Count; i++) - { - GlyphLayout g = glyphLayouts[i]; - FontRectangle bounds = g.BoundingBox(dpi); - bounds = new(0, 0, bounds.Width, bounds.Height); - - hasSize |= bounds.Width > 0 || bounds.Height > 0; - characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, in bounds, g.GraphemeIndex, g.StringIndex); - } - - characterBounds = characterBoundsList; - return hasSize; - } - - internal static bool TryGetCharacterBounds(IReadOnlyList glyphLayouts, float dpi, out ReadOnlySpan characterBounds) - { - bool hasSize = false; - if (glyphLayouts.Count == 0) - { - characterBounds = []; - return hasSize; - } - - GlyphBounds[] characterBoundsList = new GlyphBounds[glyphLayouts.Count]; - for (int i = 0; i < glyphLayouts.Count; i++) - { - GlyphLayout g = glyphLayouts[i]; - FontRectangle bounds = g.BoundingBox(dpi); - hasSize |= bounds.Width > 0 || bounds.Height > 0; - characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, in bounds, g.GraphemeIndex, g.StringIndex); - } - - characterBounds = characterBoundsList; - return hasSize; - } - - internal static bool TryGetCharacterRenderableBounds(IReadOnlyList glyphLayouts, float dpi, out ReadOnlySpan characterBounds) - { - bool hasSize = false; - if (glyphLayouts.Count == 0) - { - characterBounds = []; - return hasSize; - } - - GlyphBounds[] characterBoundsList = new GlyphBounds[glyphLayouts.Count]; - for (int i = 0; i < glyphLayouts.Count; i++) - { - GlyphLayout g = glyphLayouts[i]; - FontRectangle glyphBounds = g.BoundingBox(dpi); - FontRectangle advance = new(g.BoxLocation.X * dpi, g.BoxLocation.Y * dpi, g.AdvanceX * dpi, g.AdvanceY * dpi); - FontRectangle bounds = FontRectangle.Union(advance, glyphBounds); - hasSize |= bounds.Width > 0 || bounds.Height > 0; - characterBoundsList[i] = new GlyphBounds(g.Glyph.GlyphMetrics.CodePoint, in bounds, g.GraphemeIndex, g.StringIndex); + return ReadOnlyMemory.Empty; } - characterBounds = characterBoundsList; - return hasSize; + TextBlock block = new(text, options); + return block.GetLineMetrics(options.WrappingLength); } } diff --git a/src/SixLabors.Fonts/TextMetrics.cs b/src/SixLabors.Fonts/TextMetrics.cs index c86b2476..011140bc 100644 --- a/src/SixLabors.Fonts/TextMetrics.cs +++ b/src/SixLabors.Fonts/TextMetrics.cs @@ -1,61 +1,52 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using System.Numerics; + namespace SixLabors.Fonts; /// /// Encapsulates the full set of measurement results for laid-out text. /// -/// -/// -/// This type aggregates every measurement exposed by the granular overloads. -/// Producing one instance is cheaper than calling multiple granular overloads -/// back-to-back because the text is shaped and laid out only once. -/// -/// -/// For callers that only require one or two values, the granular overloads on -/// remain the most efficient choice because they avoid materializing the per-character and per-line arrays. -/// -/// -public readonly struct TextMetrics +public sealed class TextMetrics { - /// - /// Represents an empty instance with zeroed rectangles and empty collections. - /// - public static readonly TextMetrics Empty = new( - FontRectangle.Empty, - FontRectangle.Empty, - FontRectangle.Empty, - FontRectangle.Empty, - 0, - [], - [], - [], - [], - []); + private readonly TextBlock textBlock; + private readonly TextBox textBox; + private readonly float wrappingLength; + private readonly LayoutMode layoutMode; + private readonly TextDirection textDirection; + private readonly GraphemeMetrics[] graphemeMetrics; + private readonly LineMetrics[] lineMetrics; + private readonly WordMetrics[] wordMetrics; + private GlyphMetrics[]? glyphMetrics; internal TextMetrics( + TextBlock textBlock, + TextBox textBox, + float wrappingLength, FontRectangle advance, FontRectangle bounds, - FontRectangle size, FontRectangle renderableBounds, int lineCount, - GlyphBounds[] characterAdvances, - GlyphBounds[] characterSizes, - GlyphBounds[] characterBounds, - GlyphBounds[] characterRenderableBounds, - LineMetrics[] lines) + GraphemeMetrics[] graphemes, + LineMetrics[] lines, + WordMetrics[] words) { + this.textBlock = textBlock; + this.textBox = textBox; + this.wrappingLength = wrappingLength; + this.layoutMode = textBlock.Options.LayoutMode; + this.textDirection = textBox.TextLines.Count == 0 + ? (textBlock.Options.TextDirection == TextDirection.RightToLeft ? TextDirection.RightToLeft : TextDirection.LeftToRight) + : textBox.TextDirection(); + this.Advance = advance; this.Bounds = bounds; - this.Size = size; this.RenderableBounds = renderableBounds; this.LineCount = lineCount; - this.CharacterAdvances = characterAdvances; - this.CharacterSizes = characterSizes; - this.CharacterBounds = characterBounds; - this.CharacterRenderableBounds = characterRenderableBounds; - this.Lines = lines; + this.graphemeMetrics = graphemes; + this.lineMetrics = lines; + this.wordMetrics = words; } /// @@ -76,17 +67,9 @@ internal TextMetrics( /// public FontRectangle Bounds { get; } - /// - /// Gets the normalized rendered size of the text in pixel units with the origin at (0, 0). - /// - /// - /// Equivalent to with a zeroed origin. - /// - public FontRectangle Size { get; } - /// /// Gets the union of the logical advance rectangle (positioned at the text options origin) - /// and the rendered glyph bounds. + /// and the rendered glyph bounds in pixel units. /// /// /// Use this rectangle when both typographic advance and rendered glyph overshoot @@ -100,41 +83,146 @@ internal TextMetrics( public int LineCount { get; } /// - /// Gets the logical advance of each laid-out character in pixel units. + /// Gets the grapheme metrics entries in final layout order. /// - /// - /// Each entry reflects the typographic advance width and height for one character, - /// with an origin of (0, 0). - /// - public IReadOnlyList CharacterAdvances { get; } + public ReadOnlySpan GraphemeMetrics => this.graphemeMetrics; /// - /// Gets the normalized rendered size of each laid-out character in pixel units. + /// Gets the per-line layout metrics for the text. /// - /// - /// Each entry is the tight ink bounds of one glyph with the origin normalized to (0, 0). - /// - public IReadOnlyList CharacterSizes { get; } + public ReadOnlySpan LineMetrics => this.lineMetrics; /// - /// Gets the rendered glyph bounds of each laid-out character in pixel units. + /// Gets the word-boundary segment metrics in source order. /// - /// - /// Each entry reflects the tight ink bounds of one rendered glyph. - /// - public IReadOnlyList CharacterBounds { get; } + public ReadOnlySpan WordMetrics => this.wordMetrics; /// - /// Gets the full renderable bounds of each laid-out character in pixel units. + /// Hit tests the supplied point against the laid-out grapheme advance bounds. /// - /// - /// Each entry is the union of the logical advance rectangle and the rendered glyph bounds - /// for the corresponding laid-out character. - /// - public IReadOnlyList CharacterRenderableBounds { get; } + /// The point in pixel units. + /// The hit-tested grapheme position. + public TextHit HitTest(Vector2 point) + => TextInteraction.HitTest( + this.LineMetrics, + this.GraphemeMetrics, + point, + this.layoutMode); /// - /// Gets the per-line layout metrics for the text. + /// Gets the caret position for the supplied hit. + /// + /// The hit-tested grapheme position. + /// The caret position in pixel units. + public CaretPosition GetCaretPosition(TextHit hit) + => TextInteraction.GetCaretPosition( + this.LineMetrics, + this.GraphemeMetrics, + hit.GraphemeInsertionIndex, + this.layoutMode); + + /// + /// Gets an absolute caret position in the laid-out text. /// - public IReadOnlyList Lines { get; } + /// The absolute caret placement. + /// The caret position in pixel units. + public CaretPosition GetCaret(CaretPlacement placement) + => TextInteraction.GetCaret( + this.LineMetrics, + this.GraphemeMetrics, + placement, + this.layoutMode, + this.textDirection); + + /// + /// Moves the supplied caret by the requested operation. + /// + /// The current caret position. + /// The movement operation. + /// The moved caret position in pixel units. + public CaretPosition MoveCaret(CaretPosition caret, CaretMovement movement) + => TextInteraction.MoveCaret( + this.LineMetrics, + this.GraphemeMetrics, + this.WordMetrics, + caret, + movement, + this.layoutMode, + this.textDirection); + + /// + /// Gets the word metrics for the word-boundary segment containing the supplied hit-tested grapheme position. + /// + /// The hit-tested grapheme position. + /// The word metrics containing the hit grapheme. + public WordMetrics GetWordMetrics(TextHit hit) + => TextInteraction.GetWordMetrics(this.WordMetrics, hit.GraphemeIndex); + + /// + /// Gets the word metrics for the word-boundary segment containing the supplied caret position. + /// + /// The caret position. + /// The word metrics containing the caret's grapheme insertion index. + public WordMetrics GetWordMetrics(CaretPosition caret) + => TextInteraction.GetWordMetrics(this.WordMetrics, caret.GraphemeIndex); + + /// + /// Gets selection bounds between two hit-tested grapheme positions. + /// + /// The fixed selection endpoint. + /// The active selection endpoint. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(TextHit anchor, TextHit focus) + => TextInteraction.GetSelectionBounds( + this.LineMetrics, + this.GraphemeMetrics, + anchor.GraphemeInsertionIndex, + focus.GraphemeInsertionIndex, + this.layoutMode); + + /// + /// Gets selection bounds between two caret positions. + /// + /// The fixed selection endpoint. + /// The active selection endpoint. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(CaretPosition anchor, CaretPosition focus) + => TextInteraction.GetSelectionBounds( + this.LineMetrics, + this.GraphemeMetrics, + anchor.GraphemeIndex, + focus.GraphemeIndex, + this.layoutMode); + + /// + /// Gets selection bounds for the supplied grapheme metrics. + /// + /// The grapheme metrics to select. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(GraphemeMetrics metrics) + => TextInteraction.GetSelectionBounds( + this.LineMetrics, + this.GraphemeMetrics, + metrics, + this.layoutMode); + + /// + /// Gets selection bounds for the supplied word metrics. + /// + /// The word metrics to select. + /// A read-only memory region containing the selection bounds in visual order and pixel units. + public ReadOnlyMemory GetSelectionBounds(WordMetrics metrics) + => TextInteraction.GetSelectionBounds( + this.LineMetrics, + this.GraphemeMetrics, + metrics.GraphemeStart, + metrics.GraphemeEnd, + this.layoutMode); + + /// + public ReadOnlyMemory GetGlyphMetrics() + => this.glyphMetrics ??= TextBlock.GetGlyphMetricsArray( + this.textBox, + this.textBlock.Options, + this.wrappingLength); } diff --git a/src/SixLabors.Fonts/TextOptions.cs b/src/SixLabors.Fonts/TextOptions.cs index a6150b2e..64d9b920 100644 --- a/src/SixLabors.Fonts/TextOptions.cs +++ b/src/SixLabors.Fonts/TextOptions.cs @@ -3,6 +3,7 @@ using System.Numerics; using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts; @@ -36,8 +37,15 @@ public TextOptions(TextOptions options) this.LineSpacing = options.LineSpacing; this.Origin = options.Origin; this.WrappingLength = options.WrappingLength; + this.MaxLines = options.MaxLines; this.WordBreaking = options.WordBreaking; + this.TextEllipsis = options.TextEllipsis; + this.CustomEllipsis = options.CustomEllipsis; + this.TextHyphenation = options.TextHyphenation; + this.CustomHyphen = options.CustomHyphen; this.TextDirection = options.TextDirection; + this.TextBidiMode = options.TextBidiMode; + this.TextInteractionMode = options.TextInteractionMode; this.TextAlignment = options.TextAlignment; this.TextJustification = options.TextJustification; this.HorizontalAlignment = options.HorizontalAlignment; @@ -131,16 +139,54 @@ public float LineSpacing /// public float WrappingLength { get; set; } = -1F; + /// + /// Gets or sets the maximum number of lines to lay out. + /// + /// + /// If value is -1 then the number of lines is unlimited. + /// + public int MaxLines { get; set; } = -1; + /// /// Gets or sets the word breaking mode to use when wrapping text. /// public WordBreaking WordBreaking { get; set; } + /// + /// Gets or sets the ellipsis behavior to use when laid-out text is limited to a maximum number of lines. + /// + public TextEllipsis TextEllipsis { get; set; } + + /// + /// Gets or sets the ellipsis marker to use when is Custom. + /// + public CodePoint? CustomEllipsis { get; set; } + + /// + /// Gets or sets the hyphenation marker behavior to use when text breaks at hyphenation opportunities. + /// + public TextHyphenation TextHyphenation { get; set; } + + /// + /// Gets or sets the hyphenation marker to use when is Custom. + /// + public CodePoint? CustomHyphen { get; set; } + /// /// Gets or sets the text direction. /// public TextDirection TextDirection { get; set; } = TextDirection.Auto; + /// + /// Gets or sets how bidirectional text is resolved. + /// + public TextBidiMode TextBidiMode { get; set; } + + /// + /// Gets or sets how caret movement and selection model trailing breaking whitespace. + /// + public TextInteractionMode TextInteractionMode { get; set; } + /// /// Gets or sets the text alignment of the text within the box. /// diff --git a/src/SixLabors.Fonts/TextPlaceholder.cs b/src/SixLabors.Fonts/TextPlaceholder.cs new file mode 100644 index 00000000..7da70f40 --- /dev/null +++ b/src/SixLabors.Fonts/TextPlaceholder.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Represents an atomic inline placeholder with caller-supplied dimensions. +/// +public readonly struct TextPlaceholder +{ + /// + /// Initializes a new instance of the struct. + /// + /// The placeholder width in pixel units. + /// The placeholder height in pixel units. + /// The placeholder alignment against surrounding text. + /// The distance from the placeholder top edge to its baseline in pixel units. + public TextPlaceholder( + float width, + float height, + TextPlaceholderAlignment alignment, + float baselineOffset) + { + this.Width = width; + this.Height = height; + this.Alignment = alignment; + this.BaselineOffset = baselineOffset; + } + + /// + /// Gets the placeholder width in pixel units. + /// + public float Width { get; } + + /// + /// Gets the placeholder height in pixel units. + /// + public float Height { get; } + + /// + /// Gets the placeholder alignment against surrounding text. + /// + public TextPlaceholderAlignment Alignment { get; } + + /// + /// Gets the distance from the placeholder top edge to its baseline in pixel units. + /// + public float BaselineOffset { get; } +} diff --git a/src/SixLabors.Fonts/TextPlaceholderAlignment.cs b/src/SixLabors.Fonts/TextPlaceholderAlignment.cs new file mode 100644 index 00000000..f8f0f2ef --- /dev/null +++ b/src/SixLabors.Fonts/TextPlaceholderAlignment.cs @@ -0,0 +1,40 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Specifies how an inline placeholder is aligned against surrounding text. +/// +public enum TextPlaceholderAlignment +{ + /// + /// Align the placeholder baseline with the surrounding text baseline. + /// + Baseline, + + /// + /// Align the placeholder above the surrounding text baseline. + /// + AboveBaseline, + + /// + /// Align the placeholder below the surrounding text baseline. + /// + BelowBaseline, + + /// + /// Align the placeholder with the top of the surrounding line box. + /// + Top, + + /// + /// Align the placeholder with the bottom of the surrounding line box. + /// + Bottom, + + /// + /// Align the placeholder with the middle of the surrounding line box. + /// + Middle +} diff --git a/src/SixLabors.Fonts/TextRun.cs b/src/SixLabors.Fonts/TextRun.cs index 2ae8e1b6..963a30db 100644 --- a/src/SixLabors.Fonts/TextRun.cs +++ b/src/SixLabors.Fonts/TextRun.cs @@ -36,6 +36,14 @@ public class TextRun /// public TextDecorations TextDecorations { get; set; } + /// + /// Gets or sets the inline placeholder represented by this run. + /// + /// + /// Placeholder runs are inserted at and must have equal to . + /// + public TextPlaceholder? Placeholder { get; set; } + /// /// Returns the slice of the given text representing this . /// diff --git a/src/SixLabors.Fonts/Unicode/CodePoint.cs b/src/SixLabors.Fonts/Unicode/CodePoint.cs index 1fa9db97..ed37a434 100644 --- a/src/SixLabors.Fonts/Unicode/CodePoint.cs +++ b/src/SixLabors.Fonts/Unicode/CodePoint.cs @@ -181,6 +181,11 @@ public int Utf8SequenceLength /// public static CodePoint ReplacementChar { get; } = new CodePoint(0xFFFD); + /// + /// Gets a instance that represents the Unicode object replacement character U+FFFC. + /// + public static CodePoint ObjectReplacementChar { get; } = new CodePoint(0xFFFC); + #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member // Operators below are explicit because they may throw. @@ -558,6 +563,14 @@ public static bool TryGetVerticalMirror(in CodePoint codePoint, out CodePoint mi public static LineBreakClass GetLineBreakClass(in CodePoint codePoint) => UnicodeData.GetLineBreakClass(codePoint.value); + /// + /// Gets the for the given codepoint. + /// + /// The codepoint. + /// The . + public static WordBreakClass GetWordBreakClass(in CodePoint codePoint) + => UnicodeData.GetWordBreakClass(codePoint.value); + /// /// Gets the for the given codepoint. /// diff --git a/src/SixLabors.Fonts/Unicode/LineBreak.cs b/src/SixLabors.Fonts/Unicode/LineBreak.cs index f0151340..c72c7a22 100644 --- a/src/SixLabors.Fonts/Unicode/LineBreak.cs +++ b/src/SixLabors.Fonts/Unicode/LineBreak.cs @@ -17,11 +17,17 @@ internal readonly struct LineBreak /// The code point index to measure to /// The code point index to actually break the line at /// True if this is a required line break; otherwise false - public LineBreak(int positionMeasure, int positionWrap, bool required = false) + /// True if this is a manual hyphenation break; otherwise false + public LineBreak( + int positionMeasure, + int positionWrap, + bool required = false, + bool isHyphenationBreak = false) { this.PositionMeasure = positionMeasure; this.PositionWrap = positionWrap; this.Required = required; + this.IsHyphenationBreak = isHyphenationBreak; } /// @@ -40,4 +46,9 @@ public LineBreak(int positionMeasure, int positionWrap, bool required = false) /// Gets a value indicating whether there should be a forced line break here. /// public bool Required { get; } + + /// + /// Gets a value indicating whether this is a manual hyphenation break. + /// + public bool IsHyphenationBreak { get; } } diff --git a/src/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs b/src/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs index 86fc801e..2d93d4b4 100644 --- a/src/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs +++ b/src/SixLabors.Fonts/Unicode/LineBreakEnumerator.cs @@ -2,6 +2,7 @@ // Licensed under the Six Labors Split License. using System.Globalization; +using System.Runtime.CompilerServices; namespace SixLabors.Fonts.Unicode; @@ -51,6 +52,11 @@ internal ref struct LineBreakEnumerator /// private const int HyphenMinus = 0x002D; + /// + /// U+00AD SOFT HYPHEN. It creates a manual hyphenation opportunity but is not rendered unless that break is chosen. + /// + private const int SoftHyphen = 0x00AD; + /// /// U+002B PLUS SIGN. Valid inside URI schemes. /// @@ -253,7 +259,8 @@ public bool MoveNext() this.Current = new LineBreak( this.FindPriorNonWhitespace(this.current), this.current.Length, - action == BreakAction.MustBreak); + action == BreakAction.MustBreak, + this.current.HasValue(SoftHyphen)); this.previousBreakPosition = this.current.Length; return true; @@ -2059,6 +2066,7 @@ private LineBreakCodePoint(int sentinel, int length, int charEnd) /// The code point boundary associated with the sentinel. /// The UTF-16 boundary associated with the sentinel. /// The sentinel item. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static LineBreakCodePoint CreateSentinel(int sentinel, int length, int charEnd) => new(sentinel, length, charEnd); /// @@ -2066,6 +2074,7 @@ private LineBreakCodePoint(int sentinel, int length, int charEnd) /// /// The class to compare. /// when this is a real item with the requested class. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool Is(LineBreakClass cls) => !this.IsSentinel && this.Class == cls; /// @@ -2073,6 +2082,7 @@ private LineBreakCodePoint(int sentinel, int length, int charEnd) /// /// The scalar value to compare. /// when this is a real item with the requested value. + [MethodImpl(MethodImplOptions.AggressiveInlining)] public readonly bool HasValue(int value) => !this.IsSentinel && this.CodePoint.Value == value; } } diff --git a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs index a994dfed..b1554a24 100644 --- a/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs +++ b/src/SixLabors.Fonts/Unicode/MemoryExtensions.cs @@ -103,6 +103,28 @@ public static SpanGraphemeEnumerator EnumerateGraphemes(this Span span) public static SpanGraphemeEnumerator EnumerateGraphemes(this Span span, TerminalWidthOptions terminalWidthOptions) => new(span, terminalWidthOptions); + /// + /// Returns an enumeration of Unicode word-boundary segments from the provided span. + /// + /// The read-only span of char elements representing the text to enumerate. + /// + /// Invalid UTF-16 sequences are treated as while determining word boundaries. + /// + /// The . + public static SpanWordEnumerator EnumerateWordSegments(this ReadOnlySpan span) + => new(span); + + /// + /// Returns an enumeration of Unicode word-boundary segments from the provided span. + /// + /// The span of char elements representing the text to enumerate. + /// + /// Invalid UTF-16 sequences are treated as while determining word boundaries. + /// + /// The . + public static SpanWordEnumerator EnumerateWordSegments(this Span span) + => new(span); + /// /// Returns the terminal cell width of the provided text. /// diff --git a/src/SixLabors.Fonts/Unicode/Resources/WordBreakTrie.Generated.cs b/src/SixLabors.Fonts/Unicode/Resources/WordBreakTrie.Generated.cs new file mode 100644 index 00000000..4f5fb2a2 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/Resources/WordBreakTrie.Generated.cs @@ -0,0 +1,603 @@ +// Copyright (c) Six Labors. +// Licensed under the Apache License, Version 2.0. + +// +using System; + +namespace SixLabors.Fonts.Unicode.Resources +{ + internal static class WordBreakTrie + { + public static ReadOnlySpan Data => new byte[] + { + 0, 8, 14, 0, 0, 0, 0, 0, 80, 229, 0, 0, 170, 3, 0, 0, 178, 3, 0, 0, 186, 3, 0, 0, 194, 3, 0, 0, 233, 3, 0, 0, 241, 3, 0, 0, 249, 3, 0, 0, 1, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, + 17, 4, 0, 0, 25, 4, 0, 0, 33, 4, 0, 0, 41, 4, 0, 0, 37, 4, 0, 0, 45, 4, 0, 0, 53, 4, 0, 0, 61, 4, 0, 0, 62, 4, 0, 0, 70, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 78, 4, 0, 0, 86, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 82, 4, 0, 0, 90, 4, 0, 0, 95, 4, 0, 0, 103, 4, 0, 0, 109, 4, 0, 0, 117, 4, 0, 0, 125, 4, 0, 0, + 133, 4, 0, 0, 141, 4, 0, 0, 149, 4, 0, 0, 155, 4, 0, 0, 163, 4, 0, 0, 7, 4, 0, 0, 15, 4, 0, 0, 168, 4, 0, 0, 176, 4, 0, 0, 184, 4, 0, 0, 192, 4, 0, 0, 198, 4, 0, 0, 206, 4, 0, 0, 205, 4, 0, 0, 213, 4, 0, 0, 221, 4, 0, 0, 229, 4, 0, 0, 13, 5, 0, 0, 20, 5, 0, 0, 28, 5, 0, 0, 53, 11, 0, 0, 36, 5, 0, 0, 7, 4, 0, 0, 44, 5, 0, 0, 52, 5, 0, 0, + 59, 5, 0, 0, 61, 5, 0, 0, 69, 5, 0, 0, 77, 5, 0, 0, 85, 5, 0, 0, 91, 5, 0, 0, 99, 5, 0, 0, 107, 5, 0, 0, 115, 5, 0, 0, 121, 5, 0, 0, 129, 5, 0, 0, 137, 5, 0, 0, 145, 5, 0, 0, 151, 5, 0, 0, 159, 5, 0, 0, 167, 5, 0, 0, 175, 5, 0, 0, 151, 5, 0, 0, 183, 5, 0, 0, 191, 5, 0, 0, 199, 5, 0, 0, 207, 5, 0, 0, 215, 5, 0, 0, 45, 14, 0, 0, 223, 5, 0, 0, + 229, 5, 0, 0, 237, 5, 0, 0, 245, 5, 0, 0, 253, 5, 0, 0, 3, 6, 0, 0, 11, 6, 0, 0, 19, 6, 0, 0, 27, 6, 0, 0, 32, 6, 0, 0, 40, 6, 0, 0, 48, 6, 0, 0, 56, 6, 0, 0, 57, 11, 0, 0, 63, 6, 0, 0, 71, 6, 0, 0, 218, 3, 0, 0, 76, 6, 0, 0, 83, 6, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 90, 6, 0, 0, 98, 6, 0, 0, 218, 3, 0, 0, 106, 6, 0, 0, 114, 6, 0, 0, + 92, 4, 0, 0, 122, 6, 0, 0, 129, 6, 0, 0, 136, 6, 0, 0, 144, 6, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 150, 6, 0, 0, 158, 6, 0, 0, 166, 6, 0, 0, 174, 6, 0, 0, 7, 4, 0, 0, 65, 11, 0, 0, 0, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 71, 11, 0, 0, + 7, 4, 0, 0, 79, 11, 0, 0, 69, 11, 0, 0, 87, 11, 0, 0, 7, 4, 0, 0, 83, 11, 0, 0, 7, 4, 0, 0, 182, 6, 0, 0, 218, 3, 0, 0, 93, 11, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 101, 11, 0, 0, 201, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 109, 11, 0, 0, 117, 11, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 125, 11, 0, 0, 190, 6, 0, 0, 198, 6, 0, 0, 206, 6, 0, 0, 214, 6, 0, 0, 218, 3, 0, 0, 219, 6, 0, 0, 224, 6, 0, 0, 249, 7, 0, 0, 232, 6, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 133, 11, 0, 0, 240, 6, 0, 0, + 246, 6, 0, 0, 7, 4, 0, 0, 141, 11, 0, 0, 255, 3, 0, 0, 254, 6, 0, 0, 45, 14, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 49, 14, 0, 0, 218, 3, 0, 0, 6, 7, 0, 0, 218, 3, 0, 0, 13, 7, 0, 0, 21, 7, 0, 0, 57, 14, 0, 0, 220, 6, 0, 0, 29, 7, 0, 0, 37, 7, 0, 0, 45, 7, 0, 0, 28, 4, 0, 0, 53, 7, 0, 0, 60, 7, 0, 0, 68, 7, 0, 0, 76, 7, 0, 0, + 7, 4, 0, 0, 83, 7, 0, 0, 7, 4, 0, 0, 91, 7, 0, 0, 149, 11, 0, 0, 156, 11, 0, 0, 53, 11, 0, 0, 164, 11, 0, 0, 97, 7, 0, 0, 103, 7, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 101, 11, 0, 0, 7, 4, 0, 0, 172, 11, 0, 0, 156, 11, 0, 0, 7, 4, 0, 0, 180, 11, 0, 0, 188, 11, 0, 0, 196, 11, 0, 0, 111, 7, 0, 0, 5, 5, 0, 0, 37, 14, 0, 0, 204, 10, 0, 0, 137, 10, 0, 0, 218, 3, 0, 0, 220, 6, 0, 0, 119, 7, 0, 0, 204, 11, 0, 0, 212, 11, 0, 0, 220, 11, 0, 0, 7, 4, 0, 0, 228, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 231, 11, 0, 0, 7, 4, 0, 0, 237, 11, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 127, 7, 0, 0, 7, 4, 0, 0, 65, 11, 0, 0, 7, 4, 0, 0, 135, 7, 0, 0, 245, 11, 0, 0, 253, 11, 0, 0, 253, 11, 0, 0, 33, 4, 0, 0, 218, 3, 0, 0, 5, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 13, 12, 0, 0, 143, 7, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 151, 7, 0, 0, 251, 10, 0, 0, 251, 10, 0, 0, 253, 10, 0, 0, 20, 12, 0, 0, 90, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 25, 12, 0, 0, + 7, 4, 0, 0, 218, 3, 0, 0, 247, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 247, 10, 0, 0, 252, 10, 0, 0, 251, 10, 0, 0, 251, 10, 0, 0, 4, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 33, 12, 0, 0, 218, 3, 0, 0, 24, 5, 0, 0, 156, 11, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 41, 12, 0, 0, 49, 12, 0, 0, 7, 4, 0, 0, 159, 7, 0, 0, 207, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 167, 7, 0, 0, 26, 5, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 57, 12, 0, 0, 17, 12, 0, 0, 175, 7, 0, 0, 183, 7, 0, 0, 7, 4, 0, 0, + 65, 12, 0, 0, 80, 4, 0, 0, 28, 4, 0, 0, 191, 7, 0, 0, 199, 7, 0, 0, 221, 4, 0, 0, 207, 7, 0, 0, 214, 7, 0, 0, 57, 12, 0, 0, 59, 5, 0, 0, 153, 4, 0, 0, 222, 7, 0, 0, 229, 7, 0, 0, 7, 4, 0, 0, 237, 7, 0, 0, 245, 7, 0, 0, 252, 7, 0, 0, 218, 3, 0, 0, 4, 8, 0, 0, 12, 8, 0, 0, 20, 8, 0, 0, 73, 12, 0, 0, 81, 12, 0, 0, 7, 4, 0, 0, 87, 12, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 28, 8, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 94, 12, 0, 0, 101, 12, 0, 0, 16, 4, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 237, 4, 0, 0, 245, 4, 0, 0, 253, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 105, 12, 0, 0, 110, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 156, 11, 0, 0, 24, 5, 0, 0, 7, 4, 0, 0, 115, 12, 0, 0, 7, 4, 0, 0, 121, 12, 0, 0, 125, 12, 0, 0, 36, 8, 0, 0, 44, 8, 0, 0, 22, 14, 0, 0, 132, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 212, 10, 0, 0, 29, 14, 0, 0, 186, 3, 0, 0, 194, 3, 0, 0, 11, 11, 0, 0, 52, 8, 0, 0, 255, 3, 0, 0, 140, 12, 0, 0, + 220, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 40, 15, 0, 0, 40, 15, 0, 0, 164, 15, 0, 0, 228, 15, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 28, 16, 0, 0, 68, 16, 0, 0, 132, 16, 0, 0, 148, 16, 0, 0, 212, 16, 0, 0, 248, 16, 0, 0, 28, 16, 0, 0, + 28, 16, 0, 0, 56, 17, 0, 0, 28, 16, 0, 0, 72, 17, 0, 0, 124, 17, 0, 0, 180, 17, 0, 0, 244, 17, 0, 0, 52, 18, 0, 0, 108, 18, 0, 0, 28, 16, 0, 0, 160, 18, 0, 0, 224, 18, 0, 0, 24, 19, 0, 0, 52, 19, 0, 0, 116, 19, 0, 0, 225, 9, 0, 0, 33, 10, 0, 0, 97, 10, 0, 0, 161, 10, 0, 0, 201, 13, 0, 0, 244, 13, 0, 0, 225, 10, 0, 0, 96, 5, 0, 0, 52, 14, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 33, 11, 0, 0, 97, 11, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 73, 13, 0, 0, 137, 13, 0, 0, 161, 11, 0, 0, 155, 1, 0, 0, 199, 11, 0, 0, 2, 12, 0, 0, 66, 12, 0, 0, 130, 12, 0, 0, 194, 12, 0, 0, 249, 12, 0, 0, 103, 14, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, + 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 155, 1, 0, 0, 57, 13, 0, 0, 91, 4, 0, 0, 148, 12, 0, 0, 156, 12, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 22, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 164, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 60, 8, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 57, 12, 0, 0, 7, 4, 0, 0, 165, 12, 0, 0, 68, 8, 0, 0, 7, 4, 0, 0, 18, 12, 0, 0, 53, 11, 0, 0, 76, 8, 0, 0, 156, 11, 0, 0, 7, 4, 0, 0, 173, 12, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 156, 11, 0, 0, 113, 9, 0, 0, 181, 12, 0, 0, 16, 4, 0, 0, 7, 4, 0, 0, 187, 12, 0, 0, 7, 4, 0, 0, + 194, 12, 0, 0, 198, 12, 0, 0, 203, 12, 0, 0, 7, 4, 0, 0, 65, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 245, 11, 0, 0, 141, 11, 0, 0, 121, 12, 0, 0, 60, 4, 0, 0, 211, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 219, 12, 0, 0, 222, 12, 0, 0, 141, 11, 0, 0, 245, 11, 0, 0, + 255, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 230, 12, 0, 0, 141, 11, 0, 0, 238, 12, 0, 0, 238, 12, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 17, 4, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 84, 8, 0, 0, 91, 8, 0, 0, 218, 3, 0, 0, 57, 12, 0, 0, 57, 12, 0, 0, 218, 3, 0, 0, 92, 4, 0, 0, 99, 8, 0, 0, 7, 4, 0, 0, 141, 11, 0, 0, 141, 11, 0, 0, 24, 12, 0, 0, 105, 12, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 228, 11, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 24, 12, 0, 0, 7, 4, 0, 0, 24, 12, 0, 0, 7, 4, 0, 0, 107, 8, 0, 0, 221, 4, 0, 0, 115, 8, 0, 0, 243, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 121, 8, 0, 0, + 251, 12, 0, 0, 126, 8, 0, 0, 57, 12, 0, 0, 2, 13, 0, 0, 134, 8, 0, 0, 24, 5, 0, 0, 142, 8, 0, 0, 24, 5, 0, 0, 9, 13, 0, 0, 245, 11, 0, 0, 68, 7, 0, 0, 27, 4, 0, 0, 150, 8, 0, 0, 157, 8, 0, 0, 68, 7, 0, 0, 165, 8, 0, 0, 173, 8, 0, 0, 17, 13, 0, 0, 68, 7, 0, 0, 180, 8, 0, 0, 188, 8, 0, 0, 192, 8, 0, 0, 68, 7, 0, 0, 153, 4, 0, 0, 200, 8, 0, 0, + 218, 3, 0, 0, 57, 4, 0, 0, 208, 8, 0, 0, 216, 8, 0, 0, 218, 3, 0, 0, 25, 13, 0, 0, 129, 11, 0, 0, 150, 4, 0, 0, 224, 8, 0, 0, 232, 8, 0, 0, 238, 8, 0, 0, 246, 8, 0, 0, 254, 8, 0, 0, 30, 13, 0, 0, 6, 9, 0, 0, 14, 9, 0, 0, 22, 9, 0, 0, 7, 4, 0, 0, 71, 7, 0, 0, 30, 9, 0, 0, 38, 13, 0, 0, 7, 4, 0, 0, 29, 4, 0, 0, 38, 9, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 46, 9, 0, 0, 54, 9, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 29, 4, 0, 0, 62, 9, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 70, 9, 0, 0, 61, 14, 0, 0, 68, 14, 0, 0, 77, 9, 0, 0, 85, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, + 93, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 46, 13, 0, 0, 54, 13, 0, 0, 101, 9, 0, 0, 109, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 60, 13, 0, 0, 117, 9, 0, 0, 125, 9, 0, 0, 133, 9, 0, 0, 137, 9, 0, 0, 145, 9, 0, 0, 7, 4, 0, 0, 152, 9, 0, 0, 24, 5, 0, 0, 7, 4, 0, 0, 133, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 160, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 68, 13, 0, 0, 224, 9, 0, 0, 168, 9, 0, 0, 68, 13, 0, 0, 75, 13, 0, 0, 176, 9, 0, 0, 182, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 82, 13, 0, 0, 190, 9, 0, 0, 198, 9, 0, 0, 89, 13, 0, 0, 206, 9, 0, 0, 113, 9, 0, 0, 16, 4, 0, 0, 249, 7, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 214, 9, 0, 0, 222, 9, 0, 0, 227, 9, 0, 0, 235, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 97, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 228, 10, 0, 0, 243, 9, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 207, 4, 0, 0, 251, 9, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 133, 11, 0, 0, 255, 3, 0, 0, 113, 9, 0, 0, 7, 4, 0, 0, 255, 3, 0, 0, 113, 9, 0, 0, 3, 10, 0, 0, 7, 4, 0, 0, 11, 10, 0, 0, 121, 13, 0, 0, 129, 13, 0, 0, 93, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 137, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 218, 3, 0, 0, 145, 13, 0, 0, 65, 12, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 19, 10, 0, 0, + 33, 4, 0, 0, 25, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 33, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 152, 13, 0, 0, 41, 10, 0, 0, 235, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 42, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 33, 4, 0, 0, + 49, 10, 0, 0, 150, 8, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 57, 10, 0, 0, 65, 10, 0, 0, 71, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 79, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 160, 13, 0, 0, 7, 4, 0, 0, 166, 13, 0, 0, 174, 13, 0, 0, 182, 13, 0, 0, 7, 4, 0, 0, 189, 13, 0, 0, 184, 13, 0, 0, 197, 13, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 162, 11, 0, 0, 201, 13, 0, 0, 0, 4, 0, 0, 160, 13, 0, 0, 160, 13, 0, 0, 251, 3, 0, 0, 251, 3, 0, 0, 224, 9, 0, 0, 224, 9, 0, 0, 207, 13, 0, 0, 76, 14, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 33, 4, 0, 0, 87, 10, 0, 0, 33, 4, 0, 0, 94, 10, 0, 0, 101, 10, 0, 0, 109, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 255, 3, 0, 0, 215, 13, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 117, 10, 0, 0, 125, 10, 0, 0, 7, 4, 0, 0, 106, 12, 0, 0, 133, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 141, 10, 0, 0, 223, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 24, 5, 0, 0, 149, 10, 0, 0, 7, 4, 0, 0, 157, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 24, 5, 0, 0, 157, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 24, 5, 0, 0, 165, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 255, 3, 0, 0, 173, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 231, 13, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 181, 10, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 189, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 182, 13, 0, 0, 239, 13, 0, 0, 247, 13, 0, 0, 255, 13, 0, 0, 7, 14, 0, 0, 15, 14, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 24, 5, 0, 0, 87, 12, 0, 0, 87, 12, 0, 0, 237, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 196, 10, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 146, 6, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 243, 10, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, + 33, 4, 0, 0, 33, 4, 0, 0, 33, 4, 0, 0, 93, 7, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 19, 11, 0, 0, 27, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 35, 11, 0, 0, 38, 11, 0, 0, 45, 11, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 238, 12, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 25, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 105, 13, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 24, 5, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 165, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 22, 12, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, + 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 7, 4, 0, 0, 113, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 42, 13, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, + 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 218, 3, 0, 0, 169, 3, 1, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 11, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 10, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, + 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 16, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 2, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 13, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 14, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 9, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 14, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 15, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 8, 0, 0, 0, 3, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 255, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, 8, 0, 0, 0, + 8, 0, 0, 0, 8, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 13, 0, 0, 0, 2, 0, 0, 0, 2, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 17, 0, 0, 0, + 17, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 4, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, + 3, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 3, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, + 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 5, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, + 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 6, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, + 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, + 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 7, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 17, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, 9, 0, 0, 0, + 9, 0, 0, 0, 9, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 16, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 13, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 12, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 13, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 14, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 16, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 17, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, 15, 0, 0, 0, + 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, 255, 0, 0, 0, + }; + } +} diff --git a/src/SixLabors.Fonts/Unicode/SpanWordEnumerator.cs b/src/SixLabors.Fonts/Unicode/SpanWordEnumerator.cs new file mode 100644 index 00000000..7f6dc50d --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/SpanWordEnumerator.cs @@ -0,0 +1,373 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode; + +/// +/// An enumerator for retrieving word-boundary segments from a . +///
+/// Implements the Unicode Word Boundary Algorithm. UAX #29 +/// +///
+/// Methods are pattern-matched by compiler to allow using foreach pattern. +///
+public ref struct SpanWordEnumerator +{ + private ReadOnlySpan source; + private int sourceOffset; + private int codePointOffset; + + /// + /// Initializes a new instance of the struct. + /// + /// The buffer to read from. + public SpanWordEnumerator(ReadOnlySpan source) + { + this.source = source; + this.sourceOffset = 0; + this.codePointOffset = 0; + this.Current = default; + } + + /// + /// Gets the element in the collection at the current position of the enumerator. + /// + public WordSegment Current { get; private set; } + + /// + /// Returns an enumerator that iterates through the collection. + /// + /// An enumerator that iterates through the collection. + public readonly SpanWordEnumerator GetEnumerator() => this; + + /// + /// Advances the enumerator to the next element of the collection. + /// + /// + /// if the enumerator was successfully advanced to the next element; + /// if the enumerator has passed the end of the collection. + /// + public bool MoveNext() + { + if (this.sourceOffset >= this.source.Length) + { + return false; + } + + int segmentStart = this.sourceOffset; + int segmentCodePointStart = this.codePointOffset; + WordBreakCodePoint current = this.ReadForward(this.sourceOffset); + int currentEnd = current.Utf16End; + int boundaryCodePoint = this.codePointOffset + 1; + + while (currentEnd < this.source.Length) + { + WordBreakCodePoint next = this.ReadForward(currentEnd); + if (this.IsBoundary(current, next)) + { + break; + } + + current = next; + currentEnd = current.Utf16End; + boundaryCodePoint++; + } + + this.Current = new WordSegment( + this.source[segmentStart..currentEnd], + segmentStart, + segmentCodePointStart, + boundaryCodePoint - segmentCodePointStart); + + this.sourceOffset = currentEnd; + this.codePointOffset = boundaryCodePoint; + return true; + } + + private readonly bool IsBoundary(in WordBreakCodePoint current, in WordBreakCodePoint next) + { + // WB3, WB3a, WB3b, and WB3c are evaluated before the ignore rule, so they + // use the adjacent code points exactly as they appear in the source. + if (current.Is(WordBreakClass.CarriageReturn) && next.Is(WordBreakClass.LineFeed)) + { + return false; + } + + if (IsNewline(current) || IsNewline(next)) + { + return true; + } + + if (current.Is(WordBreakClass.ZeroWidthJoiner) + && CodePoint.GetGraphemeClusterClass(next.CodePoint) == GraphemeClusterClass.ExtendedPictographic) + { + return false; + } + + if (current.Is(WordBreakClass.WSegSpace) && next.Is(WordBreakClass.WSegSpace)) + { + return false; + } + + if (IsIgnored(next)) + { + return false; + } + + WordBreakCodePoint left = this.GetEffectivePrevious(current); + WordBreakClass right = next.Class; + + if (IsAHLetter(left.Class) && IsAHLetter(right)) + { + return false; + } + + if (IsAHLetter(left.Class) + && IsMidLetterMidNumLetQ(right) + && this.TryGetNextSignificant(next.Utf16End, out WordBreakCodePoint after) + && IsAHLetter(after.Class)) + { + return false; + } + + if (IsAHLetter(right) + && IsMidLetterMidNumLetQ(left.Class) + && this.TryGetPreviousSignificant(left.Utf16Start, out WordBreakCodePoint before) + && IsAHLetter(before.Class)) + { + return false; + } + + if (left.Is(WordBreakClass.HebrewLetter) && right == WordBreakClass.SingleQuote) + { + return false; + } + + if (left.Is(WordBreakClass.HebrewLetter) + && right == WordBreakClass.DoubleQuote + && this.TryGetNextSignificant(next.Utf16End, out after) + && after.Is(WordBreakClass.HebrewLetter)) + { + return false; + } + + if (right == WordBreakClass.HebrewLetter + && left.Is(WordBreakClass.DoubleQuote) + && this.TryGetPreviousSignificant(left.Utf16Start, out before) + && before.Is(WordBreakClass.HebrewLetter)) + { + return false; + } + + if (left.Is(WordBreakClass.Numeric) && right == WordBreakClass.Numeric) + { + return false; + } + + if (IsAHLetter(left.Class) && right == WordBreakClass.Numeric) + { + return false; + } + + if (left.Is(WordBreakClass.Numeric) && IsAHLetter(right)) + { + return false; + } + + if (right == WordBreakClass.Numeric + && IsMidNumMidNumLetQ(left.Class) + && this.TryGetPreviousSignificant(left.Utf16Start, out before) + && before.Is(WordBreakClass.Numeric)) + { + return false; + } + + if (left.Is(WordBreakClass.Numeric) + && IsMidNumMidNumLetQ(right) + && this.TryGetNextSignificant(next.Utf16End, out after) + && after.Is(WordBreakClass.Numeric)) + { + return false; + } + + if (left.Is(WordBreakClass.Katakana) && right == WordBreakClass.Katakana) + { + return false; + } + + if (IsAHLetterNumericKatakanaExtendNumLet(left.Class) && right == WordBreakClass.ExtendNumLet) + { + return false; + } + + if (left.Is(WordBreakClass.ExtendNumLet) && IsAHLetterNumericKatakana(right)) + { + return false; + } + + if (left.Is(WordBreakClass.RegionalIndicator) + && right == WordBreakClass.RegionalIndicator + && (this.CountRegionalIndicatorsBefore(next.Utf16Start) & 1) == 1) + { + return false; + } + + return true; + } + + private readonly WordBreakCodePoint GetEffectivePrevious(in WordBreakCodePoint current) + { + if (!IsIgnored(current)) + { + return current; + } + + int scanEnd = current.Utf16Start; + while (this.TryReadBackward(scanEnd, out WordBreakCodePoint previous)) + { + if (!IsIgnored(previous)) + { + // WB4 deliberately stops ignoring after sot and hard line breaks. + return IsNewline(previous) ? current : previous; + } + + scanEnd = previous.Utf16Start; + } + + return current; + } + + private readonly int CountRegionalIndicatorsBefore(int utf16End) + { + int count = 0; + int scanEnd = utf16End; + while (this.TryGetPreviousSignificant(scanEnd, out WordBreakCodePoint previous)) + { + if (!previous.Is(WordBreakClass.RegionalIndicator)) + { + break; + } + + count++; + scanEnd = previous.Utf16Start; + } + + return count; + } + + private readonly bool TryGetPreviousSignificant(int utf16End, out WordBreakCodePoint codePoint) + { + int scanEnd = utf16End; + while (this.TryReadBackward(scanEnd, out codePoint)) + { + if (!IsIgnored(codePoint)) + { + return true; + } + + scanEnd = codePoint.Utf16Start; + } + + codePoint = default; + return false; + } + + private readonly bool TryGetNextSignificant(int utf16Start, out WordBreakCodePoint codePoint) + { + int scanStart = utf16Start; + while (this.TryReadForward(scanStart, out codePoint)) + { + if (!IsIgnored(codePoint)) + { + return true; + } + + scanStart = codePoint.Utf16End; + } + + codePoint = default; + return false; + } + + private readonly WordBreakCodePoint ReadForward(int utf16Start) + { + CodePoint codePoint = CodePoint.DecodeFromUtf16At(this.source, utf16Start, out int charsConsumed); + return new WordBreakCodePoint(codePoint, CodePoint.GetWordBreakClass(codePoint), utf16Start, utf16Start + charsConsumed); + } + + private readonly bool TryReadForward(int utf16Start, out WordBreakCodePoint codePoint) + { + if (utf16Start >= this.source.Length) + { + codePoint = default; + return false; + } + + codePoint = this.ReadForward(utf16Start); + return true; + } + + private readonly bool TryReadBackward(int utf16End, out WordBreakCodePoint codePoint) + { + if (utf16End <= 0) + { + codePoint = default; + return false; + } + + int utf16Start = utf16End - 1; + if (utf16Start > 0 + && char.IsLowSurrogate(this.source[utf16Start]) + && char.IsHighSurrogate(this.source[utf16Start - 1])) + { + utf16Start--; + } + + codePoint = this.ReadForward(utf16Start); + return true; + } + + private static bool IsAHLetter(WordBreakClass cls) + => cls is WordBreakClass.ALetter or WordBreakClass.HebrewLetter; + + private static bool IsAHLetterNumericKatakana(WordBreakClass cls) + => IsAHLetter(cls) || cls is WordBreakClass.Numeric or WordBreakClass.Katakana; + + private static bool IsAHLetterNumericKatakanaExtendNumLet(WordBreakClass cls) + => IsAHLetterNumericKatakana(cls) || cls == WordBreakClass.ExtendNumLet; + + private static bool IsIgnored(in WordBreakCodePoint codePoint) => IsIgnored(codePoint.Class); + + private static bool IsIgnored(WordBreakClass cls) + => cls is WordBreakClass.Extend or WordBreakClass.Format or WordBreakClass.ZeroWidthJoiner; + + private static bool IsMidLetterMidNumLetQ(WordBreakClass cls) + => cls is WordBreakClass.MidLetter or WordBreakClass.MidNumLet or WordBreakClass.SingleQuote; + + private static bool IsMidNumMidNumLetQ(WordBreakClass cls) + => cls is WordBreakClass.MidNum or WordBreakClass.MidNumLet or WordBreakClass.SingleQuote; + + private static bool IsNewline(in WordBreakCodePoint codePoint) + => codePoint.Class is WordBreakClass.CarriageReturn or WordBreakClass.LineFeed or WordBreakClass.Newline; + + private readonly struct WordBreakCodePoint + { + public WordBreakCodePoint(CodePoint codePoint, WordBreakClass cls, int utf16Start, int utf16End) + { + this.CodePoint = codePoint; + this.Class = cls; + this.Utf16Start = utf16Start; + this.Utf16End = utf16End; + } + + public CodePoint CodePoint { get; } + + public WordBreakClass Class { get; } + + public int Utf16Start { get; } + + public int Utf16End { get; } + + public bool Is(WordBreakClass cls) => this.Class == cls; + } +} diff --git a/src/SixLabors.Fonts/Unicode/UnicodeData.cs b/src/SixLabors.Fonts/Unicode/UnicodeData.cs index 8c1b0d06..3d42765b 100644 --- a/src/SixLabors.Fonts/Unicode/UnicodeData.cs +++ b/src/SixLabors.Fonts/Unicode/UnicodeData.cs @@ -15,6 +15,7 @@ internal static class UnicodeData private static readonly Lazy LazyEmojiTrie = new(() => GetEmojiTrie(), true); private static readonly Lazy LazyGraphemeTrie = new(() => GetGraphemeTrie(), true); private static readonly Lazy LazyLineBreakTrie = new(() => GetLineBreakTrie(), true); + private static readonly Lazy LazyWordBreakTrie = new(() => GetWordBreakTrie(), true); private static readonly Lazy LazyScriptTrie = new(() => GetScriptTrie(), true); private static readonly Lazy LazyCategoryTrie = new(() => GetCategoryTrie(), true); private static readonly Lazy LazyArabicShapingTrie = new(() => GetArabicShapingTrie(), true); @@ -43,6 +44,9 @@ internal static class UnicodeData [MethodImpl(MethodImplOptions.AggressiveInlining)] public static LineBreakClass GetLineBreakClass(uint codePoint) => (LineBreakClass)LazyLineBreakTrie.Value.Get(codePoint); + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static WordBreakClass GetWordBreakClass(uint codePoint) => (WordBreakClass)LazyWordBreakTrie.Value.Get(codePoint); + [MethodImpl(MethodImplOptions.AggressiveInlining)] public static ScriptClass GetScriptClass(uint codePoint) => (ScriptClass)LazyScriptTrie.Value.Get(codePoint); @@ -82,6 +86,8 @@ internal static class UnicodeData private static UnicodeTrie GetLineBreakTrie() => new(LineBreakTrie.Data); + private static UnicodeTrie GetWordBreakTrie() => new(WordBreakTrie.Data); + private static UnicodeTrie GetScriptTrie() => new(ScriptTrie.Data); private static UnicodeTrie GetCategoryTrie() => new(UnicodeCategoryTrie.Data); diff --git a/src/SixLabors.Fonts/Unicode/WordBreakClass.cs b/src/SixLabors.Fonts/Unicode/WordBreakClass.cs new file mode 100644 index 00000000..8b7315b3 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/WordBreakClass.cs @@ -0,0 +1,106 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode; + +/// +/// Unicode Word_Break property values. +/// +/// +public enum WordBreakClass : uint +{ + /// + /// U+000D CARRIAGE RETURN (CR). + /// + CarriageReturn = 0, + + /// + /// U+000A LINE FEED (LF). + /// + LineFeed = 1, + + /// + /// Newline characters other than CR and LF. + /// + Newline = 2, + + /// + /// Extending code points that are ignored by most word boundary rules. + /// + Extend = 3, + + /// + /// U+200D ZERO WIDTH JOINER. + /// + ZeroWidthJoiner = 4, + + /// + /// Regional indicator symbols used to build flag emoji pairs. + /// + RegionalIndicator = 5, + + /// + /// Format characters that are ignored by most word boundary rules. + /// + Format = 6, + + /// + /// Katakana characters. + /// + Katakana = 7, + + /// + /// Hebrew letters. + /// + HebrewLetter = 8, + + /// + /// Alphabetic letters. + /// + ALetter = 9, + + /// + /// Single quote. + /// + SingleQuote = 10, + + /// + /// Double quote. + /// + DoubleQuote = 11, + + /// + /// Mid-letter and mid-number punctuation. + /// + MidNumLet = 12, + + /// + /// Mid-letter punctuation. + /// + MidLetter = 13, + + /// + /// Mid-number punctuation. + /// + MidNum = 14, + + /// + /// Numeric characters. + /// + Numeric = 15, + + /// + /// Connector characters that extend letters, numbers, and Katakana. + /// + ExtendNumLet = 16, + + /// + /// Horizontal whitespace segmented as word-segmentation space. + /// + WSegSpace = 17, + + /// + /// Other. + /// + Other = 0xFF +} diff --git a/src/SixLabors.Fonts/Unicode/WordSegment.cs b/src/SixLabors.Fonts/Unicode/WordSegment.cs new file mode 100644 index 00000000..af5c0ae5 --- /dev/null +++ b/src/SixLabors.Fonts/Unicode/WordSegment.cs @@ -0,0 +1,55 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts.Unicode; + +/// +/// Represents a segment between two Unicode word boundaries. +/// +public readonly ref struct WordSegment +{ + /// + /// Initializes a new instance of the struct. + /// + /// The UTF-16 span containing the word-boundary segment. + /// The UTF-16 offset of the segment in the original source. + /// The code point offset of the segment in the original source. + /// The number of Unicode scalar values in the segment. + public WordSegment( + ReadOnlySpan span, + int utf16Offset, + int codePointOffset, + int codePointCount) + { + this.Span = span; + this.Utf16Offset = utf16Offset; + this.Utf16Length = span.Length; + this.CodePointOffset = codePointOffset; + this.CodePointCount = codePointCount; + } + + /// + /// Gets the UTF-16 span containing the word-boundary segment. + /// + public ReadOnlySpan Span { get; } + + /// + /// Gets the UTF-16 offset of the segment in the original source. + /// + public int Utf16Offset { get; } + + /// + /// Gets the UTF-16 length of the segment. + /// + public int Utf16Length { get; } + + /// + /// Gets the code point offset of the segment in the original source. + /// + public int CodePointOffset { get; } + + /// + /// Gets the number of Unicode scalar values in the segment. + /// + public int CodePointCount { get; } +} diff --git a/src/SixLabors.Fonts/WordMetrics.cs b/src/SixLabors.Fonts/WordMetrics.cs new file mode 100644 index 00000000..cb9f66f6 --- /dev/null +++ b/src/SixLabors.Fonts/WordMetrics.cs @@ -0,0 +1,73 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Represents the positioned metrics for one Unicode word-boundary segment. +/// +public readonly struct WordMetrics +{ + /// + /// Initializes a new instance of the struct. + /// + /// The positioned logical advance rectangle for the word-boundary segment in pixel units. + /// The rendered glyph bounds for the word-boundary segment in pixel units. + /// The union of the positioned logical advance bounds and rendered glyph bounds in pixel units. + /// The inclusive grapheme insertion index where the word-boundary segment starts. + /// The exclusive grapheme insertion index where the word-boundary segment ends. + /// The inclusive UTF-16 index where the word-boundary segment starts. + /// The exclusive UTF-16 index where the word-boundary segment ends. + internal WordMetrics( + FontRectangle advance, + FontRectangle bounds, + FontRectangle renderableBounds, + int graphemeStart, + int graphemeEnd, + int stringStart, + int stringEnd) + { + this.Advance = advance; + this.Bounds = bounds; + this.RenderableBounds = renderableBounds; + this.GraphemeStart = graphemeStart; + this.GraphemeEnd = graphemeEnd; + this.StringStart = stringStart; + this.StringEnd = stringEnd; + } + + /// + /// Gets the positioned logical advance rectangle for the word-boundary segment in pixel units. + /// + public FontRectangle Advance { get; } + + /// + /// Gets the rendered glyph bounds for the word-boundary segment in pixel units. + /// + public FontRectangle Bounds { get; } + + /// + /// Gets the union of the positioned logical advance bounds and rendered glyph bounds in pixel units. + /// + public FontRectangle RenderableBounds { get; } + + /// + /// Gets the inclusive grapheme insertion index where the word-boundary segment starts. + /// + public int GraphemeStart { get; } + + /// + /// Gets the exclusive grapheme insertion index where the word-boundary segment ends. + /// + public int GraphemeEnd { get; } + + /// + /// Gets the inclusive UTF-16 index where the word-boundary segment starts. + /// + public int StringStart { get; } + + /// + /// Gets the exclusive UTF-16 index where the word-boundary segment ends. + /// + public int StringEnd { get; } +} diff --git a/src/SixLabors.Fonts/WordSegmentRun.cs b/src/SixLabors.Fonts/WordSegmentRun.cs new file mode 100644 index 00000000..63b8e5c3 --- /dev/null +++ b/src/SixLabors.Fonts/WordSegmentRun.cs @@ -0,0 +1,49 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +namespace SixLabors.Fonts; + +/// +/// Describes one source-order Unicode word-boundary segment run. +/// +internal readonly struct WordSegmentRun +{ + /// + /// Initializes a new instance of the struct. + /// + /// The inclusive grapheme insertion index where the word-boundary segment starts. + /// The exclusive grapheme insertion index where the word-boundary segment ends. + /// The inclusive UTF-16 index where the word-boundary segment starts. + /// The exclusive UTF-16 index where the word-boundary segment ends. + public WordSegmentRun( + int graphemeStart, + int graphemeEnd, + int stringStart, + int stringEnd) + { + this.GraphemeStart = graphemeStart; + this.GraphemeEnd = graphemeEnd; + this.StringStart = stringStart; + this.StringEnd = stringEnd; + } + + /// + /// Gets the inclusive grapheme insertion index where the word-boundary segment starts. + /// + public int GraphemeStart { get; } + + /// + /// Gets the exclusive grapheme insertion index where the word-boundary segment ends. + /// + public int GraphemeEnd { get; } + + /// + /// Gets the inclusive UTF-16 index where the word-boundary segment starts. + /// + public int StringStart { get; } + + /// + /// Gets the exclusive UTF-16 index where the word-boundary segment ends. + /// + public int StringEnd { get; } +} diff --git a/src/UnicodeTrieGenerator/Generator.cs b/src/UnicodeTrieGenerator/Generator.cs index ac48cc60..021fae15 100644 --- a/src/UnicodeTrieGenerator/Generator.cs +++ b/src/UnicodeTrieGenerator/Generator.cs @@ -147,6 +147,43 @@ private static readonly Dictionary LineBreakClassMap { "XX", LineBreakClass.Unknown } }; + private static readonly Dictionary WordBreakClassMap + = new(StringComparer.OrdinalIgnoreCase) + { + { "CR", WordBreakClass.CarriageReturn }, + { "LF", WordBreakClass.LineFeed }, + { "Newline", WordBreakClass.Newline }, + { "Extend", WordBreakClass.Extend }, + { "ZWJ", WordBreakClass.ZeroWidthJoiner }, + { "Regional_Indicator", WordBreakClass.RegionalIndicator }, + { "RI", WordBreakClass.RegionalIndicator }, + { "Format", WordBreakClass.Format }, + { "FO", WordBreakClass.Format }, + { "Katakana", WordBreakClass.Katakana }, + { "KA", WordBreakClass.Katakana }, + { "Hebrew_Letter", WordBreakClass.HebrewLetter }, + { "HL", WordBreakClass.HebrewLetter }, + { "ALetter", WordBreakClass.ALetter }, + { "LE", WordBreakClass.ALetter }, + { "Single_Quote", WordBreakClass.SingleQuote }, + { "SQ", WordBreakClass.SingleQuote }, + { "Double_Quote", WordBreakClass.DoubleQuote }, + { "DQ", WordBreakClass.DoubleQuote }, + { "MidNumLet", WordBreakClass.MidNumLet }, + { "MB", WordBreakClass.MidNumLet }, + { "MidLetter", WordBreakClass.MidLetter }, + { "ML", WordBreakClass.MidLetter }, + { "MidNum", WordBreakClass.MidNum }, + { "MN", WordBreakClass.MidNum }, + { "Numeric", WordBreakClass.Numeric }, + { "NU", WordBreakClass.Numeric }, + { "ExtendNumLet", WordBreakClass.ExtendNumLet }, + { "EX", WordBreakClass.ExtendNumLet }, + { "WSegSpace", WordBreakClass.WSegSpace }, + { "Other", WordBreakClass.Other }, + { "XX", WordBreakClass.Other } + }; + private static readonly Dictionary ScriptMap = new(StringComparer.OrdinalIgnoreCase) { @@ -541,6 +578,7 @@ public static void GenerateUnicodeTries() GenerateBidiBracketsTrie(); GenerateBidiMirrorTrie(); GenerateLineBreakTrie(); + GenerateWordBreakTrie(); GenerateEastAsianWidthTrie(); GenerateEmojiTrie(); UnicodeTrie ugc = GenerateUnicodeCategoryTrie(); @@ -781,6 +819,41 @@ private static void GenerateLineBreakTrie() GenerateTrieClass("LineBreak", trie); } + /// + /// Generates the UnicodeTrie for the Word_Break code point ranges. + /// + private static void GenerateWordBreakTrie() + { + Regex regex = new(@"^([0-9A-F]+)(?:\.\.([0-9A-F]+))?\s*;\s*(.*?)\s*#"); + UnicodeTrieBuilder builder = new((uint)WordBreakClass.Other); + + using (StreamReader sr = GetStreamReader("WordBreakProperty.txt")) + { + string? line; + while ((line = sr.ReadLine()) != null) + { + Match match = regex.Match(line); + + if (match.Success) + { + string start = match.Groups[1].Value; + string end = match.Groups[2].Value; + string point = match.Groups[3].Value; + + if (string.IsNullOrEmpty(end)) + { + end = start; + } + + builder.SetRange(ParseHexInt(start), ParseHexInt(end), (uint)WordBreakClassMap[point], true); + } + } + } + + UnicodeTrie trie = builder.Freeze(); + GenerateTrieClass("WordBreak", trie); + } + /// /// Generates the UnicodeTrie for East Asian Width classes used by UAX #14. /// diff --git a/src/UnicodeTrieGenerator/Rules/README.md b/src/UnicodeTrieGenerator/Rules/README.md index d71b0f5a..6451d1fe 100644 --- a/src/UnicodeTrieGenerator/Rules/README.md +++ b/src/UnicodeTrieGenerator/Rules/README.md @@ -1,6 +1,7 @@ Files sourced from: https://www.unicode.org/Public/17.0.0/ucd/ +https://www.unicode.org/Public/17.0.0/ucd/auxiliary/ https://www.unicode.org/Public/17.0.0/ucd/emoji/ https://www.unicode.org/Public/emoji/latest/ https://github.com/microsoft/font-tools diff --git a/src/UnicodeTrieGenerator/Rules/WordBreakProperty.txt b/src/UnicodeTrieGenerator/Rules/WordBreakProperty.txt new file mode 100644 index 00000000..20fa24e3 --- /dev/null +++ b/src/UnicodeTrieGenerator/Rules/WordBreakProperty.txt @@ -0,0 +1,1541 @@ +# WordBreakProperty-17.0.0.txt +# Date: 2025-06-30, 06:20:49 GMT +# © 2025 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use and license, see https://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see https://www.unicode.org/reports/tr44/ + +# ================================================ + +# Property: Word_Break + +# All code points not explicitly listed for Word_Break +# have the value Other (XX). + +# @missing: 0000..10FFFF; Other + +# ================================================ + +0022 ; Double_Quote # Po QUOTATION MARK + +# Total code points: 1 + +# ================================================ + +0027 ; Single_Quote # Po APOSTROPHE + +# Total code points: 1 + +# ================================================ + +05D0..05EA ; Hebrew_Letter # Lo [27] HEBREW LETTER ALEF..HEBREW LETTER TAV +05EF..05F2 ; Hebrew_Letter # Lo [4] HEBREW YOD TRIANGLE..HEBREW LIGATURE YIDDISH DOUBLE YOD +FB1D ; Hebrew_Letter # Lo HEBREW LETTER YOD WITH HIRIQ +FB1F..FB28 ; Hebrew_Letter # Lo [10] HEBREW LIGATURE YIDDISH YOD YOD PATAH..HEBREW LETTER WIDE TAV +FB2A..FB36 ; Hebrew_Letter # Lo [13] HEBREW LETTER SHIN WITH SHIN DOT..HEBREW LETTER ZAYIN WITH DAGESH +FB38..FB3C ; Hebrew_Letter # Lo [5] HEBREW LETTER TET WITH DAGESH..HEBREW LETTER LAMED WITH DAGESH +FB3E ; Hebrew_Letter # Lo HEBREW LETTER MEM WITH DAGESH +FB40..FB41 ; Hebrew_Letter # Lo [2] HEBREW LETTER NUN WITH DAGESH..HEBREW LETTER SAMEKH WITH DAGESH +FB43..FB44 ; Hebrew_Letter # Lo [2] HEBREW LETTER FINAL PE WITH DAGESH..HEBREW LETTER PE WITH DAGESH +FB46..FB4F ; Hebrew_Letter # Lo [10] HEBREW LETTER TSADI WITH DAGESH..HEBREW LIGATURE ALEF LAMED + +# Total code points: 75 + +# ================================================ + +000D ; CR # Cc + +# Total code points: 1 + +# ================================================ + +000A ; LF # Cc + +# Total code points: 1 + +# ================================================ + +000B..000C ; Newline # Cc [2] .. +0085 ; Newline # Cc +2028 ; Newline # Zl LINE SEPARATOR +2029 ; Newline # Zp PARAGRAPH SEPARATOR + +# Total code points: 5 + +# ================================================ + +0300..036F ; Extend # Mn [112] COMBINING GRAVE ACCENT..COMBINING LATIN SMALL LETTER X +0483..0487 ; Extend # Mn [5] COMBINING CYRILLIC TITLO..COMBINING CYRILLIC POKRYTIE +0488..0489 ; Extend # Me [2] COMBINING CYRILLIC HUNDRED THOUSANDS SIGN..COMBINING CYRILLIC MILLIONS SIGN +0591..05BD ; Extend # Mn [45] HEBREW ACCENT ETNAHTA..HEBREW POINT METEG +05BF ; Extend # Mn HEBREW POINT RAFE +05C1..05C2 ; Extend # Mn [2] HEBREW POINT SHIN DOT..HEBREW POINT SIN DOT +05C4..05C5 ; Extend # Mn [2] HEBREW MARK UPPER DOT..HEBREW MARK LOWER DOT +05C7 ; Extend # Mn HEBREW POINT QAMATS QATAN +0610..061A ; Extend # Mn [11] ARABIC SIGN SALLALLAHOU ALAYHE WASSALLAM..ARABIC SMALL KASRA +064B..065F ; Extend # Mn [21] ARABIC FATHATAN..ARABIC WAVY HAMZA BELOW +0670 ; Extend # Mn ARABIC LETTER SUPERSCRIPT ALEF +06D6..06DC ; Extend # Mn [7] ARABIC SMALL HIGH LIGATURE SAD WITH LAM WITH ALEF MAKSURA..ARABIC SMALL HIGH SEEN +06DF..06E4 ; Extend # Mn [6] ARABIC SMALL HIGH ROUNDED ZERO..ARABIC SMALL HIGH MADDA +06E7..06E8 ; Extend # Mn [2] ARABIC SMALL HIGH YEH..ARABIC SMALL HIGH NOON +06EA..06ED ; Extend # Mn [4] ARABIC EMPTY CENTRE LOW STOP..ARABIC SMALL LOW MEEM +0711 ; Extend # Mn SYRIAC LETTER SUPERSCRIPT ALAPH +0730..074A ; Extend # Mn [27] SYRIAC PTHAHA ABOVE..SYRIAC BARREKH +07A6..07B0 ; Extend # Mn [11] THAANA ABAFILI..THAANA SUKUN +07EB..07F3 ; Extend # Mn [9] NKO COMBINING SHORT HIGH TONE..NKO COMBINING DOUBLE DOT ABOVE +07FD ; Extend # Mn NKO DANTAYALAN +0816..0819 ; Extend # Mn [4] SAMARITAN MARK IN..SAMARITAN MARK DAGESH +081B..0823 ; Extend # Mn [9] SAMARITAN MARK EPENTHETIC YUT..SAMARITAN VOWEL SIGN A +0825..0827 ; Extend # Mn [3] SAMARITAN VOWEL SIGN SHORT A..SAMARITAN VOWEL SIGN U +0829..082D ; Extend # Mn [5] SAMARITAN VOWEL SIGN LONG I..SAMARITAN MARK NEQUDAA +0859..085B ; Extend # Mn [3] MANDAIC AFFRICATION MARK..MANDAIC GEMINATION MARK +0897..089F ; Extend # Mn [9] ARABIC PEPET..ARABIC HALF MADDA OVER MADDA +08CA..08E1 ; Extend # Mn [24] ARABIC SMALL HIGH FARSI YEH..ARABIC SMALL HIGH SIGN SAFHA +08E3..0902 ; Extend # Mn [32] ARABIC TURNED DAMMA BELOW..DEVANAGARI SIGN ANUSVARA +0903 ; Extend # Mc DEVANAGARI SIGN VISARGA +093A ; Extend # Mn DEVANAGARI VOWEL SIGN OE +093B ; Extend # Mc DEVANAGARI VOWEL SIGN OOE +093C ; Extend # Mn DEVANAGARI SIGN NUKTA +093E..0940 ; Extend # Mc [3] DEVANAGARI VOWEL SIGN AA..DEVANAGARI VOWEL SIGN II +0941..0948 ; Extend # Mn [8] DEVANAGARI VOWEL SIGN U..DEVANAGARI VOWEL SIGN AI +0949..094C ; Extend # Mc [4] DEVANAGARI VOWEL SIGN CANDRA O..DEVANAGARI VOWEL SIGN AU +094D ; Extend # Mn DEVANAGARI SIGN VIRAMA +094E..094F ; Extend # Mc [2] DEVANAGARI VOWEL SIGN PRISHTHAMATRA E..DEVANAGARI VOWEL SIGN AW +0951..0957 ; Extend # Mn [7] DEVANAGARI STRESS SIGN UDATTA..DEVANAGARI VOWEL SIGN UUE +0962..0963 ; Extend # Mn [2] DEVANAGARI VOWEL SIGN VOCALIC L..DEVANAGARI VOWEL SIGN VOCALIC LL +0981 ; Extend # Mn BENGALI SIGN CANDRABINDU +0982..0983 ; Extend # Mc [2] BENGALI SIGN ANUSVARA..BENGALI SIGN VISARGA +09BC ; Extend # Mn BENGALI SIGN NUKTA +09BE..09C0 ; Extend # Mc [3] BENGALI VOWEL SIGN AA..BENGALI VOWEL SIGN II +09C1..09C4 ; Extend # Mn [4] BENGALI VOWEL SIGN U..BENGALI VOWEL SIGN VOCALIC RR +09C7..09C8 ; Extend # Mc [2] BENGALI VOWEL SIGN E..BENGALI VOWEL SIGN AI +09CB..09CC ; Extend # Mc [2] BENGALI VOWEL SIGN O..BENGALI VOWEL SIGN AU +09CD ; Extend # Mn BENGALI SIGN VIRAMA +09D7 ; Extend # Mc BENGALI AU LENGTH MARK +09E2..09E3 ; Extend # Mn [2] BENGALI VOWEL SIGN VOCALIC L..BENGALI VOWEL SIGN VOCALIC LL +09FE ; Extend # Mn BENGALI SANDHI MARK +0A01..0A02 ; Extend # Mn [2] GURMUKHI SIGN ADAK BINDI..GURMUKHI SIGN BINDI +0A03 ; Extend # Mc GURMUKHI SIGN VISARGA +0A3C ; Extend # Mn GURMUKHI SIGN NUKTA +0A3E..0A40 ; Extend # Mc [3] GURMUKHI VOWEL SIGN AA..GURMUKHI VOWEL SIGN II +0A41..0A42 ; Extend # Mn [2] GURMUKHI VOWEL SIGN U..GURMUKHI VOWEL SIGN UU +0A47..0A48 ; Extend # Mn [2] GURMUKHI VOWEL SIGN EE..GURMUKHI VOWEL SIGN AI +0A4B..0A4D ; Extend # Mn [3] GURMUKHI VOWEL SIGN OO..GURMUKHI SIGN VIRAMA +0A51 ; Extend # Mn GURMUKHI SIGN UDAAT +0A70..0A71 ; Extend # Mn [2] GURMUKHI TIPPI..GURMUKHI ADDAK +0A75 ; Extend # Mn GURMUKHI SIGN YAKASH +0A81..0A82 ; Extend # Mn [2] GUJARATI SIGN CANDRABINDU..GUJARATI SIGN ANUSVARA +0A83 ; Extend # Mc GUJARATI SIGN VISARGA +0ABC ; Extend # Mn GUJARATI SIGN NUKTA +0ABE..0AC0 ; Extend # Mc [3] GUJARATI VOWEL SIGN AA..GUJARATI VOWEL SIGN II +0AC1..0AC5 ; Extend # Mn [5] GUJARATI VOWEL SIGN U..GUJARATI VOWEL SIGN CANDRA E +0AC7..0AC8 ; Extend # Mn [2] GUJARATI VOWEL SIGN E..GUJARATI VOWEL SIGN AI +0AC9 ; Extend # Mc GUJARATI VOWEL SIGN CANDRA O +0ACB..0ACC ; Extend # Mc [2] GUJARATI VOWEL SIGN O..GUJARATI VOWEL SIGN AU +0ACD ; Extend # Mn GUJARATI SIGN VIRAMA +0AE2..0AE3 ; Extend # Mn [2] GUJARATI VOWEL SIGN VOCALIC L..GUJARATI VOWEL SIGN VOCALIC LL +0AFA..0AFF ; Extend # Mn [6] GUJARATI SIGN SUKUN..GUJARATI SIGN TWO-CIRCLE NUKTA ABOVE +0B01 ; Extend # Mn ORIYA SIGN CANDRABINDU +0B02..0B03 ; Extend # Mc [2] ORIYA SIGN ANUSVARA..ORIYA SIGN VISARGA +0B3C ; Extend # Mn ORIYA SIGN NUKTA +0B3E ; Extend # Mc ORIYA VOWEL SIGN AA +0B3F ; Extend # Mn ORIYA VOWEL SIGN I +0B40 ; Extend # Mc ORIYA VOWEL SIGN II +0B41..0B44 ; Extend # Mn [4] ORIYA VOWEL SIGN U..ORIYA VOWEL SIGN VOCALIC RR +0B47..0B48 ; Extend # Mc [2] ORIYA VOWEL SIGN E..ORIYA VOWEL SIGN AI +0B4B..0B4C ; Extend # Mc [2] ORIYA VOWEL SIGN O..ORIYA VOWEL SIGN AU +0B4D ; Extend # Mn ORIYA SIGN VIRAMA +0B55..0B56 ; Extend # Mn [2] ORIYA SIGN OVERLINE..ORIYA AI LENGTH MARK +0B57 ; Extend # Mc ORIYA AU LENGTH MARK +0B62..0B63 ; Extend # Mn [2] ORIYA VOWEL SIGN VOCALIC L..ORIYA VOWEL SIGN VOCALIC LL +0B82 ; Extend # Mn TAMIL SIGN ANUSVARA +0BBE..0BBF ; Extend # Mc [2] TAMIL VOWEL SIGN AA..TAMIL VOWEL SIGN I +0BC0 ; Extend # Mn TAMIL VOWEL SIGN II +0BC1..0BC2 ; Extend # Mc [2] TAMIL VOWEL SIGN U..TAMIL VOWEL SIGN UU +0BC6..0BC8 ; Extend # Mc [3] TAMIL VOWEL SIGN E..TAMIL VOWEL SIGN AI +0BCA..0BCC ; Extend # Mc [3] TAMIL VOWEL SIGN O..TAMIL VOWEL SIGN AU +0BCD ; Extend # Mn TAMIL SIGN VIRAMA +0BD7 ; Extend # Mc TAMIL AU LENGTH MARK +0C00 ; Extend # Mn TELUGU SIGN COMBINING CANDRABINDU ABOVE +0C01..0C03 ; Extend # Mc [3] TELUGU SIGN CANDRABINDU..TELUGU SIGN VISARGA +0C04 ; Extend # Mn TELUGU SIGN COMBINING ANUSVARA ABOVE +0C3C ; Extend # Mn TELUGU SIGN NUKTA +0C3E..0C40 ; Extend # Mn [3] TELUGU VOWEL SIGN AA..TELUGU VOWEL SIGN II +0C41..0C44 ; Extend # Mc [4] TELUGU VOWEL SIGN U..TELUGU VOWEL SIGN VOCALIC RR +0C46..0C48 ; Extend # Mn [3] TELUGU VOWEL SIGN E..TELUGU VOWEL SIGN AI +0C4A..0C4D ; Extend # Mn [4] TELUGU VOWEL SIGN O..TELUGU SIGN VIRAMA +0C55..0C56 ; Extend # Mn [2] TELUGU LENGTH MARK..TELUGU AI LENGTH MARK +0C62..0C63 ; Extend # Mn [2] TELUGU VOWEL SIGN VOCALIC L..TELUGU VOWEL SIGN VOCALIC LL +0C81 ; Extend # Mn KANNADA SIGN CANDRABINDU +0C82..0C83 ; Extend # Mc [2] KANNADA SIGN ANUSVARA..KANNADA SIGN VISARGA +0CBC ; Extend # Mn KANNADA SIGN NUKTA +0CBE ; Extend # Mc KANNADA VOWEL SIGN AA +0CBF ; Extend # Mn KANNADA VOWEL SIGN I +0CC0..0CC4 ; Extend # Mc [5] KANNADA VOWEL SIGN II..KANNADA VOWEL SIGN VOCALIC RR +0CC6 ; Extend # Mn KANNADA VOWEL SIGN E +0CC7..0CC8 ; Extend # Mc [2] KANNADA VOWEL SIGN EE..KANNADA VOWEL SIGN AI +0CCA..0CCB ; Extend # Mc [2] KANNADA VOWEL SIGN O..KANNADA VOWEL SIGN OO +0CCC..0CCD ; Extend # Mn [2] KANNADA VOWEL SIGN AU..KANNADA SIGN VIRAMA +0CD5..0CD6 ; Extend # Mc [2] KANNADA LENGTH MARK..KANNADA AI LENGTH MARK +0CE2..0CE3 ; Extend # Mn [2] KANNADA VOWEL SIGN VOCALIC L..KANNADA VOWEL SIGN VOCALIC LL +0CF3 ; Extend # Mc KANNADA SIGN COMBINING ANUSVARA ABOVE RIGHT +0D00..0D01 ; Extend # Mn [2] MALAYALAM SIGN COMBINING ANUSVARA ABOVE..MALAYALAM SIGN CANDRABINDU +0D02..0D03 ; Extend # Mc [2] MALAYALAM SIGN ANUSVARA..MALAYALAM SIGN VISARGA +0D3B..0D3C ; Extend # Mn [2] MALAYALAM SIGN VERTICAL BAR VIRAMA..MALAYALAM SIGN CIRCULAR VIRAMA +0D3E..0D40 ; Extend # Mc [3] MALAYALAM VOWEL SIGN AA..MALAYALAM VOWEL SIGN II +0D41..0D44 ; Extend # Mn [4] MALAYALAM VOWEL SIGN U..MALAYALAM VOWEL SIGN VOCALIC RR +0D46..0D48 ; Extend # Mc [3] MALAYALAM VOWEL SIGN E..MALAYALAM VOWEL SIGN AI +0D4A..0D4C ; Extend # Mc [3] MALAYALAM VOWEL SIGN O..MALAYALAM VOWEL SIGN AU +0D4D ; Extend # Mn MALAYALAM SIGN VIRAMA +0D57 ; Extend # Mc MALAYALAM AU LENGTH MARK +0D62..0D63 ; Extend # Mn [2] MALAYALAM VOWEL SIGN VOCALIC L..MALAYALAM VOWEL SIGN VOCALIC LL +0D81 ; Extend # Mn SINHALA SIGN CANDRABINDU +0D82..0D83 ; Extend # Mc [2] SINHALA SIGN ANUSVARAYA..SINHALA SIGN VISARGAYA +0DCA ; Extend # Mn SINHALA SIGN AL-LAKUNA +0DCF..0DD1 ; Extend # Mc [3] SINHALA VOWEL SIGN AELA-PILLA..SINHALA VOWEL SIGN DIGA AEDA-PILLA +0DD2..0DD4 ; Extend # Mn [3] SINHALA VOWEL SIGN KETTI IS-PILLA..SINHALA VOWEL SIGN KETTI PAA-PILLA +0DD6 ; Extend # Mn SINHALA VOWEL SIGN DIGA PAA-PILLA +0DD8..0DDF ; Extend # Mc [8] SINHALA VOWEL SIGN GAETTA-PILLA..SINHALA VOWEL SIGN GAYANUKITTA +0DF2..0DF3 ; Extend # Mc [2] SINHALA VOWEL SIGN DIGA GAETTA-PILLA..SINHALA VOWEL SIGN DIGA GAYANUKITTA +0E31 ; Extend # Mn THAI CHARACTER MAI HAN-AKAT +0E34..0E3A ; Extend # Mn [7] THAI CHARACTER SARA I..THAI CHARACTER PHINTHU +0E47..0E4E ; Extend # Mn [8] THAI CHARACTER MAITAIKHU..THAI CHARACTER YAMAKKAN +0EB1 ; Extend # Mn LAO VOWEL SIGN MAI KAN +0EB4..0EBC ; Extend # Mn [9] LAO VOWEL SIGN I..LAO SEMIVOWEL SIGN LO +0EC8..0ECE ; Extend # Mn [7] LAO TONE MAI EK..LAO YAMAKKAN +0F18..0F19 ; Extend # Mn [2] TIBETAN ASTROLOGICAL SIGN -KHYUD PA..TIBETAN ASTROLOGICAL SIGN SDONG TSHUGS +0F35 ; Extend # Mn TIBETAN MARK NGAS BZUNG NYI ZLA +0F37 ; Extend # Mn TIBETAN MARK NGAS BZUNG SGOR RTAGS +0F39 ; Extend # Mn TIBETAN MARK TSA -PHRU +0F3E..0F3F ; Extend # Mc [2] TIBETAN SIGN YAR TSHES..TIBETAN SIGN MAR TSHES +0F71..0F7E ; Extend # Mn [14] TIBETAN VOWEL SIGN AA..TIBETAN SIGN RJES SU NGA RO +0F7F ; Extend # Mc TIBETAN SIGN RNAM BCAD +0F80..0F84 ; Extend # Mn [5] TIBETAN VOWEL SIGN REVERSED I..TIBETAN MARK HALANTA +0F86..0F87 ; Extend # Mn [2] TIBETAN SIGN LCI RTAGS..TIBETAN SIGN YANG RTAGS +0F8D..0F97 ; Extend # Mn [11] TIBETAN SUBJOINED SIGN LCE TSA CAN..TIBETAN SUBJOINED LETTER JA +0F99..0FBC ; Extend # Mn [36] TIBETAN SUBJOINED LETTER NYA..TIBETAN SUBJOINED LETTER FIXED-FORM RA +0FC6 ; Extend # Mn TIBETAN SYMBOL PADMA GDAN +102B..102C ; Extend # Mc [2] MYANMAR VOWEL SIGN TALL AA..MYANMAR VOWEL SIGN AA +102D..1030 ; Extend # Mn [4] MYANMAR VOWEL SIGN I..MYANMAR VOWEL SIGN UU +1031 ; Extend # Mc MYANMAR VOWEL SIGN E +1032..1037 ; Extend # Mn [6] MYANMAR VOWEL SIGN AI..MYANMAR SIGN DOT BELOW +1038 ; Extend # Mc MYANMAR SIGN VISARGA +1039..103A ; Extend # Mn [2] MYANMAR SIGN VIRAMA..MYANMAR SIGN ASAT +103B..103C ; Extend # Mc [2] MYANMAR CONSONANT SIGN MEDIAL YA..MYANMAR CONSONANT SIGN MEDIAL RA +103D..103E ; Extend # Mn [2] MYANMAR CONSONANT SIGN MEDIAL WA..MYANMAR CONSONANT SIGN MEDIAL HA +1056..1057 ; Extend # Mc [2] MYANMAR VOWEL SIGN VOCALIC R..MYANMAR VOWEL SIGN VOCALIC RR +1058..1059 ; Extend # Mn [2] MYANMAR VOWEL SIGN VOCALIC L..MYANMAR VOWEL SIGN VOCALIC LL +105E..1060 ; Extend # Mn [3] MYANMAR CONSONANT SIGN MON MEDIAL NA..MYANMAR CONSONANT SIGN MON MEDIAL LA +1062..1064 ; Extend # Mc [3] MYANMAR VOWEL SIGN SGAW KAREN EU..MYANMAR TONE MARK SGAW KAREN KE PHO +1067..106D ; Extend # Mc [7] MYANMAR VOWEL SIGN WESTERN PWO KAREN EU..MYANMAR SIGN WESTERN PWO KAREN TONE-5 +1071..1074 ; Extend # Mn [4] MYANMAR VOWEL SIGN GEBA KAREN I..MYANMAR VOWEL SIGN KAYAH EE +1082 ; Extend # Mn MYANMAR CONSONANT SIGN SHAN MEDIAL WA +1083..1084 ; Extend # Mc [2] MYANMAR VOWEL SIGN SHAN AA..MYANMAR VOWEL SIGN SHAN E +1085..1086 ; Extend # Mn [2] MYANMAR VOWEL SIGN SHAN E ABOVE..MYANMAR VOWEL SIGN SHAN FINAL Y +1087..108C ; Extend # Mc [6] MYANMAR SIGN SHAN TONE-2..MYANMAR SIGN SHAN COUNCIL TONE-3 +108D ; Extend # Mn MYANMAR SIGN SHAN COUNCIL EMPHATIC TONE +108F ; Extend # Mc MYANMAR SIGN RUMAI PALAUNG TONE-5 +109A..109C ; Extend # Mc [3] MYANMAR SIGN KHAMTI TONE-1..MYANMAR VOWEL SIGN AITON A +109D ; Extend # Mn MYANMAR VOWEL SIGN AITON AI +135D..135F ; Extend # Mn [3] ETHIOPIC COMBINING GEMINATION AND VOWEL LENGTH MARK..ETHIOPIC COMBINING GEMINATION MARK +1712..1714 ; Extend # Mn [3] TAGALOG VOWEL SIGN I..TAGALOG SIGN VIRAMA +1715 ; Extend # Mc TAGALOG SIGN PAMUDPOD +1732..1733 ; Extend # Mn [2] HANUNOO VOWEL SIGN I..HANUNOO VOWEL SIGN U +1734 ; Extend # Mc HANUNOO SIGN PAMUDPOD +1752..1753 ; Extend # Mn [2] BUHID VOWEL SIGN I..BUHID VOWEL SIGN U +1772..1773 ; Extend # Mn [2] TAGBANWA VOWEL SIGN I..TAGBANWA VOWEL SIGN U +17B4..17B5 ; Extend # Mn [2] KHMER VOWEL INHERENT AQ..KHMER VOWEL INHERENT AA +17B6 ; Extend # Mc KHMER VOWEL SIGN AA +17B7..17BD ; Extend # Mn [7] KHMER VOWEL SIGN I..KHMER VOWEL SIGN UA +17BE..17C5 ; Extend # Mc [8] KHMER VOWEL SIGN OE..KHMER VOWEL SIGN AU +17C6 ; Extend # Mn KHMER SIGN NIKAHIT +17C7..17C8 ; Extend # Mc [2] KHMER SIGN REAHMUK..KHMER SIGN YUUKALEAPINTU +17C9..17D3 ; Extend # Mn [11] KHMER SIGN MUUSIKATOAN..KHMER SIGN BATHAMASAT +17DD ; Extend # Mn KHMER SIGN ATTHACAN +180B..180D ; Extend # Mn [3] MONGOLIAN FREE VARIATION SELECTOR ONE..MONGOLIAN FREE VARIATION SELECTOR THREE +180F ; Extend # Mn MONGOLIAN FREE VARIATION SELECTOR FOUR +1885..1886 ; Extend # Mn [2] MONGOLIAN LETTER ALI GALI BALUDA..MONGOLIAN LETTER ALI GALI THREE BALUDA +18A9 ; Extend # Mn MONGOLIAN LETTER ALI GALI DAGALGA +1920..1922 ; Extend # Mn [3] LIMBU VOWEL SIGN A..LIMBU VOWEL SIGN U +1923..1926 ; Extend # Mc [4] LIMBU VOWEL SIGN EE..LIMBU VOWEL SIGN AU +1927..1928 ; Extend # Mn [2] LIMBU VOWEL SIGN E..LIMBU VOWEL SIGN O +1929..192B ; Extend # Mc [3] LIMBU SUBJOINED LETTER YA..LIMBU SUBJOINED LETTER WA +1930..1931 ; Extend # Mc [2] LIMBU SMALL LETTER KA..LIMBU SMALL LETTER NGA +1932 ; Extend # Mn LIMBU SMALL LETTER ANUSVARA +1933..1938 ; Extend # Mc [6] LIMBU SMALL LETTER TA..LIMBU SMALL LETTER LA +1939..193B ; Extend # Mn [3] LIMBU SIGN MUKPHRENG..LIMBU SIGN SA-I +1A17..1A18 ; Extend # Mn [2] BUGINESE VOWEL SIGN I..BUGINESE VOWEL SIGN U +1A19..1A1A ; Extend # Mc [2] BUGINESE VOWEL SIGN E..BUGINESE VOWEL SIGN O +1A1B ; Extend # Mn BUGINESE VOWEL SIGN AE +1A55 ; Extend # Mc TAI THAM CONSONANT SIGN MEDIAL RA +1A56 ; Extend # Mn TAI THAM CONSONANT SIGN MEDIAL LA +1A57 ; Extend # Mc TAI THAM CONSONANT SIGN LA TANG LAI +1A58..1A5E ; Extend # Mn [7] TAI THAM SIGN MAI KANG LAI..TAI THAM CONSONANT SIGN SA +1A60 ; Extend # Mn TAI THAM SIGN SAKOT +1A61 ; Extend # Mc TAI THAM VOWEL SIGN A +1A62 ; Extend # Mn TAI THAM VOWEL SIGN MAI SAT +1A63..1A64 ; Extend # Mc [2] TAI THAM VOWEL SIGN AA..TAI THAM VOWEL SIGN TALL AA +1A65..1A6C ; Extend # Mn [8] TAI THAM VOWEL SIGN I..TAI THAM VOWEL SIGN OA BELOW +1A6D..1A72 ; Extend # Mc [6] TAI THAM VOWEL SIGN OY..TAI THAM VOWEL SIGN THAM AI +1A73..1A7C ; Extend # Mn [10] TAI THAM VOWEL SIGN OA ABOVE..TAI THAM SIGN KHUEN-LUE KARAN +1A7F ; Extend # Mn TAI THAM COMBINING CRYPTOGRAMMIC DOT +1AB0..1ABD ; Extend # Mn [14] COMBINING DOUBLED CIRCUMFLEX ACCENT..COMBINING PARENTHESES BELOW +1ABE ; Extend # Me COMBINING PARENTHESES OVERLAY +1ABF..1ADD ; Extend # Mn [31] COMBINING LATIN SMALL LETTER W BELOW..COMBINING DOT-AND-RING BELOW +1AE0..1AEB ; Extend # Mn [12] COMBINING LEFT TACK ABOVE..COMBINING DOUBLE RIGHTWARDS ARROW ABOVE +1B00..1B03 ; Extend # Mn [4] BALINESE SIGN ULU RICEM..BALINESE SIGN SURANG +1B04 ; Extend # Mc BALINESE SIGN BISAH +1B34 ; Extend # Mn BALINESE SIGN REREKAN +1B35 ; Extend # Mc BALINESE VOWEL SIGN TEDUNG +1B36..1B3A ; Extend # Mn [5] BALINESE VOWEL SIGN ULU..BALINESE VOWEL SIGN RA REPA +1B3B ; Extend # Mc BALINESE VOWEL SIGN RA REPA TEDUNG +1B3C ; Extend # Mn BALINESE VOWEL SIGN LA LENGA +1B3D..1B41 ; Extend # Mc [5] BALINESE VOWEL SIGN LA LENGA TEDUNG..BALINESE VOWEL SIGN TALING REPA TEDUNG +1B42 ; Extend # Mn BALINESE VOWEL SIGN PEPET +1B43..1B44 ; Extend # Mc [2] BALINESE VOWEL SIGN PEPET TEDUNG..BALINESE ADEG ADEG +1B6B..1B73 ; Extend # Mn [9] BALINESE MUSICAL SYMBOL COMBINING TEGEH..BALINESE MUSICAL SYMBOL COMBINING GONG +1B80..1B81 ; Extend # Mn [2] SUNDANESE SIGN PANYECEK..SUNDANESE SIGN PANGLAYAR +1B82 ; Extend # Mc SUNDANESE SIGN PANGWISAD +1BA1 ; Extend # Mc SUNDANESE CONSONANT SIGN PAMINGKAL +1BA2..1BA5 ; Extend # Mn [4] SUNDANESE CONSONANT SIGN PANYAKRA..SUNDANESE VOWEL SIGN PANYUKU +1BA6..1BA7 ; Extend # Mc [2] SUNDANESE VOWEL SIGN PANAELAENG..SUNDANESE VOWEL SIGN PANOLONG +1BA8..1BA9 ; Extend # Mn [2] SUNDANESE VOWEL SIGN PAMEPET..SUNDANESE VOWEL SIGN PANEULEUNG +1BAA ; Extend # Mc SUNDANESE SIGN PAMAAEH +1BAB..1BAD ; Extend # Mn [3] SUNDANESE SIGN VIRAMA..SUNDANESE CONSONANT SIGN PASANGAN WA +1BE6 ; Extend # Mn BATAK SIGN TOMPI +1BE7 ; Extend # Mc BATAK VOWEL SIGN E +1BE8..1BE9 ; Extend # Mn [2] BATAK VOWEL SIGN PAKPAK E..BATAK VOWEL SIGN EE +1BEA..1BEC ; Extend # Mc [3] BATAK VOWEL SIGN I..BATAK VOWEL SIGN O +1BED ; Extend # Mn BATAK VOWEL SIGN KARO O +1BEE ; Extend # Mc BATAK VOWEL SIGN U +1BEF..1BF1 ; Extend # Mn [3] BATAK VOWEL SIGN U FOR SIMALUNGUN SA..BATAK CONSONANT SIGN H +1BF2..1BF3 ; Extend # Mc [2] BATAK PANGOLAT..BATAK PANONGONAN +1C24..1C2B ; Extend # Mc [8] LEPCHA SUBJOINED LETTER YA..LEPCHA VOWEL SIGN UU +1C2C..1C33 ; Extend # Mn [8] LEPCHA VOWEL SIGN E..LEPCHA CONSONANT SIGN T +1C34..1C35 ; Extend # Mc [2] LEPCHA CONSONANT SIGN NYIN-DO..LEPCHA CONSONANT SIGN KANG +1C36..1C37 ; Extend # Mn [2] LEPCHA SIGN RAN..LEPCHA SIGN NUKTA +1CD0..1CD2 ; Extend # Mn [3] VEDIC TONE KARSHANA..VEDIC TONE PRENKHA +1CD4..1CE0 ; Extend # Mn [13] VEDIC SIGN YAJURVEDIC MIDLINE SVARITA..VEDIC TONE RIGVEDIC KASHMIRI INDEPENDENT SVARITA +1CE1 ; Extend # Mc VEDIC TONE ATHARVAVEDIC INDEPENDENT SVARITA +1CE2..1CE8 ; Extend # Mn [7] VEDIC SIGN VISARGA SVARITA..VEDIC SIGN VISARGA ANUDATTA WITH TAIL +1CED ; Extend # Mn VEDIC SIGN TIRYAK +1CF4 ; Extend # Mn VEDIC TONE CANDRA ABOVE +1CF7 ; Extend # Mc VEDIC SIGN ATIKRAMA +1CF8..1CF9 ; Extend # Mn [2] VEDIC TONE RING ABOVE..VEDIC TONE DOUBLE RING ABOVE +1DC0..1DFF ; Extend # Mn [64] COMBINING DOTTED GRAVE ACCENT..COMBINING RIGHT ARROWHEAD AND DOWN ARROWHEAD BELOW +200C ; Extend # Cf ZERO WIDTH NON-JOINER +20D0..20DC ; Extend # Mn [13] COMBINING LEFT HARPOON ABOVE..COMBINING FOUR DOTS ABOVE +20DD..20E0 ; Extend # Me [4] COMBINING ENCLOSING CIRCLE..COMBINING ENCLOSING CIRCLE BACKSLASH +20E1 ; Extend # Mn COMBINING LEFT RIGHT ARROW ABOVE +20E2..20E4 ; Extend # Me [3] COMBINING ENCLOSING SCREEN..COMBINING ENCLOSING UPWARD POINTING TRIANGLE +20E5..20F0 ; Extend # Mn [12] COMBINING REVERSE SOLIDUS OVERLAY..COMBINING ASTERISK ABOVE +2CEF..2CF1 ; Extend # Mn [3] COPTIC COMBINING NI ABOVE..COPTIC COMBINING SPIRITUS LENIS +2D7F ; Extend # Mn TIFINAGH CONSONANT JOINER +2DE0..2DFF ; Extend # Mn [32] COMBINING CYRILLIC LETTER BE..COMBINING CYRILLIC LETTER IOTIFIED BIG YUS +302A..302D ; Extend # Mn [4] IDEOGRAPHIC LEVEL TONE MARK..IDEOGRAPHIC ENTERING TONE MARK +302E..302F ; Extend # Mc [2] HANGUL SINGLE DOT TONE MARK..HANGUL DOUBLE DOT TONE MARK +3099..309A ; Extend # Mn [2] COMBINING KATAKANA-HIRAGANA VOICED SOUND MARK..COMBINING KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK +A66F ; Extend # Mn COMBINING CYRILLIC VZMET +A670..A672 ; Extend # Me [3] COMBINING CYRILLIC TEN MILLIONS SIGN..COMBINING CYRILLIC THOUSAND MILLIONS SIGN +A674..A67D ; Extend # Mn [10] COMBINING CYRILLIC LETTER UKRAINIAN IE..COMBINING CYRILLIC PAYEROK +A69E..A69F ; Extend # Mn [2] COMBINING CYRILLIC LETTER EF..COMBINING CYRILLIC LETTER IOTIFIED E +A6F0..A6F1 ; Extend # Mn [2] BAMUM COMBINING MARK KOQNDON..BAMUM COMBINING MARK TUKWENTIS +A802 ; Extend # Mn SYLOTI NAGRI SIGN DVISVARA +A806 ; Extend # Mn SYLOTI NAGRI SIGN HASANTA +A80B ; Extend # Mn SYLOTI NAGRI SIGN ANUSVARA +A823..A824 ; Extend # Mc [2] SYLOTI NAGRI VOWEL SIGN A..SYLOTI NAGRI VOWEL SIGN I +A825..A826 ; Extend # Mn [2] SYLOTI NAGRI VOWEL SIGN U..SYLOTI NAGRI VOWEL SIGN E +A827 ; Extend # Mc SYLOTI NAGRI VOWEL SIGN OO +A82C ; Extend # Mn SYLOTI NAGRI SIGN ALTERNATE HASANTA +A880..A881 ; Extend # Mc [2] SAURASHTRA SIGN ANUSVARA..SAURASHTRA SIGN VISARGA +A8B4..A8C3 ; Extend # Mc [16] SAURASHTRA CONSONANT SIGN HAARU..SAURASHTRA VOWEL SIGN AU +A8C4..A8C5 ; Extend # Mn [2] SAURASHTRA SIGN VIRAMA..SAURASHTRA SIGN CANDRABINDU +A8E0..A8F1 ; Extend # Mn [18] COMBINING DEVANAGARI DIGIT ZERO..COMBINING DEVANAGARI SIGN AVAGRAHA +A8FF ; Extend # Mn DEVANAGARI VOWEL SIGN AY +A926..A92D ; Extend # Mn [8] KAYAH LI VOWEL UE..KAYAH LI TONE CALYA PLOPHU +A947..A951 ; Extend # Mn [11] REJANG VOWEL SIGN I..REJANG CONSONANT SIGN R +A952..A953 ; Extend # Mc [2] REJANG CONSONANT SIGN H..REJANG VIRAMA +A980..A982 ; Extend # Mn [3] JAVANESE SIGN PANYANGGA..JAVANESE SIGN LAYAR +A983 ; Extend # Mc JAVANESE SIGN WIGNYAN +A9B3 ; Extend # Mn JAVANESE SIGN CECAK TELU +A9B4..A9B5 ; Extend # Mc [2] JAVANESE VOWEL SIGN TARUNG..JAVANESE VOWEL SIGN TOLONG +A9B6..A9B9 ; Extend # Mn [4] JAVANESE VOWEL SIGN WULU..JAVANESE VOWEL SIGN SUKU MENDUT +A9BA..A9BB ; Extend # Mc [2] JAVANESE VOWEL SIGN TALING..JAVANESE VOWEL SIGN DIRGA MURE +A9BC..A9BD ; Extend # Mn [2] JAVANESE VOWEL SIGN PEPET..JAVANESE CONSONANT SIGN KERET +A9BE..A9C0 ; Extend # Mc [3] JAVANESE CONSONANT SIGN PENGKAL..JAVANESE PANGKON +A9E5 ; Extend # Mn MYANMAR SIGN SHAN SAW +AA29..AA2E ; Extend # Mn [6] CHAM VOWEL SIGN AA..CHAM VOWEL SIGN OE +AA2F..AA30 ; Extend # Mc [2] CHAM VOWEL SIGN O..CHAM VOWEL SIGN AI +AA31..AA32 ; Extend # Mn [2] CHAM VOWEL SIGN AU..CHAM VOWEL SIGN UE +AA33..AA34 ; Extend # Mc [2] CHAM CONSONANT SIGN YA..CHAM CONSONANT SIGN RA +AA35..AA36 ; Extend # Mn [2] CHAM CONSONANT SIGN LA..CHAM CONSONANT SIGN WA +AA43 ; Extend # Mn CHAM CONSONANT SIGN FINAL NG +AA4C ; Extend # Mn CHAM CONSONANT SIGN FINAL M +AA4D ; Extend # Mc CHAM CONSONANT SIGN FINAL H +AA7B ; Extend # Mc MYANMAR SIGN PAO KAREN TONE +AA7C ; Extend # Mn MYANMAR SIGN TAI LAING TONE-2 +AA7D ; Extend # Mc MYANMAR SIGN TAI LAING TONE-5 +AAB0 ; Extend # Mn TAI VIET MAI KANG +AAB2..AAB4 ; Extend # Mn [3] TAI VIET VOWEL I..TAI VIET VOWEL U +AAB7..AAB8 ; Extend # Mn [2] TAI VIET MAI KHIT..TAI VIET VOWEL IA +AABE..AABF ; Extend # Mn [2] TAI VIET VOWEL AM..TAI VIET TONE MAI EK +AAC1 ; Extend # Mn TAI VIET TONE MAI THO +AAEB ; Extend # Mc MEETEI MAYEK VOWEL SIGN II +AAEC..AAED ; Extend # Mn [2] MEETEI MAYEK VOWEL SIGN UU..MEETEI MAYEK VOWEL SIGN AAI +AAEE..AAEF ; Extend # Mc [2] MEETEI MAYEK VOWEL SIGN AU..MEETEI MAYEK VOWEL SIGN AAU +AAF5 ; Extend # Mc MEETEI MAYEK VOWEL SIGN VISARGA +AAF6 ; Extend # Mn MEETEI MAYEK VIRAMA +ABE3..ABE4 ; Extend # Mc [2] MEETEI MAYEK VOWEL SIGN ONAP..MEETEI MAYEK VOWEL SIGN INAP +ABE5 ; Extend # Mn MEETEI MAYEK VOWEL SIGN ANAP +ABE6..ABE7 ; Extend # Mc [2] MEETEI MAYEK VOWEL SIGN YENAP..MEETEI MAYEK VOWEL SIGN SOUNAP +ABE8 ; Extend # Mn MEETEI MAYEK VOWEL SIGN UNAP +ABE9..ABEA ; Extend # Mc [2] MEETEI MAYEK VOWEL SIGN CHEINAP..MEETEI MAYEK VOWEL SIGN NUNG +ABEC ; Extend # Mc MEETEI MAYEK LUM IYEK +ABED ; Extend # Mn MEETEI MAYEK APUN IYEK +FB1E ; Extend # Mn HEBREW POINT JUDEO-SPANISH VARIKA +FE00..FE0F ; Extend # Mn [16] VARIATION SELECTOR-1..VARIATION SELECTOR-16 +FE20..FE2F ; Extend # Mn [16] COMBINING LIGATURE LEFT HALF..COMBINING CYRILLIC TITLO RIGHT HALF +FF9E..FF9F ; Extend # Lm [2] HALFWIDTH KATAKANA VOICED SOUND MARK..HALFWIDTH KATAKANA SEMI-VOICED SOUND MARK +101FD ; Extend # Mn PHAISTOS DISC SIGN COMBINING OBLIQUE STROKE +102E0 ; Extend # Mn COPTIC EPACT THOUSANDS MARK +10376..1037A ; Extend # Mn [5] COMBINING OLD PERMIC LETTER AN..COMBINING OLD PERMIC LETTER SII +10A01..10A03 ; Extend # Mn [3] KHAROSHTHI VOWEL SIGN I..KHAROSHTHI VOWEL SIGN VOCALIC R +10A05..10A06 ; Extend # Mn [2] KHAROSHTHI VOWEL SIGN E..KHAROSHTHI VOWEL SIGN O +10A0C..10A0F ; Extend # Mn [4] KHAROSHTHI VOWEL LENGTH MARK..KHAROSHTHI SIGN VISARGA +10A38..10A3A ; Extend # Mn [3] KHAROSHTHI SIGN BAR ABOVE..KHAROSHTHI SIGN DOT BELOW +10A3F ; Extend # Mn KHAROSHTHI VIRAMA +10AE5..10AE6 ; Extend # Mn [2] MANICHAEAN ABBREVIATION MARK ABOVE..MANICHAEAN ABBREVIATION MARK BELOW +10D24..10D27 ; Extend # Mn [4] HANIFI ROHINGYA SIGN HARBAHAY..HANIFI ROHINGYA SIGN TASSI +10D69..10D6D ; Extend # Mn [5] GARAY VOWEL SIGN E..GARAY CONSONANT NASALIZATION MARK +10EAB..10EAC ; Extend # Mn [2] YEZIDI COMBINING HAMZA MARK..YEZIDI COMBINING MADDA MARK +10EFA..10EFF ; Extend # Mn [6] ARABIC DOUBLE VERTICAL BAR BELOW..ARABIC SMALL LOW WORD MADDA +10F46..10F50 ; Extend # Mn [11] SOGDIAN COMBINING DOT BELOW..SOGDIAN COMBINING STROKE BELOW +10F82..10F85 ; Extend # Mn [4] OLD UYGHUR COMBINING DOT ABOVE..OLD UYGHUR COMBINING TWO DOTS BELOW +11000 ; Extend # Mc BRAHMI SIGN CANDRABINDU +11001 ; Extend # Mn BRAHMI SIGN ANUSVARA +11002 ; Extend # Mc BRAHMI SIGN VISARGA +11038..11046 ; Extend # Mn [15] BRAHMI VOWEL SIGN AA..BRAHMI VIRAMA +11070 ; Extend # Mn BRAHMI SIGN OLD TAMIL VIRAMA +11073..11074 ; Extend # Mn [2] BRAHMI VOWEL SIGN OLD TAMIL SHORT E..BRAHMI VOWEL SIGN OLD TAMIL SHORT O +1107F..11081 ; Extend # Mn [3] BRAHMI NUMBER JOINER..KAITHI SIGN ANUSVARA +11082 ; Extend # Mc KAITHI SIGN VISARGA +110B0..110B2 ; Extend # Mc [3] KAITHI VOWEL SIGN AA..KAITHI VOWEL SIGN II +110B3..110B6 ; Extend # Mn [4] KAITHI VOWEL SIGN U..KAITHI VOWEL SIGN AI +110B7..110B8 ; Extend # Mc [2] KAITHI VOWEL SIGN O..KAITHI VOWEL SIGN AU +110B9..110BA ; Extend # Mn [2] KAITHI SIGN VIRAMA..KAITHI SIGN NUKTA +110C2 ; Extend # Mn KAITHI VOWEL SIGN VOCALIC R +11100..11102 ; Extend # Mn [3] CHAKMA SIGN CANDRABINDU..CHAKMA SIGN VISARGA +11127..1112B ; Extend # Mn [5] CHAKMA VOWEL SIGN A..CHAKMA VOWEL SIGN UU +1112C ; Extend # Mc CHAKMA VOWEL SIGN E +1112D..11134 ; Extend # Mn [8] CHAKMA VOWEL SIGN AI..CHAKMA MAAYYAA +11145..11146 ; Extend # Mc [2] CHAKMA VOWEL SIGN AA..CHAKMA VOWEL SIGN EI +11173 ; Extend # Mn MAHAJANI SIGN NUKTA +11180..11181 ; Extend # Mn [2] SHARADA SIGN CANDRABINDU..SHARADA SIGN ANUSVARA +11182 ; Extend # Mc SHARADA SIGN VISARGA +111B3..111B5 ; Extend # Mc [3] SHARADA VOWEL SIGN AA..SHARADA VOWEL SIGN II +111B6..111BE ; Extend # Mn [9] SHARADA VOWEL SIGN U..SHARADA VOWEL SIGN O +111BF..111C0 ; Extend # Mc [2] SHARADA VOWEL SIGN AU..SHARADA SIGN VIRAMA +111C9..111CC ; Extend # Mn [4] SHARADA SANDHI MARK..SHARADA EXTRA SHORT VOWEL MARK +111CE ; Extend # Mc SHARADA VOWEL SIGN PRISHTHAMATRA E +111CF ; Extend # Mn SHARADA SIGN INVERTED CANDRABINDU +1122C..1122E ; Extend # Mc [3] KHOJKI VOWEL SIGN AA..KHOJKI VOWEL SIGN II +1122F..11231 ; Extend # Mn [3] KHOJKI VOWEL SIGN U..KHOJKI VOWEL SIGN AI +11232..11233 ; Extend # Mc [2] KHOJKI VOWEL SIGN O..KHOJKI VOWEL SIGN AU +11234 ; Extend # Mn KHOJKI SIGN ANUSVARA +11235 ; Extend # Mc KHOJKI SIGN VIRAMA +11236..11237 ; Extend # Mn [2] KHOJKI SIGN NUKTA..KHOJKI SIGN SHADDA +1123E ; Extend # Mn KHOJKI SIGN SUKUN +11241 ; Extend # Mn KHOJKI VOWEL SIGN VOCALIC R +112DF ; Extend # Mn KHUDAWADI SIGN ANUSVARA +112E0..112E2 ; Extend # Mc [3] KHUDAWADI VOWEL SIGN AA..KHUDAWADI VOWEL SIGN II +112E3..112EA ; Extend # Mn [8] KHUDAWADI VOWEL SIGN U..KHUDAWADI SIGN VIRAMA +11300..11301 ; Extend # Mn [2] GRANTHA SIGN COMBINING ANUSVARA ABOVE..GRANTHA SIGN CANDRABINDU +11302..11303 ; Extend # Mc [2] GRANTHA SIGN ANUSVARA..GRANTHA SIGN VISARGA +1133B..1133C ; Extend # Mn [2] COMBINING BINDU BELOW..GRANTHA SIGN NUKTA +1133E..1133F ; Extend # Mc [2] GRANTHA VOWEL SIGN AA..GRANTHA VOWEL SIGN I +11340 ; Extend # Mn GRANTHA VOWEL SIGN II +11341..11344 ; Extend # Mc [4] GRANTHA VOWEL SIGN U..GRANTHA VOWEL SIGN VOCALIC RR +11347..11348 ; Extend # Mc [2] GRANTHA VOWEL SIGN EE..GRANTHA VOWEL SIGN AI +1134B..1134D ; Extend # Mc [3] GRANTHA VOWEL SIGN OO..GRANTHA SIGN VIRAMA +11357 ; Extend # Mc GRANTHA AU LENGTH MARK +11362..11363 ; Extend # Mc [2] GRANTHA VOWEL SIGN VOCALIC L..GRANTHA VOWEL SIGN VOCALIC LL +11366..1136C ; Extend # Mn [7] COMBINING GRANTHA DIGIT ZERO..COMBINING GRANTHA DIGIT SIX +11370..11374 ; Extend # Mn [5] COMBINING GRANTHA LETTER A..COMBINING GRANTHA LETTER PA +113B8..113BA ; Extend # Mc [3] TULU-TIGALARI VOWEL SIGN AA..TULU-TIGALARI VOWEL SIGN II +113BB..113C0 ; Extend # Mn [6] TULU-TIGALARI VOWEL SIGN U..TULU-TIGALARI VOWEL SIGN VOCALIC LL +113C2 ; Extend # Mc TULU-TIGALARI VOWEL SIGN EE +113C5 ; Extend # Mc TULU-TIGALARI VOWEL SIGN AI +113C7..113CA ; Extend # Mc [4] TULU-TIGALARI VOWEL SIGN OO..TULU-TIGALARI SIGN CANDRA ANUNASIKA +113CC..113CD ; Extend # Mc [2] TULU-TIGALARI SIGN ANUSVARA..TULU-TIGALARI SIGN VISARGA +113CE ; Extend # Mn TULU-TIGALARI SIGN VIRAMA +113CF ; Extend # Mc TULU-TIGALARI SIGN LOOPED VIRAMA +113D0 ; Extend # Mn TULU-TIGALARI CONJOINER +113D2 ; Extend # Mn TULU-TIGALARI GEMINATION MARK +113E1..113E2 ; Extend # Mn [2] TULU-TIGALARI VEDIC TONE SVARITA..TULU-TIGALARI VEDIC TONE ANUDATTA +11435..11437 ; Extend # Mc [3] NEWA VOWEL SIGN AA..NEWA VOWEL SIGN II +11438..1143F ; Extend # Mn [8] NEWA VOWEL SIGN U..NEWA VOWEL SIGN AI +11440..11441 ; Extend # Mc [2] NEWA VOWEL SIGN O..NEWA VOWEL SIGN AU +11442..11444 ; Extend # Mn [3] NEWA SIGN VIRAMA..NEWA SIGN ANUSVARA +11445 ; Extend # Mc NEWA SIGN VISARGA +11446 ; Extend # Mn NEWA SIGN NUKTA +1145E ; Extend # Mn NEWA SANDHI MARK +114B0..114B2 ; Extend # Mc [3] TIRHUTA VOWEL SIGN AA..TIRHUTA VOWEL SIGN II +114B3..114B8 ; Extend # Mn [6] TIRHUTA VOWEL SIGN U..TIRHUTA VOWEL SIGN VOCALIC LL +114B9 ; Extend # Mc TIRHUTA VOWEL SIGN E +114BA ; Extend # Mn TIRHUTA VOWEL SIGN SHORT E +114BB..114BE ; Extend # Mc [4] TIRHUTA VOWEL SIGN AI..TIRHUTA VOWEL SIGN AU +114BF..114C0 ; Extend # Mn [2] TIRHUTA SIGN CANDRABINDU..TIRHUTA SIGN ANUSVARA +114C1 ; Extend # Mc TIRHUTA SIGN VISARGA +114C2..114C3 ; Extend # Mn [2] TIRHUTA SIGN VIRAMA..TIRHUTA SIGN NUKTA +115AF..115B1 ; Extend # Mc [3] SIDDHAM VOWEL SIGN AA..SIDDHAM VOWEL SIGN II +115B2..115B5 ; Extend # Mn [4] SIDDHAM VOWEL SIGN U..SIDDHAM VOWEL SIGN VOCALIC RR +115B8..115BB ; Extend # Mc [4] SIDDHAM VOWEL SIGN E..SIDDHAM VOWEL SIGN AU +115BC..115BD ; Extend # Mn [2] SIDDHAM SIGN CANDRABINDU..SIDDHAM SIGN ANUSVARA +115BE ; Extend # Mc SIDDHAM SIGN VISARGA +115BF..115C0 ; Extend # Mn [2] SIDDHAM SIGN VIRAMA..SIDDHAM SIGN NUKTA +115DC..115DD ; Extend # Mn [2] SIDDHAM VOWEL SIGN ALTERNATE U..SIDDHAM VOWEL SIGN ALTERNATE UU +11630..11632 ; Extend # Mc [3] MODI VOWEL SIGN AA..MODI VOWEL SIGN II +11633..1163A ; Extend # Mn [8] MODI VOWEL SIGN U..MODI VOWEL SIGN AI +1163B..1163C ; Extend # Mc [2] MODI VOWEL SIGN O..MODI VOWEL SIGN AU +1163D ; Extend # Mn MODI SIGN ANUSVARA +1163E ; Extend # Mc MODI SIGN VISARGA +1163F..11640 ; Extend # Mn [2] MODI SIGN VIRAMA..MODI SIGN ARDHACANDRA +116AB ; Extend # Mn TAKRI SIGN ANUSVARA +116AC ; Extend # Mc TAKRI SIGN VISARGA +116AD ; Extend # Mn TAKRI VOWEL SIGN AA +116AE..116AF ; Extend # Mc [2] TAKRI VOWEL SIGN I..TAKRI VOWEL SIGN II +116B0..116B5 ; Extend # Mn [6] TAKRI VOWEL SIGN U..TAKRI VOWEL SIGN AU +116B6 ; Extend # Mc TAKRI SIGN VIRAMA +116B7 ; Extend # Mn TAKRI SIGN NUKTA +1171D ; Extend # Mn AHOM CONSONANT SIGN MEDIAL LA +1171E ; Extend # Mc AHOM CONSONANT SIGN MEDIAL RA +1171F ; Extend # Mn AHOM CONSONANT SIGN MEDIAL LIGATING RA +11720..11721 ; Extend # Mc [2] AHOM VOWEL SIGN A..AHOM VOWEL SIGN AA +11722..11725 ; Extend # Mn [4] AHOM VOWEL SIGN I..AHOM VOWEL SIGN UU +11726 ; Extend # Mc AHOM VOWEL SIGN E +11727..1172B ; Extend # Mn [5] AHOM VOWEL SIGN AW..AHOM SIGN KILLER +1182C..1182E ; Extend # Mc [3] DOGRA VOWEL SIGN AA..DOGRA VOWEL SIGN II +1182F..11837 ; Extend # Mn [9] DOGRA VOWEL SIGN U..DOGRA SIGN ANUSVARA +11838 ; Extend # Mc DOGRA SIGN VISARGA +11839..1183A ; Extend # Mn [2] DOGRA SIGN VIRAMA..DOGRA SIGN NUKTA +11930..11935 ; Extend # Mc [6] DIVES AKURU VOWEL SIGN AA..DIVES AKURU VOWEL SIGN E +11937..11938 ; Extend # Mc [2] DIVES AKURU VOWEL SIGN AI..DIVES AKURU VOWEL SIGN O +1193B..1193C ; Extend # Mn [2] DIVES AKURU SIGN ANUSVARA..DIVES AKURU SIGN CANDRABINDU +1193D ; Extend # Mc DIVES AKURU SIGN HALANTA +1193E ; Extend # Mn DIVES AKURU VIRAMA +11940 ; Extend # Mc DIVES AKURU MEDIAL YA +11942 ; Extend # Mc DIVES AKURU MEDIAL RA +11943 ; Extend # Mn DIVES AKURU SIGN NUKTA +119D1..119D3 ; Extend # Mc [3] NANDINAGARI VOWEL SIGN AA..NANDINAGARI VOWEL SIGN II +119D4..119D7 ; Extend # Mn [4] NANDINAGARI VOWEL SIGN U..NANDINAGARI VOWEL SIGN VOCALIC RR +119DA..119DB ; Extend # Mn [2] NANDINAGARI VOWEL SIGN E..NANDINAGARI VOWEL SIGN AI +119DC..119DF ; Extend # Mc [4] NANDINAGARI VOWEL SIGN O..NANDINAGARI SIGN VISARGA +119E0 ; Extend # Mn NANDINAGARI SIGN VIRAMA +119E4 ; Extend # Mc NANDINAGARI VOWEL SIGN PRISHTHAMATRA E +11A01..11A0A ; Extend # Mn [10] ZANABAZAR SQUARE VOWEL SIGN I..ZANABAZAR SQUARE VOWEL LENGTH MARK +11A33..11A38 ; Extend # Mn [6] ZANABAZAR SQUARE FINAL CONSONANT MARK..ZANABAZAR SQUARE SIGN ANUSVARA +11A39 ; Extend # Mc ZANABAZAR SQUARE SIGN VISARGA +11A3B..11A3E ; Extend # Mn [4] ZANABAZAR SQUARE CLUSTER-FINAL LETTER YA..ZANABAZAR SQUARE CLUSTER-FINAL LETTER VA +11A47 ; Extend # Mn ZANABAZAR SQUARE SUBJOINER +11A51..11A56 ; Extend # Mn [6] SOYOMBO VOWEL SIGN I..SOYOMBO VOWEL SIGN OE +11A57..11A58 ; Extend # Mc [2] SOYOMBO VOWEL SIGN AI..SOYOMBO VOWEL SIGN AU +11A59..11A5B ; Extend # Mn [3] SOYOMBO VOWEL SIGN VOCALIC R..SOYOMBO VOWEL LENGTH MARK +11A8A..11A96 ; Extend # Mn [13] SOYOMBO FINAL CONSONANT SIGN G..SOYOMBO SIGN ANUSVARA +11A97 ; Extend # Mc SOYOMBO SIGN VISARGA +11A98..11A99 ; Extend # Mn [2] SOYOMBO GEMINATION MARK..SOYOMBO SUBJOINER +11B60 ; Extend # Mn SHARADA VOWEL SIGN OE +11B61 ; Extend # Mc SHARADA VOWEL SIGN OOE +11B62..11B64 ; Extend # Mn [3] SHARADA VOWEL SIGN UE..SHARADA VOWEL SIGN SHORT E +11B65 ; Extend # Mc SHARADA VOWEL SIGN SHORT O +11B66 ; Extend # Mn SHARADA VOWEL SIGN CANDRA E +11B67 ; Extend # Mc SHARADA VOWEL SIGN CANDRA O +11C2F ; Extend # Mc BHAIKSUKI VOWEL SIGN AA +11C30..11C36 ; Extend # Mn [7] BHAIKSUKI VOWEL SIGN I..BHAIKSUKI VOWEL SIGN VOCALIC L +11C38..11C3D ; Extend # Mn [6] BHAIKSUKI VOWEL SIGN E..BHAIKSUKI SIGN ANUSVARA +11C3E ; Extend # Mc BHAIKSUKI SIGN VISARGA +11C3F ; Extend # Mn BHAIKSUKI SIGN VIRAMA +11C92..11CA7 ; Extend # Mn [22] MARCHEN SUBJOINED LETTER KA..MARCHEN SUBJOINED LETTER ZA +11CA9 ; Extend # Mc MARCHEN SUBJOINED LETTER YA +11CAA..11CB0 ; Extend # Mn [7] MARCHEN SUBJOINED LETTER RA..MARCHEN VOWEL SIGN AA +11CB1 ; Extend # Mc MARCHEN VOWEL SIGN I +11CB2..11CB3 ; Extend # Mn [2] MARCHEN VOWEL SIGN U..MARCHEN VOWEL SIGN E +11CB4 ; Extend # Mc MARCHEN VOWEL SIGN O +11CB5..11CB6 ; Extend # Mn [2] MARCHEN SIGN ANUSVARA..MARCHEN SIGN CANDRABINDU +11D31..11D36 ; Extend # Mn [6] MASARAM GONDI VOWEL SIGN AA..MASARAM GONDI VOWEL SIGN VOCALIC R +11D3A ; Extend # Mn MASARAM GONDI VOWEL SIGN E +11D3C..11D3D ; Extend # Mn [2] MASARAM GONDI VOWEL SIGN AI..MASARAM GONDI VOWEL SIGN O +11D3F..11D45 ; Extend # Mn [7] MASARAM GONDI VOWEL SIGN AU..MASARAM GONDI VIRAMA +11D47 ; Extend # Mn MASARAM GONDI RA-KARA +11D8A..11D8E ; Extend # Mc [5] GUNJALA GONDI VOWEL SIGN AA..GUNJALA GONDI VOWEL SIGN UU +11D90..11D91 ; Extend # Mn [2] GUNJALA GONDI VOWEL SIGN EE..GUNJALA GONDI VOWEL SIGN AI +11D93..11D94 ; Extend # Mc [2] GUNJALA GONDI VOWEL SIGN OO..GUNJALA GONDI VOWEL SIGN AU +11D95 ; Extend # Mn GUNJALA GONDI SIGN ANUSVARA +11D96 ; Extend # Mc GUNJALA GONDI SIGN VISARGA +11D97 ; Extend # Mn GUNJALA GONDI VIRAMA +11EF3..11EF4 ; Extend # Mn [2] MAKASAR VOWEL SIGN I..MAKASAR VOWEL SIGN U +11EF5..11EF6 ; Extend # Mc [2] MAKASAR VOWEL SIGN E..MAKASAR VOWEL SIGN O +11F00..11F01 ; Extend # Mn [2] KAWI SIGN CANDRABINDU..KAWI SIGN ANUSVARA +11F03 ; Extend # Mc KAWI SIGN VISARGA +11F34..11F35 ; Extend # Mc [2] KAWI VOWEL SIGN AA..KAWI VOWEL SIGN ALTERNATE AA +11F36..11F3A ; Extend # Mn [5] KAWI VOWEL SIGN I..KAWI VOWEL SIGN VOCALIC R +11F3E..11F3F ; Extend # Mc [2] KAWI VOWEL SIGN E..KAWI VOWEL SIGN AI +11F40 ; Extend # Mn KAWI VOWEL SIGN EU +11F41 ; Extend # Mc KAWI SIGN KILLER +11F42 ; Extend # Mn KAWI CONJOINER +11F5A ; Extend # Mn KAWI SIGN NUKTA +13440 ; Extend # Mn EGYPTIAN HIEROGLYPH MIRROR HORIZONTALLY +13447..13455 ; Extend # Mn [15] EGYPTIAN HIEROGLYPH MODIFIER DAMAGED AT TOP START..EGYPTIAN HIEROGLYPH MODIFIER DAMAGED +1611E..16129 ; Extend # Mn [12] GURUNG KHEMA VOWEL SIGN AA..GURUNG KHEMA VOWEL LENGTH MARK +1612A..1612C ; Extend # Mc [3] GURUNG KHEMA CONSONANT SIGN MEDIAL YA..GURUNG KHEMA CONSONANT SIGN MEDIAL HA +1612D..1612F ; Extend # Mn [3] GURUNG KHEMA SIGN ANUSVARA..GURUNG KHEMA SIGN THOLHOMA +16AF0..16AF4 ; Extend # Mn [5] BASSA VAH COMBINING HIGH TONE..BASSA VAH COMBINING HIGH-LOW TONE +16B30..16B36 ; Extend # Mn [7] PAHAWH HMONG MARK CIM TUB..PAHAWH HMONG MARK CIM TAUM +16F4F ; Extend # Mn MIAO SIGN CONSONANT MODIFIER BAR +16F51..16F87 ; Extend # Mc [55] MIAO SIGN ASPIRATION..MIAO VOWEL SIGN UI +16F8F..16F92 ; Extend # Mn [4] MIAO TONE RIGHT..MIAO TONE BELOW +16FE4 ; Extend # Mn KHITAN SMALL SCRIPT FILLER +16FF0..16FF1 ; Extend # Mc [2] VIETNAMESE ALTERNATE READING MARK CA..VIETNAMESE ALTERNATE READING MARK NHAY +1BC9D..1BC9E ; Extend # Mn [2] DUPLOYAN THICK LETTER SELECTOR..DUPLOYAN DOUBLE MARK +1CF00..1CF2D ; Extend # Mn [46] ZNAMENNY COMBINING MARK GORAZDO NIZKO S KRYZHEM ON LEFT..ZNAMENNY COMBINING MARK KRYZH ON LEFT +1CF30..1CF46 ; Extend # Mn [23] ZNAMENNY COMBINING TONAL RANGE MARK MRACHNO..ZNAMENNY PRIZNAK MODIFIER ROG +1D165..1D166 ; Extend # Mc [2] MUSICAL SYMBOL COMBINING STEM..MUSICAL SYMBOL COMBINING SPRECHGESANG STEM +1D167..1D169 ; Extend # Mn [3] MUSICAL SYMBOL COMBINING TREMOLO-1..MUSICAL SYMBOL COMBINING TREMOLO-3 +1D16D..1D172 ; Extend # Mc [6] MUSICAL SYMBOL COMBINING AUGMENTATION DOT..MUSICAL SYMBOL COMBINING FLAG-5 +1D17B..1D182 ; Extend # Mn [8] MUSICAL SYMBOL COMBINING ACCENT..MUSICAL SYMBOL COMBINING LOURE +1D185..1D18B ; Extend # Mn [7] MUSICAL SYMBOL COMBINING DOIT..MUSICAL SYMBOL COMBINING TRIPLE TONGUE +1D1AA..1D1AD ; Extend # Mn [4] MUSICAL SYMBOL COMBINING DOWN BOW..MUSICAL SYMBOL COMBINING SNAP PIZZICATO +1D242..1D244 ; Extend # Mn [3] COMBINING GREEK MUSICAL TRISEME..COMBINING GREEK MUSICAL PENTASEME +1DA00..1DA36 ; Extend # Mn [55] SIGNWRITING HEAD RIM..SIGNWRITING AIR SUCKING IN +1DA3B..1DA6C ; Extend # Mn [50] SIGNWRITING MOUTH CLOSED NEUTRAL..SIGNWRITING EXCITEMENT +1DA75 ; Extend # Mn SIGNWRITING UPPER BODY TILTING FROM HIP JOINTS +1DA84 ; Extend # Mn SIGNWRITING LOCATION HEAD NECK +1DA9B..1DA9F ; Extend # Mn [5] SIGNWRITING FILL MODIFIER-2..SIGNWRITING FILL MODIFIER-6 +1DAA1..1DAAF ; Extend # Mn [15] SIGNWRITING ROTATION MODIFIER-2..SIGNWRITING ROTATION MODIFIER-16 +1E000..1E006 ; Extend # Mn [7] COMBINING GLAGOLITIC LETTER AZU..COMBINING GLAGOLITIC LETTER ZHIVETE +1E008..1E018 ; Extend # Mn [17] COMBINING GLAGOLITIC LETTER ZEMLJA..COMBINING GLAGOLITIC LETTER HERU +1E01B..1E021 ; Extend # Mn [7] COMBINING GLAGOLITIC LETTER SHTA..COMBINING GLAGOLITIC LETTER YATI +1E023..1E024 ; Extend # Mn [2] COMBINING GLAGOLITIC LETTER YU..COMBINING GLAGOLITIC LETTER SMALL YUS +1E026..1E02A ; Extend # Mn [5] COMBINING GLAGOLITIC LETTER YO..COMBINING GLAGOLITIC LETTER FITA +1E08F ; Extend # Mn COMBINING CYRILLIC SMALL LETTER BYELORUSSIAN-UKRAINIAN I +1E130..1E136 ; Extend # Mn [7] NYIAKENG PUACHUE HMONG TONE-B..NYIAKENG PUACHUE HMONG TONE-D +1E2AE ; Extend # Mn TOTO SIGN RISING TONE +1E2EC..1E2EF ; Extend # Mn [4] WANCHO TONE TUP..WANCHO TONE KOINI +1E4EC..1E4EF ; Extend # Mn [4] NAG MUNDARI SIGN MUHOR..NAG MUNDARI SIGN SUTUH +1E5EE..1E5EF ; Extend # Mn [2] OL ONAL SIGN MU..OL ONAL SIGN IKIR +1E6E3 ; Extend # Mn TAI YO SIGN UE +1E6E6 ; Extend # Mn TAI YO SIGN AU +1E6EE..1E6EF ; Extend # Mn [2] TAI YO SIGN AY..TAI YO SIGN ANG +1E6F5 ; Extend # Mn TAI YO SIGN OM +1E8D0..1E8D6 ; Extend # Mn [7] MENDE KIKAKUI COMBINING NUMBER TEENS..MENDE KIKAKUI COMBINING NUMBER MILLIONS +1E944..1E94A ; Extend # Mn [7] ADLAM ALIF LENGTHENER..ADLAM NUKTA +1F3FB..1F3FF ; Extend # Sk [5] EMOJI MODIFIER FITZPATRICK TYPE-1-2..EMOJI MODIFIER FITZPATRICK TYPE-6 +E0020..E007F ; Extend # Cf [96] TAG SPACE..CANCEL TAG +E0100..E01EF ; Extend # Mn [240] VARIATION SELECTOR-17..VARIATION SELECTOR-256 + +# Total code points: 2647 + +# ================================================ + +1F1E6..1F1FF ; Regional_Indicator # So [26] REGIONAL INDICATOR SYMBOL LETTER A..REGIONAL INDICATOR SYMBOL LETTER Z + +# Total code points: 26 + +# ================================================ + +00AD ; Format # Cf SOFT HYPHEN +061C ; Format # Cf ARABIC LETTER MARK +180E ; Format # Cf MONGOLIAN VOWEL SEPARATOR +200E..200F ; Format # Cf [2] LEFT-TO-RIGHT MARK..RIGHT-TO-LEFT MARK +202A..202E ; Format # Cf [5] LEFT-TO-RIGHT EMBEDDING..RIGHT-TO-LEFT OVERRIDE +2060..2064 ; Format # Cf [5] WORD JOINER..INVISIBLE PLUS +2066..206F ; Format # Cf [10] LEFT-TO-RIGHT ISOLATE..NOMINAL DIGIT SHAPES +FEFF ; Format # Cf ZERO WIDTH NO-BREAK SPACE +FFF9..FFFB ; Format # Cf [3] INTERLINEAR ANNOTATION ANCHOR..INTERLINEAR ANNOTATION TERMINATOR +13430..1343F ; Format # Cf [16] EGYPTIAN HIEROGLYPH VERTICAL JOINER..EGYPTIAN HIEROGLYPH END WALLED ENCLOSURE +1BCA0..1BCA3 ; Format # Cf [4] SHORTHAND FORMAT LETTER OVERLAP..SHORTHAND FORMAT UP STEP +1D173..1D17A ; Format # Cf [8] MUSICAL SYMBOL BEGIN BEAM..MUSICAL SYMBOL END PHRASE +E0001 ; Format # Cf LANGUAGE TAG + +# Total code points: 58 + +# ================================================ + +3031..3035 ; Katakana # Lm [5] VERTICAL KANA REPEAT MARK..VERTICAL KANA REPEAT MARK LOWER HALF +309B..309C ; Katakana # Sk [2] KATAKANA-HIRAGANA VOICED SOUND MARK..KATAKANA-HIRAGANA SEMI-VOICED SOUND MARK +30A0 ; Katakana # Pd KATAKANA-HIRAGANA DOUBLE HYPHEN +30A1..30FA ; Katakana # Lo [90] KATAKANA LETTER SMALL A..KATAKANA LETTER VO +30FC..30FE ; Katakana # Lm [3] KATAKANA-HIRAGANA PROLONGED SOUND MARK..KATAKANA VOICED ITERATION MARK +30FF ; Katakana # Lo KATAKANA DIGRAPH KOTO +31F0..31FF ; Katakana # Lo [16] KATAKANA LETTER SMALL KU..KATAKANA LETTER SMALL RO +32D0..32FE ; Katakana # So [47] CIRCLED KATAKANA A..CIRCLED KATAKANA WO +3300..3357 ; Katakana # So [88] SQUARE APAATO..SQUARE WATTO +FF66..FF6F ; Katakana # Lo [10] HALFWIDTH KATAKANA LETTER WO..HALFWIDTH KATAKANA LETTER SMALL TU +FF70 ; Katakana # Lm HALFWIDTH KATAKANA-HIRAGANA PROLONGED SOUND MARK +FF71..FF9D ; Katakana # Lo [45] HALFWIDTH KATAKANA LETTER A..HALFWIDTH KATAKANA LETTER N +1AFF0..1AFF3 ; Katakana # Lm [4] KATAKANA LETTER MINNAN TONE-2..KATAKANA LETTER MINNAN TONE-5 +1AFF5..1AFFB ; Katakana # Lm [7] KATAKANA LETTER MINNAN TONE-7..KATAKANA LETTER MINNAN NASALIZED TONE-5 +1AFFD..1AFFE ; Katakana # Lm [2] KATAKANA LETTER MINNAN NASALIZED TONE-7..KATAKANA LETTER MINNAN NASALIZED TONE-8 +1B000 ; Katakana # Lo KATAKANA LETTER ARCHAIC E +1B120..1B122 ; Katakana # Lo [3] KATAKANA LETTER ARCHAIC YI..KATAKANA LETTER ARCHAIC WU +1B155 ; Katakana # Lo KATAKANA LETTER SMALL KO +1B164..1B167 ; Katakana # Lo [4] KATAKANA LETTER SMALL WI..KATAKANA LETTER SMALL N + +# Total code points: 331 + +# ================================================ + +0041..005A ; ALetter # L& [26] LATIN CAPITAL LETTER A..LATIN CAPITAL LETTER Z +0061..007A ; ALetter # L& [26] LATIN SMALL LETTER A..LATIN SMALL LETTER Z +00AA ; ALetter # Lo FEMININE ORDINAL INDICATOR +00B5 ; ALetter # L& MICRO SIGN +00B8 ; ALetter # Sk CEDILLA +00BA ; ALetter # Lo MASCULINE ORDINAL INDICATOR +00C0..00D6 ; ALetter # L& [23] LATIN CAPITAL LETTER A WITH GRAVE..LATIN CAPITAL LETTER O WITH DIAERESIS +00D8..00F6 ; ALetter # L& [31] LATIN CAPITAL LETTER O WITH STROKE..LATIN SMALL LETTER O WITH DIAERESIS +00F8..01BA ; ALetter # L& [195] LATIN SMALL LETTER O WITH STROKE..LATIN SMALL LETTER EZH WITH TAIL +01BB ; ALetter # Lo LATIN LETTER TWO WITH STROKE +01BC..01BF ; ALetter # L& [4] LATIN CAPITAL LETTER TONE FIVE..LATIN LETTER WYNN +01C0..01C3 ; ALetter # Lo [4] LATIN LETTER DENTAL CLICK..LATIN LETTER RETROFLEX CLICK +01C4..0293 ; ALetter # L& [208] LATIN CAPITAL LETTER DZ WITH CARON..LATIN SMALL LETTER EZH WITH CURL +0294..0295 ; ALetter # Lo [2] LATIN LETTER GLOTTAL STOP..LATIN LETTER PHARYNGEAL VOICED FRICATIVE +0296..02AF ; ALetter # L& [26] LATIN LETTER INVERTED GLOTTAL STOP..LATIN SMALL LETTER TURNED H WITH FISHHOOK AND TAIL +02B0..02C1 ; ALetter # Lm [18] MODIFIER LETTER SMALL H..MODIFIER LETTER REVERSED GLOTTAL STOP +02C2..02C5 ; ALetter # Sk [4] MODIFIER LETTER LEFT ARROWHEAD..MODIFIER LETTER DOWN ARROWHEAD +02C6..02D1 ; ALetter # Lm [12] MODIFIER LETTER CIRCUMFLEX ACCENT..MODIFIER LETTER HALF TRIANGULAR COLON +02D2..02D7 ; ALetter # Sk [6] MODIFIER LETTER CENTRED RIGHT HALF RING..MODIFIER LETTER MINUS SIGN +02DE..02DF ; ALetter # Sk [2] MODIFIER LETTER RHOTIC HOOK..MODIFIER LETTER CROSS ACCENT +02E0..02E4 ; ALetter # Lm [5] MODIFIER LETTER SMALL GAMMA..MODIFIER LETTER SMALL REVERSED GLOTTAL STOP +02E5..02EB ; ALetter # Sk [7] MODIFIER LETTER EXTRA-HIGH TONE BAR..MODIFIER LETTER YANG DEPARTING TONE MARK +02EC ; ALetter # Lm MODIFIER LETTER VOICING +02ED ; ALetter # Sk MODIFIER LETTER UNASPIRATED +02EE ; ALetter # Lm MODIFIER LETTER DOUBLE APOSTROPHE +02EF..02FF ; ALetter # Sk [17] MODIFIER LETTER LOW DOWN ARROWHEAD..MODIFIER LETTER LOW LEFT ARROW +0370..0373 ; ALetter # L& [4] GREEK CAPITAL LETTER HETA..GREEK SMALL LETTER ARCHAIC SAMPI +0374 ; ALetter # Lm GREEK NUMERAL SIGN +0376..0377 ; ALetter # L& [2] GREEK CAPITAL LETTER PAMPHYLIAN DIGAMMA..GREEK SMALL LETTER PAMPHYLIAN DIGAMMA +037A ; ALetter # Lm GREEK YPOGEGRAMMENI +037B..037D ; ALetter # L& [3] GREEK SMALL REVERSED LUNATE SIGMA SYMBOL..GREEK SMALL REVERSED DOTTED LUNATE SIGMA SYMBOL +037F ; ALetter # L& GREEK CAPITAL LETTER YOT +0386 ; ALetter # L& GREEK CAPITAL LETTER ALPHA WITH TONOS +0388..038A ; ALetter # L& [3] GREEK CAPITAL LETTER EPSILON WITH TONOS..GREEK CAPITAL LETTER IOTA WITH TONOS +038C ; ALetter # L& GREEK CAPITAL LETTER OMICRON WITH TONOS +038E..03A1 ; ALetter # L& [20] GREEK CAPITAL LETTER UPSILON WITH TONOS..GREEK CAPITAL LETTER RHO +03A3..03F5 ; ALetter # L& [83] GREEK CAPITAL LETTER SIGMA..GREEK LUNATE EPSILON SYMBOL +03F7..0481 ; ALetter # L& [139] GREEK CAPITAL LETTER SHO..CYRILLIC SMALL LETTER KOPPA +048A..052F ; ALetter # L& [166] CYRILLIC CAPITAL LETTER SHORT I WITH TAIL..CYRILLIC SMALL LETTER EL WITH DESCENDER +0531..0556 ; ALetter # L& [38] ARMENIAN CAPITAL LETTER AYB..ARMENIAN CAPITAL LETTER FEH +0559 ; ALetter # Lm ARMENIAN MODIFIER LETTER LEFT HALF RING +055A..055C ; ALetter # Po [3] ARMENIAN APOSTROPHE..ARMENIAN EXCLAMATION MARK +055E ; ALetter # Po ARMENIAN QUESTION MARK +0560..0588 ; ALetter # L& [41] ARMENIAN SMALL LETTER TURNED AYB..ARMENIAN SMALL LETTER YI WITH STROKE +058A ; ALetter # Pd ARMENIAN HYPHEN +05F3 ; ALetter # Po HEBREW PUNCTUATION GERESH +0620..063F ; ALetter # Lo [32] ARABIC LETTER KASHMIRI YEH..ARABIC LETTER FARSI YEH WITH THREE DOTS ABOVE +0640 ; ALetter # Lm ARABIC TATWEEL +0641..064A ; ALetter # Lo [10] ARABIC LETTER FEH..ARABIC LETTER YEH +066E..066F ; ALetter # Lo [2] ARABIC LETTER DOTLESS BEH..ARABIC LETTER DOTLESS QAF +0671..06D3 ; ALetter # Lo [99] ARABIC LETTER ALEF WASLA..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE +06D5 ; ALetter # Lo ARABIC LETTER AE +06E5..06E6 ; ALetter # Lm [2] ARABIC SMALL WAW..ARABIC SMALL YEH +06EE..06EF ; ALetter # Lo [2] ARABIC LETTER DAL WITH INVERTED V..ARABIC LETTER REH WITH INVERTED V +06FA..06FC ; ALetter # Lo [3] ARABIC LETTER SHEEN WITH DOT BELOW..ARABIC LETTER GHAIN WITH DOT BELOW +06FF ; ALetter # Lo ARABIC LETTER HEH WITH INVERTED V +070F ; ALetter # Cf SYRIAC ABBREVIATION MARK +0710 ; ALetter # Lo SYRIAC LETTER ALAPH +0712..072F ; ALetter # Lo [30] SYRIAC LETTER BETH..SYRIAC LETTER PERSIAN DHALATH +074D..07A5 ; ALetter # Lo [89] SYRIAC LETTER SOGDIAN ZHAIN..THAANA LETTER WAAVU +07B1 ; ALetter # Lo THAANA LETTER NAA +07CA..07EA ; ALetter # Lo [33] NKO LETTER A..NKO LETTER JONA RA +07F4..07F5 ; ALetter # Lm [2] NKO HIGH TONE APOSTROPHE..NKO LOW TONE APOSTROPHE +07FA ; ALetter # Lm NKO LAJANYALAN +0800..0815 ; ALetter # Lo [22] SAMARITAN LETTER ALAF..SAMARITAN LETTER TAAF +081A ; ALetter # Lm SAMARITAN MODIFIER LETTER EPENTHETIC YUT +0824 ; ALetter # Lm SAMARITAN MODIFIER LETTER SHORT A +0828 ; ALetter # Lm SAMARITAN MODIFIER LETTER I +0840..0858 ; ALetter # Lo [25] MANDAIC LETTER HALQA..MANDAIC LETTER AIN +0860..086A ; ALetter # Lo [11] SYRIAC LETTER MALAYALAM NGA..SYRIAC LETTER MALAYALAM SSA +0870..0887 ; ALetter # Lo [24] ARABIC LETTER ALEF WITH ATTACHED FATHA..ARABIC BASELINE ROUND DOT +0889..088F ; ALetter # Lo [7] ARABIC LETTER NOON WITH INVERTED SMALL V..ARABIC LETTER NOON WITH RING ABOVE +08A0..08C8 ; ALetter # Lo [41] ARABIC LETTER BEH WITH SMALL V BELOW..ARABIC LETTER GRAF +08C9 ; ALetter # Lm ARABIC SMALL FARSI YEH +0904..0939 ; ALetter # Lo [54] DEVANAGARI LETTER SHORT A..DEVANAGARI LETTER HA +093D ; ALetter # Lo DEVANAGARI SIGN AVAGRAHA +0950 ; ALetter # Lo DEVANAGARI OM +0958..0961 ; ALetter # Lo [10] DEVANAGARI LETTER QA..DEVANAGARI LETTER VOCALIC LL +0971 ; ALetter # Lm DEVANAGARI SIGN HIGH SPACING DOT +0972..0980 ; ALetter # Lo [15] DEVANAGARI LETTER CANDRA A..BENGALI ANJI +0985..098C ; ALetter # Lo [8] BENGALI LETTER A..BENGALI LETTER VOCALIC L +098F..0990 ; ALetter # Lo [2] BENGALI LETTER E..BENGALI LETTER AI +0993..09A8 ; ALetter # Lo [22] BENGALI LETTER O..BENGALI LETTER NA +09AA..09B0 ; ALetter # Lo [7] BENGALI LETTER PA..BENGALI LETTER RA +09B2 ; ALetter # Lo BENGALI LETTER LA +09B6..09B9 ; ALetter # Lo [4] BENGALI LETTER SHA..BENGALI LETTER HA +09BD ; ALetter # Lo BENGALI SIGN AVAGRAHA +09CE ; ALetter # Lo BENGALI LETTER KHANDA TA +09DC..09DD ; ALetter # Lo [2] BENGALI LETTER RRA..BENGALI LETTER RHA +09DF..09E1 ; ALetter # Lo [3] BENGALI LETTER YYA..BENGALI LETTER VOCALIC LL +09F0..09F1 ; ALetter # Lo [2] BENGALI LETTER RA WITH MIDDLE DIAGONAL..BENGALI LETTER RA WITH LOWER DIAGONAL +09FC ; ALetter # Lo BENGALI LETTER VEDIC ANUSVARA +0A05..0A0A ; ALetter # Lo [6] GURMUKHI LETTER A..GURMUKHI LETTER UU +0A0F..0A10 ; ALetter # Lo [2] GURMUKHI LETTER EE..GURMUKHI LETTER AI +0A13..0A28 ; ALetter # Lo [22] GURMUKHI LETTER OO..GURMUKHI LETTER NA +0A2A..0A30 ; ALetter # Lo [7] GURMUKHI LETTER PA..GURMUKHI LETTER RA +0A32..0A33 ; ALetter # Lo [2] GURMUKHI LETTER LA..GURMUKHI LETTER LLA +0A35..0A36 ; ALetter # Lo [2] GURMUKHI LETTER VA..GURMUKHI LETTER SHA +0A38..0A39 ; ALetter # Lo [2] GURMUKHI LETTER SA..GURMUKHI LETTER HA +0A59..0A5C ; ALetter # Lo [4] GURMUKHI LETTER KHHA..GURMUKHI LETTER RRA +0A5E ; ALetter # Lo GURMUKHI LETTER FA +0A72..0A74 ; ALetter # Lo [3] GURMUKHI IRI..GURMUKHI EK ONKAR +0A85..0A8D ; ALetter # Lo [9] GUJARATI LETTER A..GUJARATI VOWEL CANDRA E +0A8F..0A91 ; ALetter # Lo [3] GUJARATI LETTER E..GUJARATI VOWEL CANDRA O +0A93..0AA8 ; ALetter # Lo [22] GUJARATI LETTER O..GUJARATI LETTER NA +0AAA..0AB0 ; ALetter # Lo [7] GUJARATI LETTER PA..GUJARATI LETTER RA +0AB2..0AB3 ; ALetter # Lo [2] GUJARATI LETTER LA..GUJARATI LETTER LLA +0AB5..0AB9 ; ALetter # Lo [5] GUJARATI LETTER VA..GUJARATI LETTER HA +0ABD ; ALetter # Lo GUJARATI SIGN AVAGRAHA +0AD0 ; ALetter # Lo GUJARATI OM +0AE0..0AE1 ; ALetter # Lo [2] GUJARATI LETTER VOCALIC RR..GUJARATI LETTER VOCALIC LL +0AF9 ; ALetter # Lo GUJARATI LETTER ZHA +0B05..0B0C ; ALetter # Lo [8] ORIYA LETTER A..ORIYA LETTER VOCALIC L +0B0F..0B10 ; ALetter # Lo [2] ORIYA LETTER E..ORIYA LETTER AI +0B13..0B28 ; ALetter # Lo [22] ORIYA LETTER O..ORIYA LETTER NA +0B2A..0B30 ; ALetter # Lo [7] ORIYA LETTER PA..ORIYA LETTER RA +0B32..0B33 ; ALetter # Lo [2] ORIYA LETTER LA..ORIYA LETTER LLA +0B35..0B39 ; ALetter # Lo [5] ORIYA LETTER VA..ORIYA LETTER HA +0B3D ; ALetter # Lo ORIYA SIGN AVAGRAHA +0B5C..0B5D ; ALetter # Lo [2] ORIYA LETTER RRA..ORIYA LETTER RHA +0B5F..0B61 ; ALetter # Lo [3] ORIYA LETTER YYA..ORIYA LETTER VOCALIC LL +0B71 ; ALetter # Lo ORIYA LETTER WA +0B83 ; ALetter # Lo TAMIL SIGN VISARGA +0B85..0B8A ; ALetter # Lo [6] TAMIL LETTER A..TAMIL LETTER UU +0B8E..0B90 ; ALetter # Lo [3] TAMIL LETTER E..TAMIL LETTER AI +0B92..0B95 ; ALetter # Lo [4] TAMIL LETTER O..TAMIL LETTER KA +0B99..0B9A ; ALetter # Lo [2] TAMIL LETTER NGA..TAMIL LETTER CA +0B9C ; ALetter # Lo TAMIL LETTER JA +0B9E..0B9F ; ALetter # Lo [2] TAMIL LETTER NYA..TAMIL LETTER TTA +0BA3..0BA4 ; ALetter # Lo [2] TAMIL LETTER NNA..TAMIL LETTER TA +0BA8..0BAA ; ALetter # Lo [3] TAMIL LETTER NA..TAMIL LETTER PA +0BAE..0BB9 ; ALetter # Lo [12] TAMIL LETTER MA..TAMIL LETTER HA +0BD0 ; ALetter # Lo TAMIL OM +0C05..0C0C ; ALetter # Lo [8] TELUGU LETTER A..TELUGU LETTER VOCALIC L +0C0E..0C10 ; ALetter # Lo [3] TELUGU LETTER E..TELUGU LETTER AI +0C12..0C28 ; ALetter # Lo [23] TELUGU LETTER O..TELUGU LETTER NA +0C2A..0C39 ; ALetter # Lo [16] TELUGU LETTER PA..TELUGU LETTER HA +0C3D ; ALetter # Lo TELUGU SIGN AVAGRAHA +0C58..0C5A ; ALetter # Lo [3] TELUGU LETTER TSA..TELUGU LETTER RRRA +0C5C..0C5D ; ALetter # Lo [2] TELUGU ARCHAIC SHRII..TELUGU LETTER NAKAARA POLLU +0C60..0C61 ; ALetter # Lo [2] TELUGU LETTER VOCALIC RR..TELUGU LETTER VOCALIC LL +0C80 ; ALetter # Lo KANNADA SIGN SPACING CANDRABINDU +0C85..0C8C ; ALetter # Lo [8] KANNADA LETTER A..KANNADA LETTER VOCALIC L +0C8E..0C90 ; ALetter # Lo [3] KANNADA LETTER E..KANNADA LETTER AI +0C92..0CA8 ; ALetter # Lo [23] KANNADA LETTER O..KANNADA LETTER NA +0CAA..0CB3 ; ALetter # Lo [10] KANNADA LETTER PA..KANNADA LETTER LLA +0CB5..0CB9 ; ALetter # Lo [5] KANNADA LETTER VA..KANNADA LETTER HA +0CBD ; ALetter # Lo KANNADA SIGN AVAGRAHA +0CDC..0CDE ; ALetter # Lo [3] KANNADA ARCHAIC SHRII..KANNADA LETTER FA +0CE0..0CE1 ; ALetter # Lo [2] KANNADA LETTER VOCALIC RR..KANNADA LETTER VOCALIC LL +0CF1..0CF2 ; ALetter # Lo [2] KANNADA SIGN JIHVAMULIYA..KANNADA SIGN UPADHMANIYA +0D04..0D0C ; ALetter # Lo [9] MALAYALAM LETTER VEDIC ANUSVARA..MALAYALAM LETTER VOCALIC L +0D0E..0D10 ; ALetter # Lo [3] MALAYALAM LETTER E..MALAYALAM LETTER AI +0D12..0D3A ; ALetter # Lo [41] MALAYALAM LETTER O..MALAYALAM LETTER TTTA +0D3D ; ALetter # Lo MALAYALAM SIGN AVAGRAHA +0D4E ; ALetter # Lo MALAYALAM LETTER DOT REPH +0D54..0D56 ; ALetter # Lo [3] MALAYALAM LETTER CHILLU M..MALAYALAM LETTER CHILLU LLL +0D5F..0D61 ; ALetter # Lo [3] MALAYALAM LETTER ARCHAIC II..MALAYALAM LETTER VOCALIC LL +0D7A..0D7F ; ALetter # Lo [6] MALAYALAM LETTER CHILLU NN..MALAYALAM LETTER CHILLU K +0D85..0D96 ; ALetter # Lo [18] SINHALA LETTER AYANNA..SINHALA LETTER AUYANNA +0D9A..0DB1 ; ALetter # Lo [24] SINHALA LETTER ALPAPRAANA KAYANNA..SINHALA LETTER DANTAJA NAYANNA +0DB3..0DBB ; ALetter # Lo [9] SINHALA LETTER SANYAKA DAYANNA..SINHALA LETTER RAYANNA +0DBD ; ALetter # Lo SINHALA LETTER DANTAJA LAYANNA +0DC0..0DC6 ; ALetter # Lo [7] SINHALA LETTER VAYANNA..SINHALA LETTER FAYANNA +0F00 ; ALetter # Lo TIBETAN SYLLABLE OM +0F40..0F47 ; ALetter # Lo [8] TIBETAN LETTER KA..TIBETAN LETTER JA +0F49..0F6C ; ALetter # Lo [36] TIBETAN LETTER NYA..TIBETAN LETTER RRA +0F88..0F8C ; ALetter # Lo [5] TIBETAN SIGN LCE TSA CAN..TIBETAN SIGN INVERTED MCHU CAN +10A0..10C5 ; ALetter # L& [38] GEORGIAN CAPITAL LETTER AN..GEORGIAN CAPITAL LETTER HOE +10C7 ; ALetter # L& GEORGIAN CAPITAL LETTER YN +10CD ; ALetter # L& GEORGIAN CAPITAL LETTER AEN +10D0..10FA ; ALetter # L& [43] GEORGIAN LETTER AN..GEORGIAN LETTER AIN +10FC ; ALetter # Lm MODIFIER LETTER GEORGIAN NAR +10FD..10FF ; ALetter # L& [3] GEORGIAN LETTER AEN..GEORGIAN LETTER LABIAL SIGN +1100..1248 ; ALetter # Lo [329] HANGUL CHOSEONG KIYEOK..ETHIOPIC SYLLABLE QWA +124A..124D ; ALetter # Lo [4] ETHIOPIC SYLLABLE QWI..ETHIOPIC SYLLABLE QWE +1250..1256 ; ALetter # Lo [7] ETHIOPIC SYLLABLE QHA..ETHIOPIC SYLLABLE QHO +1258 ; ALetter # Lo ETHIOPIC SYLLABLE QHWA +125A..125D ; ALetter # Lo [4] ETHIOPIC SYLLABLE QHWI..ETHIOPIC SYLLABLE QHWE +1260..1288 ; ALetter # Lo [41] ETHIOPIC SYLLABLE BA..ETHIOPIC SYLLABLE XWA +128A..128D ; ALetter # Lo [4] ETHIOPIC SYLLABLE XWI..ETHIOPIC SYLLABLE XWE +1290..12B0 ; ALetter # Lo [33] ETHIOPIC SYLLABLE NA..ETHIOPIC SYLLABLE KWA +12B2..12B5 ; ALetter # Lo [4] ETHIOPIC SYLLABLE KWI..ETHIOPIC SYLLABLE KWE +12B8..12BE ; ALetter # Lo [7] ETHIOPIC SYLLABLE KXA..ETHIOPIC SYLLABLE KXO +12C0 ; ALetter # Lo ETHIOPIC SYLLABLE KXWA +12C2..12C5 ; ALetter # Lo [4] ETHIOPIC SYLLABLE KXWI..ETHIOPIC SYLLABLE KXWE +12C8..12D6 ; ALetter # Lo [15] ETHIOPIC SYLLABLE WA..ETHIOPIC SYLLABLE PHARYNGEAL O +12D8..1310 ; ALetter # Lo [57] ETHIOPIC SYLLABLE ZA..ETHIOPIC SYLLABLE GWA +1312..1315 ; ALetter # Lo [4] ETHIOPIC SYLLABLE GWI..ETHIOPIC SYLLABLE GWE +1318..135A ; ALetter # Lo [67] ETHIOPIC SYLLABLE GGA..ETHIOPIC SYLLABLE FYA +1380..138F ; ALetter # Lo [16] ETHIOPIC SYLLABLE SEBATBEIT MWA..ETHIOPIC SYLLABLE PWE +13A0..13F5 ; ALetter # L& [86] CHEROKEE LETTER A..CHEROKEE LETTER MV +13F8..13FD ; ALetter # L& [6] CHEROKEE SMALL LETTER YE..CHEROKEE SMALL LETTER MV +1401..166C ; ALetter # Lo [620] CANADIAN SYLLABICS E..CANADIAN SYLLABICS CARRIER TTSA +166F..167F ; ALetter # Lo [17] CANADIAN SYLLABICS QAI..CANADIAN SYLLABICS BLACKFOOT W +1681..169A ; ALetter # Lo [26] OGHAM LETTER BEITH..OGHAM LETTER PEITH +16A0..16EA ; ALetter # Lo [75] RUNIC LETTER FEHU FEOH FE F..RUNIC LETTER X +16EE..16F0 ; ALetter # Nl [3] RUNIC ARLAUG SYMBOL..RUNIC BELGTHOR SYMBOL +16F1..16F8 ; ALetter # Lo [8] RUNIC LETTER K..RUNIC LETTER FRANKS CASKET AESC +1700..1711 ; ALetter # Lo [18] TAGALOG LETTER A..TAGALOG LETTER HA +171F..1731 ; ALetter # Lo [19] TAGALOG LETTER ARCHAIC RA..HANUNOO LETTER HA +1740..1751 ; ALetter # Lo [18] BUHID LETTER A..BUHID LETTER HA +1760..176C ; ALetter # Lo [13] TAGBANWA LETTER A..TAGBANWA LETTER YA +176E..1770 ; ALetter # Lo [3] TAGBANWA LETTER LA..TAGBANWA LETTER SA +1820..1842 ; ALetter # Lo [35] MONGOLIAN LETTER A..MONGOLIAN LETTER CHI +1843 ; ALetter # Lm MONGOLIAN LETTER TODO LONG VOWEL SIGN +1844..1878 ; ALetter # Lo [53] MONGOLIAN LETTER TODO E..MONGOLIAN LETTER CHA WITH TWO DOTS +1880..1884 ; ALetter # Lo [5] MONGOLIAN LETTER ALI GALI ANUSVARA ONE..MONGOLIAN LETTER ALI GALI INVERTED UBADAMA +1887..18A8 ; ALetter # Lo [34] MONGOLIAN LETTER ALI GALI A..MONGOLIAN LETTER MANCHU ALI GALI BHA +18AA ; ALetter # Lo MONGOLIAN LETTER MANCHU ALI GALI LHA +18B0..18F5 ; ALetter # Lo [70] CANADIAN SYLLABICS OY..CANADIAN SYLLABICS CARRIER DENTAL S +1900..191E ; ALetter # Lo [31] LIMBU VOWEL-CARRIER LETTER..LIMBU LETTER TRA +1A00..1A16 ; ALetter # Lo [23] BUGINESE LETTER KA..BUGINESE LETTER HA +1B05..1B33 ; ALetter # Lo [47] BALINESE LETTER AKARA..BALINESE LETTER HA +1B45..1B4C ; ALetter # Lo [8] BALINESE LETTER KAF SASAK..BALINESE LETTER ARCHAIC JNYA +1B83..1BA0 ; ALetter # Lo [30] SUNDANESE LETTER A..SUNDANESE LETTER HA +1BAE..1BAF ; ALetter # Lo [2] SUNDANESE LETTER KHA..SUNDANESE LETTER SYA +1BBA..1BE5 ; ALetter # Lo [44] SUNDANESE AVAGRAHA..BATAK LETTER U +1C00..1C23 ; ALetter # Lo [36] LEPCHA LETTER KA..LEPCHA LETTER A +1C4D..1C4F ; ALetter # Lo [3] LEPCHA LETTER TTA..LEPCHA LETTER DDA +1C5A..1C77 ; ALetter # Lo [30] OL CHIKI LETTER LA..OL CHIKI LETTER OH +1C78..1C7D ; ALetter # Lm [6] OL CHIKI MU TTUDDAG..OL CHIKI AHAD +1C80..1C8A ; ALetter # L& [11] CYRILLIC SMALL LETTER ROUNDED VE..CYRILLIC SMALL LETTER TJE +1C90..1CBA ; ALetter # L& [43] GEORGIAN MTAVRULI CAPITAL LETTER AN..GEORGIAN MTAVRULI CAPITAL LETTER AIN +1CBD..1CBF ; ALetter # L& [3] GEORGIAN MTAVRULI CAPITAL LETTER AEN..GEORGIAN MTAVRULI CAPITAL LETTER LABIAL SIGN +1CE9..1CEC ; ALetter # Lo [4] VEDIC SIGN ANUSVARA ANTARGOMUKHA..VEDIC SIGN ANUSVARA VAMAGOMUKHA WITH TAIL +1CEE..1CF3 ; ALetter # Lo [6] VEDIC SIGN HEXIFORM LONG ANUSVARA..VEDIC SIGN ROTATED ARDHAVISARGA +1CF5..1CF6 ; ALetter # Lo [2] VEDIC SIGN JIHVAMULIYA..VEDIC SIGN UPADHMANIYA +1CFA ; ALetter # Lo VEDIC SIGN DOUBLE ANUSVARA ANTARGOMUKHA +1D00..1D2B ; ALetter # L& [44] LATIN LETTER SMALL CAPITAL A..CYRILLIC LETTER SMALL CAPITAL EL +1D2C..1D6A ; ALetter # Lm [63] MODIFIER LETTER CAPITAL A..GREEK SUBSCRIPT SMALL LETTER CHI +1D6B..1D77 ; ALetter # L& [13] LATIN SMALL LETTER UE..LATIN SMALL LETTER TURNED G +1D78 ; ALetter # Lm MODIFIER LETTER CYRILLIC EN +1D79..1D9A ; ALetter # L& [34] LATIN SMALL LETTER INSULAR G..LATIN SMALL LETTER EZH WITH RETROFLEX HOOK +1D9B..1DBF ; ALetter # Lm [37] MODIFIER LETTER SMALL TURNED ALPHA..MODIFIER LETTER SMALL THETA +1E00..1F15 ; ALetter # L& [278] LATIN CAPITAL LETTER A WITH RING BELOW..GREEK SMALL LETTER EPSILON WITH DASIA AND OXIA +1F18..1F1D ; ALetter # L& [6] GREEK CAPITAL LETTER EPSILON WITH PSILI..GREEK CAPITAL LETTER EPSILON WITH DASIA AND OXIA +1F20..1F45 ; ALetter # L& [38] GREEK SMALL LETTER ETA WITH PSILI..GREEK SMALL LETTER OMICRON WITH DASIA AND OXIA +1F48..1F4D ; ALetter # L& [6] GREEK CAPITAL LETTER OMICRON WITH PSILI..GREEK CAPITAL LETTER OMICRON WITH DASIA AND OXIA +1F50..1F57 ; ALetter # L& [8] GREEK SMALL LETTER UPSILON WITH PSILI..GREEK SMALL LETTER UPSILON WITH DASIA AND PERISPOMENI +1F59 ; ALetter # L& GREEK CAPITAL LETTER UPSILON WITH DASIA +1F5B ; ALetter # L& GREEK CAPITAL LETTER UPSILON WITH DASIA AND VARIA +1F5D ; ALetter # L& GREEK CAPITAL LETTER UPSILON WITH DASIA AND OXIA +1F5F..1F7D ; ALetter # L& [31] GREEK CAPITAL LETTER UPSILON WITH DASIA AND PERISPOMENI..GREEK SMALL LETTER OMEGA WITH OXIA +1F80..1FB4 ; ALetter # L& [53] GREEK SMALL LETTER ALPHA WITH PSILI AND YPOGEGRAMMENI..GREEK SMALL LETTER ALPHA WITH OXIA AND YPOGEGRAMMENI +1FB6..1FBC ; ALetter # L& [7] GREEK SMALL LETTER ALPHA WITH PERISPOMENI..GREEK CAPITAL LETTER ALPHA WITH PROSGEGRAMMENI +1FBE ; ALetter # L& GREEK PROSGEGRAMMENI +1FC2..1FC4 ; ALetter # L& [3] GREEK SMALL LETTER ETA WITH VARIA AND YPOGEGRAMMENI..GREEK SMALL LETTER ETA WITH OXIA AND YPOGEGRAMMENI +1FC6..1FCC ; ALetter # L& [7] GREEK SMALL LETTER ETA WITH PERISPOMENI..GREEK CAPITAL LETTER ETA WITH PROSGEGRAMMENI +1FD0..1FD3 ; ALetter # L& [4] GREEK SMALL LETTER IOTA WITH VRACHY..GREEK SMALL LETTER IOTA WITH DIALYTIKA AND OXIA +1FD6..1FDB ; ALetter # L& [6] GREEK SMALL LETTER IOTA WITH PERISPOMENI..GREEK CAPITAL LETTER IOTA WITH OXIA +1FE0..1FEC ; ALetter # L& [13] GREEK SMALL LETTER UPSILON WITH VRACHY..GREEK CAPITAL LETTER RHO WITH DASIA +1FF2..1FF4 ; ALetter # L& [3] GREEK SMALL LETTER OMEGA WITH VARIA AND YPOGEGRAMMENI..GREEK SMALL LETTER OMEGA WITH OXIA AND YPOGEGRAMMENI +1FF6..1FFC ; ALetter # L& [7] GREEK SMALL LETTER OMEGA WITH PERISPOMENI..GREEK CAPITAL LETTER OMEGA WITH PROSGEGRAMMENI +2071 ; ALetter # Lm SUPERSCRIPT LATIN SMALL LETTER I +207F ; ALetter # Lm SUPERSCRIPT LATIN SMALL LETTER N +2090..209C ; ALetter # Lm [13] LATIN SUBSCRIPT SMALL LETTER A..LATIN SUBSCRIPT SMALL LETTER T +2102 ; ALetter # L& DOUBLE-STRUCK CAPITAL C +2107 ; ALetter # L& EULER CONSTANT +210A..2113 ; ALetter # L& [10] SCRIPT SMALL G..SCRIPT SMALL L +2115 ; ALetter # L& DOUBLE-STRUCK CAPITAL N +2119..211D ; ALetter # L& [5] DOUBLE-STRUCK CAPITAL P..DOUBLE-STRUCK CAPITAL R +2124 ; ALetter # L& DOUBLE-STRUCK CAPITAL Z +2126 ; ALetter # L& OHM SIGN +2128 ; ALetter # L& BLACK-LETTER CAPITAL Z +212A..212D ; ALetter # L& [4] KELVIN SIGN..BLACK-LETTER CAPITAL C +212F..2134 ; ALetter # L& [6] SCRIPT SMALL E..SCRIPT SMALL O +2135..2138 ; ALetter # Lo [4] ALEF SYMBOL..DALET SYMBOL +2139 ; ALetter # L& INFORMATION SOURCE +213C..213F ; ALetter # L& [4] DOUBLE-STRUCK SMALL PI..DOUBLE-STRUCK CAPITAL PI +2145..2149 ; ALetter # L& [5] DOUBLE-STRUCK ITALIC CAPITAL D..DOUBLE-STRUCK ITALIC SMALL J +214E ; ALetter # L& TURNED SMALL F +2160..2182 ; ALetter # Nl [35] ROMAN NUMERAL ONE..ROMAN NUMERAL TEN THOUSAND +2183..2184 ; ALetter # L& [2] ROMAN NUMERAL REVERSED ONE HUNDRED..LATIN SMALL LETTER REVERSED C +2185..2188 ; ALetter # Nl [4] ROMAN NUMERAL SIX LATE FORM..ROMAN NUMERAL ONE HUNDRED THOUSAND +24B6..24E9 ; ALetter # So [52] CIRCLED LATIN CAPITAL LETTER A..CIRCLED LATIN SMALL LETTER Z +2C00..2C7B ; ALetter # L& [124] GLAGOLITIC CAPITAL LETTER AZU..LATIN LETTER SMALL CAPITAL TURNED E +2C7C..2C7D ; ALetter # Lm [2] LATIN SUBSCRIPT SMALL LETTER J..MODIFIER LETTER CAPITAL V +2C7E..2CE4 ; ALetter # L& [103] LATIN CAPITAL LETTER S WITH SWASH TAIL..COPTIC SYMBOL KAI +2CEB..2CEE ; ALetter # L& [4] COPTIC CAPITAL LETTER CRYPTOGRAMMIC SHEI..COPTIC SMALL LETTER CRYPTOGRAMMIC GANGIA +2CF2..2CF3 ; ALetter # L& [2] COPTIC CAPITAL LETTER BOHAIRIC KHEI..COPTIC SMALL LETTER BOHAIRIC KHEI +2D00..2D25 ; ALetter # L& [38] GEORGIAN SMALL LETTER AN..GEORGIAN SMALL LETTER HOE +2D27 ; ALetter # L& GEORGIAN SMALL LETTER YN +2D2D ; ALetter # L& GEORGIAN SMALL LETTER AEN +2D30..2D67 ; ALetter # Lo [56] TIFINAGH LETTER YA..TIFINAGH LETTER YO +2D6F ; ALetter # Lm TIFINAGH MODIFIER LETTER LABIALIZATION MARK +2D80..2D96 ; ALetter # Lo [23] ETHIOPIC SYLLABLE LOA..ETHIOPIC SYLLABLE GGWE +2DA0..2DA6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE SSA..ETHIOPIC SYLLABLE SSO +2DA8..2DAE ; ALetter # Lo [7] ETHIOPIC SYLLABLE CCA..ETHIOPIC SYLLABLE CCO +2DB0..2DB6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE ZZA..ETHIOPIC SYLLABLE ZZO +2DB8..2DBE ; ALetter # Lo [7] ETHIOPIC SYLLABLE CCHA..ETHIOPIC SYLLABLE CCHO +2DC0..2DC6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE QYA..ETHIOPIC SYLLABLE QYO +2DC8..2DCE ; ALetter # Lo [7] ETHIOPIC SYLLABLE KYA..ETHIOPIC SYLLABLE KYO +2DD0..2DD6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE XYA..ETHIOPIC SYLLABLE XYO +2DD8..2DDE ; ALetter # Lo [7] ETHIOPIC SYLLABLE GYA..ETHIOPIC SYLLABLE GYO +2E2F ; ALetter # Lm VERTICAL TILDE +3005 ; ALetter # Lm IDEOGRAPHIC ITERATION MARK +303B ; ALetter # Lm VERTICAL IDEOGRAPHIC ITERATION MARK +303C ; ALetter # Lo MASU MARK +3105..312F ; ALetter # Lo [43] BOPOMOFO LETTER B..BOPOMOFO LETTER NN +3131..318E ; ALetter # Lo [94] HANGUL LETTER KIYEOK..HANGUL LETTER ARAEAE +31A0..31BF ; ALetter # Lo [32] BOPOMOFO LETTER BU..BOPOMOFO LETTER AH +A000..A014 ; ALetter # Lo [21] YI SYLLABLE IT..YI SYLLABLE E +A015 ; ALetter # Lm YI SYLLABLE WU +A016..A48C ; ALetter # Lo [1143] YI SYLLABLE BIT..YI SYLLABLE YYR +A4D0..A4F7 ; ALetter # Lo [40] LISU LETTER BA..LISU LETTER OE +A4F8..A4FD ; ALetter # Lm [6] LISU LETTER TONE MYA TI..LISU LETTER TONE MYA JEU +A500..A60B ; ALetter # Lo [268] VAI SYLLABLE EE..VAI SYLLABLE NG +A60C ; ALetter # Lm VAI SYLLABLE LENGTHENER +A610..A61F ; ALetter # Lo [16] VAI SYLLABLE NDOLE FA..VAI SYMBOL JONG +A62A..A62B ; ALetter # Lo [2] VAI SYLLABLE NDOLE MA..VAI SYLLABLE NDOLE DO +A640..A66D ; ALetter # L& [46] CYRILLIC CAPITAL LETTER ZEMLYA..CYRILLIC SMALL LETTER DOUBLE MONOCULAR O +A66E ; ALetter # Lo CYRILLIC LETTER MULTIOCULAR O +A67F ; ALetter # Lm CYRILLIC PAYEROK +A680..A69B ; ALetter # L& [28] CYRILLIC CAPITAL LETTER DWE..CYRILLIC SMALL LETTER CROSSED O +A69C..A69D ; ALetter # Lm [2] MODIFIER LETTER CYRILLIC HARD SIGN..MODIFIER LETTER CYRILLIC SOFT SIGN +A6A0..A6E5 ; ALetter # Lo [70] BAMUM LETTER A..BAMUM LETTER KI +A6E6..A6EF ; ALetter # Nl [10] BAMUM LETTER MO..BAMUM LETTER KOGHOM +A708..A716 ; ALetter # Sk [15] MODIFIER LETTER EXTRA-HIGH DOTTED TONE BAR..MODIFIER LETTER EXTRA-LOW LEFT-STEM TONE BAR +A717..A71F ; ALetter # Lm [9] MODIFIER LETTER DOT VERTICAL BAR..MODIFIER LETTER LOW INVERTED EXCLAMATION MARK +A720..A721 ; ALetter # Sk [2] MODIFIER LETTER STRESS AND HIGH TONE..MODIFIER LETTER STRESS AND LOW TONE +A722..A76F ; ALetter # L& [78] LATIN CAPITAL LETTER EGYPTOLOGICAL ALEF..LATIN SMALL LETTER CON +A770 ; ALetter # Lm MODIFIER LETTER US +A771..A787 ; ALetter # L& [23] LATIN SMALL LETTER DUM..LATIN SMALL LETTER INSULAR T +A788 ; ALetter # Lm MODIFIER LETTER LOW CIRCUMFLEX ACCENT +A789..A78A ; ALetter # Sk [2] MODIFIER LETTER COLON..MODIFIER LETTER SHORT EQUALS SIGN +A78B..A78E ; ALetter # L& [4] LATIN CAPITAL LETTER SALTILLO..LATIN SMALL LETTER L WITH RETROFLEX HOOK AND BELT +A78F ; ALetter # Lo LATIN LETTER SINOLOGICAL DOT +A790..A7DC ; ALetter # L& [77] LATIN CAPITAL LETTER N WITH DESCENDER..LATIN CAPITAL LETTER LAMBDA WITH STROKE +A7F1..A7F4 ; ALetter # Lm [4] MODIFIER LETTER CAPITAL S..MODIFIER LETTER CAPITAL Q +A7F5..A7F6 ; ALetter # L& [2] LATIN CAPITAL LETTER REVERSED HALF H..LATIN SMALL LETTER REVERSED HALF H +A7F7 ; ALetter # Lo LATIN EPIGRAPHIC LETTER SIDEWAYS I +A7F8..A7F9 ; ALetter # Lm [2] MODIFIER LETTER CAPITAL H WITH STROKE..MODIFIER LETTER SMALL LIGATURE OE +A7FA ; ALetter # L& LATIN LETTER SMALL CAPITAL TURNED M +A7FB..A801 ; ALetter # Lo [7] LATIN EPIGRAPHIC LETTER REVERSED F..SYLOTI NAGRI LETTER I +A803..A805 ; ALetter # Lo [3] SYLOTI NAGRI LETTER U..SYLOTI NAGRI LETTER O +A807..A80A ; ALetter # Lo [4] SYLOTI NAGRI LETTER KO..SYLOTI NAGRI LETTER GHO +A80C..A822 ; ALetter # Lo [23] SYLOTI NAGRI LETTER CO..SYLOTI NAGRI LETTER HO +A840..A873 ; ALetter # Lo [52] PHAGS-PA LETTER KA..PHAGS-PA LETTER CANDRABINDU +A882..A8B3 ; ALetter # Lo [50] SAURASHTRA LETTER A..SAURASHTRA LETTER LLA +A8F2..A8F7 ; ALetter # Lo [6] DEVANAGARI SIGN SPACING CANDRABINDU..DEVANAGARI SIGN CANDRABINDU AVAGRAHA +A8FB ; ALetter # Lo DEVANAGARI HEADSTROKE +A8FD..A8FE ; ALetter # Lo [2] DEVANAGARI JAIN OM..DEVANAGARI LETTER AY +A90A..A925 ; ALetter # Lo [28] KAYAH LI LETTER KA..KAYAH LI LETTER OO +A930..A946 ; ALetter # Lo [23] REJANG LETTER KA..REJANG LETTER A +A960..A97C ; ALetter # Lo [29] HANGUL CHOSEONG TIKEUT-MIEUM..HANGUL CHOSEONG SSANGYEORINHIEUH +A984..A9B2 ; ALetter # Lo [47] JAVANESE LETTER A..JAVANESE LETTER HA +A9CF ; ALetter # Lm JAVANESE PANGRANGKEP +AA00..AA28 ; ALetter # Lo [41] CHAM LETTER A..CHAM LETTER HA +AA40..AA42 ; ALetter # Lo [3] CHAM LETTER FINAL K..CHAM LETTER FINAL NG +AA44..AA4B ; ALetter # Lo [8] CHAM LETTER FINAL CH..CHAM LETTER FINAL SS +AAE0..AAEA ; ALetter # Lo [11] MEETEI MAYEK LETTER E..MEETEI MAYEK LETTER SSA +AAF2 ; ALetter # Lo MEETEI MAYEK ANJI +AAF3..AAF4 ; ALetter # Lm [2] MEETEI MAYEK SYLLABLE REPETITION MARK..MEETEI MAYEK WORD REPETITION MARK +AB01..AB06 ; ALetter # Lo [6] ETHIOPIC SYLLABLE TTHU..ETHIOPIC SYLLABLE TTHO +AB09..AB0E ; ALetter # Lo [6] ETHIOPIC SYLLABLE DDHU..ETHIOPIC SYLLABLE DDHO +AB11..AB16 ; ALetter # Lo [6] ETHIOPIC SYLLABLE DZU..ETHIOPIC SYLLABLE DZO +AB20..AB26 ; ALetter # Lo [7] ETHIOPIC SYLLABLE CCHHA..ETHIOPIC SYLLABLE CCHHO +AB28..AB2E ; ALetter # Lo [7] ETHIOPIC SYLLABLE BBA..ETHIOPIC SYLLABLE BBO +AB30..AB5A ; ALetter # L& [43] LATIN SMALL LETTER BARRED ALPHA..LATIN SMALL LETTER Y WITH SHORT RIGHT LEG +AB5B ; ALetter # Sk MODIFIER BREVE WITH INVERTED BREVE +AB5C..AB5F ; ALetter # Lm [4] MODIFIER LETTER SMALL HENG..MODIFIER LETTER SMALL U WITH LEFT HOOK +AB60..AB68 ; ALetter # L& [9] LATIN SMALL LETTER SAKHA YAT..LATIN SMALL LETTER TURNED R WITH MIDDLE TILDE +AB69 ; ALetter # Lm MODIFIER LETTER SMALL TURNED W +AB70..ABBF ; ALetter # L& [80] CHEROKEE SMALL LETTER A..CHEROKEE SMALL LETTER YA +ABC0..ABE2 ; ALetter # Lo [35] MEETEI MAYEK LETTER KOK..MEETEI MAYEK LETTER I LONSUM +AC00..D7A3 ; ALetter # Lo [11172] HANGUL SYLLABLE GA..HANGUL SYLLABLE HIH +D7B0..D7C6 ; ALetter # Lo [23] HANGUL JUNGSEONG O-YEO..HANGUL JUNGSEONG ARAEA-E +D7CB..D7FB ; ALetter # Lo [49] HANGUL JONGSEONG NIEUN-RIEUL..HANGUL JONGSEONG PHIEUPH-THIEUTH +FB00..FB06 ; ALetter # L& [7] LATIN SMALL LIGATURE FF..LATIN SMALL LIGATURE ST +FB13..FB17 ; ALetter # L& [5] ARMENIAN SMALL LIGATURE MEN NOW..ARMENIAN SMALL LIGATURE MEN XEH +FB50..FBB1 ; ALetter # Lo [98] ARABIC LETTER ALEF WASLA ISOLATED FORM..ARABIC LETTER YEH BARREE WITH HAMZA ABOVE FINAL FORM +FBD3..FD3D ; ALetter # Lo [363] ARABIC LETTER NG ISOLATED FORM..ARABIC LIGATURE ALEF WITH FATHATAN ISOLATED FORM +FD50..FD8F ; ALetter # Lo [64] ARABIC LIGATURE TEH WITH JEEM WITH MEEM INITIAL FORM..ARABIC LIGATURE MEEM WITH KHAH WITH MEEM INITIAL FORM +FD92..FDC7 ; ALetter # Lo [54] ARABIC LIGATURE MEEM WITH JEEM WITH KHAH INITIAL FORM..ARABIC LIGATURE NOON WITH JEEM WITH YEH FINAL FORM +FDF0..FDFB ; ALetter # Lo [12] ARABIC LIGATURE SALLA USED AS KORANIC STOP SIGN ISOLATED FORM..ARABIC LIGATURE JALLAJALALOUHOU +FE70..FE74 ; ALetter # Lo [5] ARABIC FATHATAN ISOLATED FORM..ARABIC KASRATAN ISOLATED FORM +FE76..FEFC ; ALetter # Lo [135] ARABIC FATHA ISOLATED FORM..ARABIC LIGATURE LAM WITH ALEF FINAL FORM +FF21..FF3A ; ALetter # L& [26] FULLWIDTH LATIN CAPITAL LETTER A..FULLWIDTH LATIN CAPITAL LETTER Z +FF41..FF5A ; ALetter # L& [26] FULLWIDTH LATIN SMALL LETTER A..FULLWIDTH LATIN SMALL LETTER Z +FFA0..FFBE ; ALetter # Lo [31] HALFWIDTH HANGUL FILLER..HALFWIDTH HANGUL LETTER HIEUH +FFC2..FFC7 ; ALetter # Lo [6] HALFWIDTH HANGUL LETTER A..HALFWIDTH HANGUL LETTER E +FFCA..FFCF ; ALetter # Lo [6] HALFWIDTH HANGUL LETTER YEO..HALFWIDTH HANGUL LETTER OE +FFD2..FFD7 ; ALetter # Lo [6] HALFWIDTH HANGUL LETTER YO..HALFWIDTH HANGUL LETTER YU +FFDA..FFDC ; ALetter # Lo [3] HALFWIDTH HANGUL LETTER EU..HALFWIDTH HANGUL LETTER I +10000..1000B ; ALetter # Lo [12] LINEAR B SYLLABLE B008 A..LINEAR B SYLLABLE B046 JE +1000D..10026 ; ALetter # Lo [26] LINEAR B SYLLABLE B036 JO..LINEAR B SYLLABLE B032 QO +10028..1003A ; ALetter # Lo [19] LINEAR B SYLLABLE B060 RA..LINEAR B SYLLABLE B042 WO +1003C..1003D ; ALetter # Lo [2] LINEAR B SYLLABLE B017 ZA..LINEAR B SYLLABLE B074 ZE +1003F..1004D ; ALetter # Lo [15] LINEAR B SYLLABLE B020 ZO..LINEAR B SYLLABLE B091 TWO +10050..1005D ; ALetter # Lo [14] LINEAR B SYMBOL B018..LINEAR B SYMBOL B089 +10080..100FA ; ALetter # Lo [123] LINEAR B IDEOGRAM B100 MAN..LINEAR B IDEOGRAM VESSEL B305 +10140..10174 ; ALetter # Nl [53] GREEK ACROPHONIC ATTIC ONE QUARTER..GREEK ACROPHONIC STRATIAN FIFTY MNAS +10280..1029C ; ALetter # Lo [29] LYCIAN LETTER A..LYCIAN LETTER X +102A0..102D0 ; ALetter # Lo [49] CARIAN LETTER A..CARIAN LETTER UUU3 +10300..1031F ; ALetter # Lo [32] OLD ITALIC LETTER A..OLD ITALIC LETTER ESS +1032D..10340 ; ALetter # Lo [20] OLD ITALIC LETTER YE..GOTHIC LETTER PAIRTHRA +10341 ; ALetter # Nl GOTHIC LETTER NINETY +10342..10349 ; ALetter # Lo [8] GOTHIC LETTER RAIDA..GOTHIC LETTER OTHAL +1034A ; ALetter # Nl GOTHIC LETTER NINE HUNDRED +10350..10375 ; ALetter # Lo [38] OLD PERMIC LETTER AN..OLD PERMIC LETTER IA +10380..1039D ; ALetter # Lo [30] UGARITIC LETTER ALPA..UGARITIC LETTER SSU +103A0..103C3 ; ALetter # Lo [36] OLD PERSIAN SIGN A..OLD PERSIAN SIGN HA +103C8..103CF ; ALetter # Lo [8] OLD PERSIAN SIGN AURAMAZDAA..OLD PERSIAN SIGN BUUMISH +103D1..103D5 ; ALetter # Nl [5] OLD PERSIAN NUMBER ONE..OLD PERSIAN NUMBER HUNDRED +10400..1044F ; ALetter # L& [80] DESERET CAPITAL LETTER LONG I..DESERET SMALL LETTER EW +10450..1049D ; ALetter # Lo [78] SHAVIAN LETTER PEEP..OSMANYA LETTER OO +104B0..104D3 ; ALetter # L& [36] OSAGE CAPITAL LETTER A..OSAGE CAPITAL LETTER ZHA +104D8..104FB ; ALetter # L& [36] OSAGE SMALL LETTER A..OSAGE SMALL LETTER ZHA +10500..10527 ; ALetter # Lo [40] ELBASAN LETTER A..ELBASAN LETTER KHE +10530..10563 ; ALetter # Lo [52] CAUCASIAN ALBANIAN LETTER ALT..CAUCASIAN ALBANIAN LETTER KIW +10570..1057A ; ALetter # L& [11] VITHKUQI CAPITAL LETTER A..VITHKUQI CAPITAL LETTER GA +1057C..1058A ; ALetter # L& [15] VITHKUQI CAPITAL LETTER HA..VITHKUQI CAPITAL LETTER RE +1058C..10592 ; ALetter # L& [7] VITHKUQI CAPITAL LETTER SE..VITHKUQI CAPITAL LETTER XE +10594..10595 ; ALetter # L& [2] VITHKUQI CAPITAL LETTER Y..VITHKUQI CAPITAL LETTER ZE +10597..105A1 ; ALetter # L& [11] VITHKUQI SMALL LETTER A..VITHKUQI SMALL LETTER GA +105A3..105B1 ; ALetter # L& [15] VITHKUQI SMALL LETTER HA..VITHKUQI SMALL LETTER RE +105B3..105B9 ; ALetter # L& [7] VITHKUQI SMALL LETTER SE..VITHKUQI SMALL LETTER XE +105BB..105BC ; ALetter # L& [2] VITHKUQI SMALL LETTER Y..VITHKUQI SMALL LETTER ZE +105C0..105F3 ; ALetter # Lo [52] TODHRI LETTER A..TODHRI LETTER OO +10600..10736 ; ALetter # Lo [311] LINEAR A SIGN AB001..LINEAR A SIGN A664 +10740..10755 ; ALetter # Lo [22] LINEAR A SIGN A701 A..LINEAR A SIGN A732 JE +10760..10767 ; ALetter # Lo [8] LINEAR A SIGN A800..LINEAR A SIGN A807 +10780..10785 ; ALetter # Lm [6] MODIFIER LETTER SMALL CAPITAL AA..MODIFIER LETTER SMALL B WITH HOOK +10787..107B0 ; ALetter # Lm [42] MODIFIER LETTER SMALL DZ DIGRAPH..MODIFIER LETTER SMALL V WITH RIGHT HOOK +107B2..107BA ; ALetter # Lm [9] MODIFIER LETTER SMALL CAPITAL Y..MODIFIER LETTER SMALL S WITH CURL +10800..10805 ; ALetter # Lo [6] CYPRIOT SYLLABLE A..CYPRIOT SYLLABLE JA +10808 ; ALetter # Lo CYPRIOT SYLLABLE JO +1080A..10835 ; ALetter # Lo [44] CYPRIOT SYLLABLE KA..CYPRIOT SYLLABLE WO +10837..10838 ; ALetter # Lo [2] CYPRIOT SYLLABLE XA..CYPRIOT SYLLABLE XE +1083C ; ALetter # Lo CYPRIOT SYLLABLE ZA +1083F..10855 ; ALetter # Lo [23] CYPRIOT SYLLABLE ZO..IMPERIAL ARAMAIC LETTER TAW +10860..10876 ; ALetter # Lo [23] PALMYRENE LETTER ALEPH..PALMYRENE LETTER TAW +10880..1089E ; ALetter # Lo [31] NABATAEAN LETTER FINAL ALEPH..NABATAEAN LETTER TAW +108E0..108F2 ; ALetter # Lo [19] HATRAN LETTER ALEPH..HATRAN LETTER QOPH +108F4..108F5 ; ALetter # Lo [2] HATRAN LETTER SHIN..HATRAN LETTER TAW +10900..10915 ; ALetter # Lo [22] PHOENICIAN LETTER ALF..PHOENICIAN LETTER TAU +10920..10939 ; ALetter # Lo [26] LYDIAN LETTER A..LYDIAN LETTER C +10940..10959 ; ALetter # Lo [26] SIDETIC LETTER N01..SIDETIC LETTER N26 +10980..109B7 ; ALetter # Lo [56] MEROITIC HIEROGLYPHIC LETTER A..MEROITIC CURSIVE LETTER DA +109BE..109BF ; ALetter # Lo [2] MEROITIC CURSIVE LOGOGRAM RMT..MEROITIC CURSIVE LOGOGRAM IMN +10A00 ; ALetter # Lo KHAROSHTHI LETTER A +10A10..10A13 ; ALetter # Lo [4] KHAROSHTHI LETTER KA..KHAROSHTHI LETTER GHA +10A15..10A17 ; ALetter # Lo [3] KHAROSHTHI LETTER CA..KHAROSHTHI LETTER JA +10A19..10A35 ; ALetter # Lo [29] KHAROSHTHI LETTER NYA..KHAROSHTHI LETTER VHA +10A60..10A7C ; ALetter # Lo [29] OLD SOUTH ARABIAN LETTER HE..OLD SOUTH ARABIAN LETTER THETH +10A80..10A9C ; ALetter # Lo [29] OLD NORTH ARABIAN LETTER HEH..OLD NORTH ARABIAN LETTER ZAH +10AC0..10AC7 ; ALetter # Lo [8] MANICHAEAN LETTER ALEPH..MANICHAEAN LETTER WAW +10AC9..10AE4 ; ALetter # Lo [28] MANICHAEAN LETTER ZAYIN..MANICHAEAN LETTER TAW +10B00..10B35 ; ALetter # Lo [54] AVESTAN LETTER A..AVESTAN LETTER HE +10B40..10B55 ; ALetter # Lo [22] INSCRIPTIONAL PARTHIAN LETTER ALEPH..INSCRIPTIONAL PARTHIAN LETTER TAW +10B60..10B72 ; ALetter # Lo [19] INSCRIPTIONAL PAHLAVI LETTER ALEPH..INSCRIPTIONAL PAHLAVI LETTER TAW +10B80..10B91 ; ALetter # Lo [18] PSALTER PAHLAVI LETTER ALEPH..PSALTER PAHLAVI LETTER TAW +10C00..10C48 ; ALetter # Lo [73] OLD TURKIC LETTER ORKHON A..OLD TURKIC LETTER ORKHON BASH +10C80..10CB2 ; ALetter # L& [51] OLD HUNGARIAN CAPITAL LETTER A..OLD HUNGARIAN CAPITAL LETTER US +10CC0..10CF2 ; ALetter # L& [51] OLD HUNGARIAN SMALL LETTER A..OLD HUNGARIAN SMALL LETTER US +10D00..10D23 ; ALetter # Lo [36] HANIFI ROHINGYA LETTER A..HANIFI ROHINGYA MARK NA KHONNA +10D4A..10D4D ; ALetter # Lo [4] GARAY VOWEL SIGN A..GARAY VOWEL SIGN EE +10D4E ; ALetter # Lm GARAY VOWEL LENGTH MARK +10D4F ; ALetter # Lo GARAY SUKUN +10D50..10D65 ; ALetter # L& [22] GARAY CAPITAL LETTER A..GARAY CAPITAL LETTER OLD NA +10D6F ; ALetter # Lm GARAY REDUPLICATION MARK +10D70..10D85 ; ALetter # L& [22] GARAY SMALL LETTER A..GARAY SMALL LETTER OLD NA +10E80..10EA9 ; ALetter # Lo [42] YEZIDI LETTER ELIF..YEZIDI LETTER ET +10EB0..10EB1 ; ALetter # Lo [2] YEZIDI LETTER LAM WITH DOT ABOVE..YEZIDI LETTER YOT WITH CIRCUMFLEX ABOVE +10EC2..10EC4 ; ALetter # Lo [3] ARABIC LETTER DAL WITH TWO DOTS VERTICALLY BELOW..ARABIC LETTER KAF WITH TWO DOTS VERTICALLY BELOW +10EC5 ; ALetter # Lm ARABIC SMALL YEH BARREE WITH TWO DOTS BELOW +10EC6..10EC7 ; ALetter # Lo [2] ARABIC LETTER THIN NOON..ARABIC LETTER YEH WITH FOUR DOTS BELOW +10F00..10F1C ; ALetter # Lo [29] OLD SOGDIAN LETTER ALEPH..OLD SOGDIAN LETTER FINAL TAW WITH VERTICAL TAIL +10F27 ; ALetter # Lo OLD SOGDIAN LIGATURE AYIN-DALETH +10F30..10F45 ; ALetter # Lo [22] SOGDIAN LETTER ALEPH..SOGDIAN INDEPENDENT SHIN +10F70..10F81 ; ALetter # Lo [18] OLD UYGHUR LETTER ALEPH..OLD UYGHUR LETTER LESH +10FB0..10FC4 ; ALetter # Lo [21] CHORASMIAN LETTER ALEPH..CHORASMIAN LETTER TAW +10FE0..10FF6 ; ALetter # Lo [23] ELYMAIC LETTER ALEPH..ELYMAIC LIGATURE ZAYIN-YODH +11003..11037 ; ALetter # Lo [53] BRAHMI SIGN JIHVAMULIYA..BRAHMI LETTER OLD TAMIL NNNA +11071..11072 ; ALetter # Lo [2] BRAHMI LETTER OLD TAMIL SHORT E..BRAHMI LETTER OLD TAMIL SHORT O +11075 ; ALetter # Lo BRAHMI LETTER OLD TAMIL LLA +11083..110AF ; ALetter # Lo [45] KAITHI LETTER A..KAITHI LETTER HA +110D0..110E8 ; ALetter # Lo [25] SORA SOMPENG LETTER SAH..SORA SOMPENG LETTER MAE +11103..11126 ; ALetter # Lo [36] CHAKMA LETTER AA..CHAKMA LETTER HAA +11144 ; ALetter # Lo CHAKMA LETTER LHAA +11147 ; ALetter # Lo CHAKMA LETTER VAA +11150..11172 ; ALetter # Lo [35] MAHAJANI LETTER A..MAHAJANI LETTER RRA +11176 ; ALetter # Lo MAHAJANI LIGATURE SHRI +11183..111B2 ; ALetter # Lo [48] SHARADA LETTER A..SHARADA LETTER HA +111C1..111C4 ; ALetter # Lo [4] SHARADA SIGN AVAGRAHA..SHARADA OM +111DA ; ALetter # Lo SHARADA EKAM +111DC ; ALetter # Lo SHARADA HEADSTROKE +11200..11211 ; ALetter # Lo [18] KHOJKI LETTER A..KHOJKI LETTER JJA +11213..1122B ; ALetter # Lo [25] KHOJKI LETTER NYA..KHOJKI LETTER LLA +1123F..11240 ; ALetter # Lo [2] KHOJKI LETTER QA..KHOJKI LETTER SHORT I +11280..11286 ; ALetter # Lo [7] MULTANI LETTER A..MULTANI LETTER GA +11288 ; ALetter # Lo MULTANI LETTER GHA +1128A..1128D ; ALetter # Lo [4] MULTANI LETTER CA..MULTANI LETTER JJA +1128F..1129D ; ALetter # Lo [15] MULTANI LETTER NYA..MULTANI LETTER BA +1129F..112A8 ; ALetter # Lo [10] MULTANI LETTER BHA..MULTANI LETTER RHA +112B0..112DE ; ALetter # Lo [47] KHUDAWADI LETTER A..KHUDAWADI LETTER HA +11305..1130C ; ALetter # Lo [8] GRANTHA LETTER A..GRANTHA LETTER VOCALIC L +1130F..11310 ; ALetter # Lo [2] GRANTHA LETTER EE..GRANTHA LETTER AI +11313..11328 ; ALetter # Lo [22] GRANTHA LETTER OO..GRANTHA LETTER NA +1132A..11330 ; ALetter # Lo [7] GRANTHA LETTER PA..GRANTHA LETTER RA +11332..11333 ; ALetter # Lo [2] GRANTHA LETTER LA..GRANTHA LETTER LLA +11335..11339 ; ALetter # Lo [5] GRANTHA LETTER VA..GRANTHA LETTER HA +1133D ; ALetter # Lo GRANTHA SIGN AVAGRAHA +11350 ; ALetter # Lo GRANTHA OM +1135D..11361 ; ALetter # Lo [5] GRANTHA SIGN PLUTA..GRANTHA LETTER VOCALIC LL +11380..11389 ; ALetter # Lo [10] TULU-TIGALARI LETTER A..TULU-TIGALARI LETTER VOCALIC LL +1138B ; ALetter # Lo TULU-TIGALARI LETTER EE +1138E ; ALetter # Lo TULU-TIGALARI LETTER AI +11390..113B5 ; ALetter # Lo [38] TULU-TIGALARI LETTER OO..TULU-TIGALARI LETTER LLLA +113B7 ; ALetter # Lo TULU-TIGALARI SIGN AVAGRAHA +113D1 ; ALetter # Lo TULU-TIGALARI REPHA +113D3 ; ALetter # Lo TULU-TIGALARI SIGN PLUTA +11400..11434 ; ALetter # Lo [53] NEWA LETTER A..NEWA LETTER HA +11447..1144A ; ALetter # Lo [4] NEWA SIGN AVAGRAHA..NEWA SIDDHI +1145F..11461 ; ALetter # Lo [3] NEWA LETTER VEDIC ANUSVARA..NEWA SIGN UPADHMANIYA +11480..114AF ; ALetter # Lo [48] TIRHUTA ANJI..TIRHUTA LETTER HA +114C4..114C5 ; ALetter # Lo [2] TIRHUTA SIGN AVAGRAHA..TIRHUTA GVANG +114C7 ; ALetter # Lo TIRHUTA OM +11580..115AE ; ALetter # Lo [47] SIDDHAM LETTER A..SIDDHAM LETTER HA +115D8..115DB ; ALetter # Lo [4] SIDDHAM LETTER THREE-CIRCLE ALTERNATE I..SIDDHAM LETTER ALTERNATE U +11600..1162F ; ALetter # Lo [48] MODI LETTER A..MODI LETTER LLA +11644 ; ALetter # Lo MODI SIGN HUVA +11680..116AA ; ALetter # Lo [43] TAKRI LETTER A..TAKRI LETTER RRA +116B8 ; ALetter # Lo TAKRI LETTER ARCHAIC KHA +11800..1182B ; ALetter # Lo [44] DOGRA LETTER A..DOGRA LETTER RRA +118A0..118DF ; ALetter # L& [64] WARANG CITI CAPITAL LETTER NGAA..WARANG CITI SMALL LETTER VIYO +118FF..11906 ; ALetter # Lo [8] WARANG CITI OM..DIVES AKURU LETTER E +11909 ; ALetter # Lo DIVES AKURU LETTER O +1190C..11913 ; ALetter # Lo [8] DIVES AKURU LETTER KA..DIVES AKURU LETTER JA +11915..11916 ; ALetter # Lo [2] DIVES AKURU LETTER NYA..DIVES AKURU LETTER TTA +11918..1192F ; ALetter # Lo [24] DIVES AKURU LETTER DDA..DIVES AKURU LETTER ZA +1193F ; ALetter # Lo DIVES AKURU PREFIXED NASAL SIGN +11941 ; ALetter # Lo DIVES AKURU INITIAL RA +119A0..119A7 ; ALetter # Lo [8] NANDINAGARI LETTER A..NANDINAGARI LETTER VOCALIC RR +119AA..119D0 ; ALetter # Lo [39] NANDINAGARI LETTER E..NANDINAGARI LETTER RRA +119E1 ; ALetter # Lo NANDINAGARI SIGN AVAGRAHA +119E3 ; ALetter # Lo NANDINAGARI HEADSTROKE +11A00 ; ALetter # Lo ZANABAZAR SQUARE LETTER A +11A0B..11A32 ; ALetter # Lo [40] ZANABAZAR SQUARE LETTER KA..ZANABAZAR SQUARE LETTER KSSA +11A3A ; ALetter # Lo ZANABAZAR SQUARE CLUSTER-INITIAL LETTER RA +11A50 ; ALetter # Lo SOYOMBO LETTER A +11A5C..11A89 ; ALetter # Lo [46] SOYOMBO LETTER KA..SOYOMBO CLUSTER-INITIAL LETTER SA +11A9D ; ALetter # Lo SOYOMBO MARK PLUTA +11AB0..11AF8 ; ALetter # Lo [73] CANADIAN SYLLABICS NATTILIK HI..PAU CIN HAU GLOTTAL STOP FINAL +11BC0..11BE0 ; ALetter # Lo [33] SUNUWAR LETTER DEVI..SUNUWAR LETTER KLOKO +11C00..11C08 ; ALetter # Lo [9] BHAIKSUKI LETTER A..BHAIKSUKI LETTER VOCALIC L +11C0A..11C2E ; ALetter # Lo [37] BHAIKSUKI LETTER E..BHAIKSUKI LETTER HA +11C40 ; ALetter # Lo BHAIKSUKI SIGN AVAGRAHA +11C72..11C8F ; ALetter # Lo [30] MARCHEN LETTER KA..MARCHEN LETTER A +11D00..11D06 ; ALetter # Lo [7] MASARAM GONDI LETTER A..MASARAM GONDI LETTER E +11D08..11D09 ; ALetter # Lo [2] MASARAM GONDI LETTER AI..MASARAM GONDI LETTER O +11D0B..11D30 ; ALetter # Lo [38] MASARAM GONDI LETTER AU..MASARAM GONDI LETTER TRA +11D46 ; ALetter # Lo MASARAM GONDI REPHA +11D60..11D65 ; ALetter # Lo [6] GUNJALA GONDI LETTER A..GUNJALA GONDI LETTER UU +11D67..11D68 ; ALetter # Lo [2] GUNJALA GONDI LETTER EE..GUNJALA GONDI LETTER AI +11D6A..11D89 ; ALetter # Lo [32] GUNJALA GONDI LETTER OO..GUNJALA GONDI LETTER SA +11D98 ; ALetter # Lo GUNJALA GONDI OM +11DB0..11DD8 ; ALetter # Lo [41] TOLONG SIKI LETTER I..TOLONG SIKI LETTER RRH +11DD9 ; ALetter # Lm TOLONG SIKI SIGN SELA +11DDA..11DDB ; ALetter # Lo [2] TOLONG SIKI SIGN HECAKA..TOLONG SIKI UNGGA +11EE0..11EF2 ; ALetter # Lo [19] MAKASAR LETTER KA..MAKASAR ANGKA +11F02 ; ALetter # Lo KAWI SIGN REPHA +11F04..11F10 ; ALetter # Lo [13] KAWI LETTER A..KAWI LETTER O +11F12..11F33 ; ALetter # Lo [34] KAWI LETTER KA..KAWI LETTER JNYA +11FB0 ; ALetter # Lo LISU LETTER YHA +12000..12399 ; ALetter # Lo [922] CUNEIFORM SIGN A..CUNEIFORM SIGN U U +12400..1246E ; ALetter # Nl [111] CUNEIFORM NUMERIC SIGN TWO ASH..CUNEIFORM NUMERIC SIGN NINE U VARIANT FORM +12480..12543 ; ALetter # Lo [196] CUNEIFORM SIGN AB TIMES NUN TENU..CUNEIFORM SIGN ZU5 TIMES THREE DISH TENU +12F90..12FF0 ; ALetter # Lo [97] CYPRO-MINOAN SIGN CM001..CYPRO-MINOAN SIGN CM114 +13000..1342F ; ALetter # Lo [1072] EGYPTIAN HIEROGLYPH A001..EGYPTIAN HIEROGLYPH V011D +13441..13446 ; ALetter # Lo [6] EGYPTIAN HIEROGLYPH FULL BLANK..EGYPTIAN HIEROGLYPH WIDE LOST SIGN +13460..143FA ; ALetter # Lo [3995] EGYPTIAN HIEROGLYPH-13460..EGYPTIAN HIEROGLYPH-143FA +14400..14646 ; ALetter # Lo [583] ANATOLIAN HIEROGLYPH A001..ANATOLIAN HIEROGLYPH A530 +16100..1611D ; ALetter # Lo [30] GURUNG KHEMA LETTER A..GURUNG KHEMA LETTER SA +16800..16A38 ; ALetter # Lo [569] BAMUM LETTER PHASE-A NGKUE MFON..BAMUM LETTER PHASE-F VUEQ +16A40..16A5E ; ALetter # Lo [31] MRO LETTER TA..MRO LETTER TEK +16A70..16ABE ; ALetter # Lo [79] TANGSA LETTER OZ..TANGSA LETTER ZA +16AD0..16AED ; ALetter # Lo [30] BASSA VAH LETTER ENNI..BASSA VAH LETTER I +16B00..16B2F ; ALetter # Lo [48] PAHAWH HMONG VOWEL KEEB..PAHAWH HMONG CONSONANT CAU +16B40..16B43 ; ALetter # Lm [4] PAHAWH HMONG SIGN VOS SEEV..PAHAWH HMONG SIGN IB YAM +16B63..16B77 ; ALetter # Lo [21] PAHAWH HMONG SIGN VOS LUB..PAHAWH HMONG SIGN CIM NRES TOS +16B7D..16B8F ; ALetter # Lo [19] PAHAWH HMONG CLAN SIGN TSHEEJ..PAHAWH HMONG CLAN SIGN VWJ +16D40..16D42 ; ALetter # Lm [3] KIRAT RAI SIGN ANUSVARA..KIRAT RAI SIGN VISARGA +16D43..16D6A ; ALetter # Lo [40] KIRAT RAI LETTER A..KIRAT RAI VOWEL SIGN AU +16D6B..16D6C ; ALetter # Lm [2] KIRAT RAI SIGN VIRAMA..KIRAT RAI SIGN SAAT +16E40..16E7F ; ALetter # L& [64] MEDEFAIDRIN CAPITAL LETTER M..MEDEFAIDRIN SMALL LETTER Y +16EA0..16EB8 ; ALetter # L& [25] BERIA ERFE CAPITAL LETTER ARKAB..BERIA ERFE CAPITAL LETTER AY +16EBB..16ED3 ; ALetter # L& [25] BERIA ERFE SMALL LETTER ARKAB..BERIA ERFE SMALL LETTER AY +16F00..16F4A ; ALetter # Lo [75] MIAO LETTER PA..MIAO LETTER RTE +16F50 ; ALetter # Lo MIAO LETTER NASALIZATION +16F93..16F9F ; ALetter # Lm [13] MIAO LETTER TONE-2..MIAO LETTER REFORMED TONE-8 +16FE0..16FE1 ; ALetter # Lm [2] TANGUT ITERATION MARK..NUSHU ITERATION MARK +16FE3 ; ALetter # Lm OLD CHINESE ITERATION MARK +1BC00..1BC6A ; ALetter # Lo [107] DUPLOYAN LETTER H..DUPLOYAN LETTER VOCALIC M +1BC70..1BC7C ; ALetter # Lo [13] DUPLOYAN AFFIX LEFT HORIZONTAL SECANT..DUPLOYAN AFFIX ATTACHED TANGENT HOOK +1BC80..1BC88 ; ALetter # Lo [9] DUPLOYAN AFFIX HIGH ACUTE..DUPLOYAN AFFIX HIGH VERTICAL +1BC90..1BC99 ; ALetter # Lo [10] DUPLOYAN AFFIX LOW ACUTE..DUPLOYAN AFFIX LOW ARROW +1D400..1D454 ; ALetter # L& [85] MATHEMATICAL BOLD CAPITAL A..MATHEMATICAL ITALIC SMALL G +1D456..1D49C ; ALetter # L& [71] MATHEMATICAL ITALIC SMALL I..MATHEMATICAL SCRIPT CAPITAL A +1D49E..1D49F ; ALetter # L& [2] MATHEMATICAL SCRIPT CAPITAL C..MATHEMATICAL SCRIPT CAPITAL D +1D4A2 ; ALetter # L& MATHEMATICAL SCRIPT CAPITAL G +1D4A5..1D4A6 ; ALetter # L& [2] MATHEMATICAL SCRIPT CAPITAL J..MATHEMATICAL SCRIPT CAPITAL K +1D4A9..1D4AC ; ALetter # L& [4] MATHEMATICAL SCRIPT CAPITAL N..MATHEMATICAL SCRIPT CAPITAL Q +1D4AE..1D4B9 ; ALetter # L& [12] MATHEMATICAL SCRIPT CAPITAL S..MATHEMATICAL SCRIPT SMALL D +1D4BB ; ALetter # L& MATHEMATICAL SCRIPT SMALL F +1D4BD..1D4C3 ; ALetter # L& [7] MATHEMATICAL SCRIPT SMALL H..MATHEMATICAL SCRIPT SMALL N +1D4C5..1D505 ; ALetter # L& [65] MATHEMATICAL SCRIPT SMALL P..MATHEMATICAL FRAKTUR CAPITAL B +1D507..1D50A ; ALetter # L& [4] MATHEMATICAL FRAKTUR CAPITAL D..MATHEMATICAL FRAKTUR CAPITAL G +1D50D..1D514 ; ALetter # L& [8] MATHEMATICAL FRAKTUR CAPITAL J..MATHEMATICAL FRAKTUR CAPITAL Q +1D516..1D51C ; ALetter # L& [7] MATHEMATICAL FRAKTUR CAPITAL S..MATHEMATICAL FRAKTUR CAPITAL Y +1D51E..1D539 ; ALetter # L& [28] MATHEMATICAL FRAKTUR SMALL A..MATHEMATICAL DOUBLE-STRUCK CAPITAL B +1D53B..1D53E ; ALetter # L& [4] MATHEMATICAL DOUBLE-STRUCK CAPITAL D..MATHEMATICAL DOUBLE-STRUCK CAPITAL G +1D540..1D544 ; ALetter # L& [5] MATHEMATICAL DOUBLE-STRUCK CAPITAL I..MATHEMATICAL DOUBLE-STRUCK CAPITAL M +1D546 ; ALetter # L& MATHEMATICAL DOUBLE-STRUCK CAPITAL O +1D54A..1D550 ; ALetter # L& [7] MATHEMATICAL DOUBLE-STRUCK CAPITAL S..MATHEMATICAL DOUBLE-STRUCK CAPITAL Y +1D552..1D6A5 ; ALetter # L& [340] MATHEMATICAL DOUBLE-STRUCK SMALL A..MATHEMATICAL ITALIC SMALL DOTLESS J +1D6A8..1D6C0 ; ALetter # L& [25] MATHEMATICAL BOLD CAPITAL ALPHA..MATHEMATICAL BOLD CAPITAL OMEGA +1D6C2..1D6DA ; ALetter # L& [25] MATHEMATICAL BOLD SMALL ALPHA..MATHEMATICAL BOLD SMALL OMEGA +1D6DC..1D6FA ; ALetter # L& [31] MATHEMATICAL BOLD EPSILON SYMBOL..MATHEMATICAL ITALIC CAPITAL OMEGA +1D6FC..1D714 ; ALetter # L& [25] MATHEMATICAL ITALIC SMALL ALPHA..MATHEMATICAL ITALIC SMALL OMEGA +1D716..1D734 ; ALetter # L& [31] MATHEMATICAL ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD ITALIC CAPITAL OMEGA +1D736..1D74E ; ALetter # L& [25] MATHEMATICAL BOLD ITALIC SMALL ALPHA..MATHEMATICAL BOLD ITALIC SMALL OMEGA +1D750..1D76E ; ALetter # L& [31] MATHEMATICAL BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD CAPITAL OMEGA +1D770..1D788 ; ALetter # L& [25] MATHEMATICAL SANS-SERIF BOLD SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD SMALL OMEGA +1D78A..1D7A8 ; ALetter # L& [31] MATHEMATICAL SANS-SERIF BOLD EPSILON SYMBOL..MATHEMATICAL SANS-SERIF BOLD ITALIC CAPITAL OMEGA +1D7AA..1D7C2 ; ALetter # L& [25] MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL ALPHA..MATHEMATICAL SANS-SERIF BOLD ITALIC SMALL OMEGA +1D7C4..1D7CB ; ALetter # L& [8] MATHEMATICAL SANS-SERIF BOLD ITALIC EPSILON SYMBOL..MATHEMATICAL BOLD SMALL DIGAMMA +1DF00..1DF09 ; ALetter # L& [10] LATIN SMALL LETTER FENG DIGRAPH WITH TRILL..LATIN SMALL LETTER T WITH HOOK AND RETROFLEX HOOK +1DF0A ; ALetter # Lo LATIN LETTER RETROFLEX CLICK WITH RETROFLEX HOOK +1DF0B..1DF1E ; ALetter # L& [20] LATIN SMALL LETTER ESH WITH DOUBLE BAR..LATIN SMALL LETTER S WITH CURL +1DF25..1DF2A ; ALetter # L& [6] LATIN SMALL LETTER D WITH MID-HEIGHT LEFT HOOK..LATIN SMALL LETTER T WITH MID-HEIGHT LEFT HOOK +1E030..1E06D ; ALetter # Lm [62] MODIFIER LETTER CYRILLIC SMALL A..MODIFIER LETTER CYRILLIC SMALL STRAIGHT U WITH STROKE +1E100..1E12C ; ALetter # Lo [45] NYIAKENG PUACHUE HMONG LETTER MA..NYIAKENG PUACHUE HMONG LETTER W +1E137..1E13D ; ALetter # Lm [7] NYIAKENG PUACHUE HMONG SIGN FOR PERSON..NYIAKENG PUACHUE HMONG SYLLABLE LENGTHENER +1E14E ; ALetter # Lo NYIAKENG PUACHUE HMONG LOGOGRAM NYAJ +1E290..1E2AD ; ALetter # Lo [30] TOTO LETTER PA..TOTO LETTER A +1E2C0..1E2EB ; ALetter # Lo [44] WANCHO LETTER AA..WANCHO LETTER YIH +1E4D0..1E4EA ; ALetter # Lo [27] NAG MUNDARI LETTER O..NAG MUNDARI LETTER ELL +1E4EB ; ALetter # Lm NAG MUNDARI SIGN OJOD +1E5D0..1E5ED ; ALetter # Lo [30] OL ONAL LETTER O..OL ONAL LETTER EG +1E5F0 ; ALetter # Lo OL ONAL SIGN HODDOND +1E6C0..1E6DE ; ALetter # Lo [31] TAI YO LETTER LOW KO..TAI YO LETTER HIGH KVO +1E6E0..1E6E2 ; ALetter # Lo [3] TAI YO LETTER AA..TAI YO LETTER UE +1E6E4..1E6E5 ; ALetter # Lo [2] TAI YO LETTER U..TAI YO LETTER AE +1E6E7..1E6ED ; ALetter # Lo [7] TAI YO LETTER O..TAI YO LETTER AUE +1E6F0..1E6F4 ; ALetter # Lo [5] TAI YO LETTER AN..TAI YO LETTER AP +1E6FE ; ALetter # Lo TAI YO SYMBOL MUEANG +1E6FF ; ALetter # Lm TAI YO XAM LAI +1E7E0..1E7E6 ; ALetter # Lo [7] ETHIOPIC SYLLABLE HHYA..ETHIOPIC SYLLABLE HHYO +1E7E8..1E7EB ; ALetter # Lo [4] ETHIOPIC SYLLABLE GURAGE HHWA..ETHIOPIC SYLLABLE HHWE +1E7ED..1E7EE ; ALetter # Lo [2] ETHIOPIC SYLLABLE GURAGE MWI..ETHIOPIC SYLLABLE GURAGE MWEE +1E7F0..1E7FE ; ALetter # Lo [15] ETHIOPIC SYLLABLE GURAGE QWI..ETHIOPIC SYLLABLE GURAGE PWEE +1E800..1E8C4 ; ALetter # Lo [197] MENDE KIKAKUI SYLLABLE M001 KI..MENDE KIKAKUI SYLLABLE M060 NYON +1E900..1E943 ; ALetter # L& [68] ADLAM CAPITAL LETTER ALIF..ADLAM SMALL LETTER SHA +1E94B ; ALetter # Lm ADLAM NASALIZATION MARK +1EE00..1EE03 ; ALetter # Lo [4] ARABIC MATHEMATICAL ALEF..ARABIC MATHEMATICAL DAL +1EE05..1EE1F ; ALetter # Lo [27] ARABIC MATHEMATICAL WAW..ARABIC MATHEMATICAL DOTLESS QAF +1EE21..1EE22 ; ALetter # Lo [2] ARABIC MATHEMATICAL INITIAL BEH..ARABIC MATHEMATICAL INITIAL JEEM +1EE24 ; ALetter # Lo ARABIC MATHEMATICAL INITIAL HEH +1EE27 ; ALetter # Lo ARABIC MATHEMATICAL INITIAL HAH +1EE29..1EE32 ; ALetter # Lo [10] ARABIC MATHEMATICAL INITIAL YEH..ARABIC MATHEMATICAL INITIAL QAF +1EE34..1EE37 ; ALetter # Lo [4] ARABIC MATHEMATICAL INITIAL SHEEN..ARABIC MATHEMATICAL INITIAL KHAH +1EE39 ; ALetter # Lo ARABIC MATHEMATICAL INITIAL DAD +1EE3B ; ALetter # Lo ARABIC MATHEMATICAL INITIAL GHAIN +1EE42 ; ALetter # Lo ARABIC MATHEMATICAL TAILED JEEM +1EE47 ; ALetter # Lo ARABIC MATHEMATICAL TAILED HAH +1EE49 ; ALetter # Lo ARABIC MATHEMATICAL TAILED YEH +1EE4B ; ALetter # Lo ARABIC MATHEMATICAL TAILED LAM +1EE4D..1EE4F ; ALetter # Lo [3] ARABIC MATHEMATICAL TAILED NOON..ARABIC MATHEMATICAL TAILED AIN +1EE51..1EE52 ; ALetter # Lo [2] ARABIC MATHEMATICAL TAILED SAD..ARABIC MATHEMATICAL TAILED QAF +1EE54 ; ALetter # Lo ARABIC MATHEMATICAL TAILED SHEEN +1EE57 ; ALetter # Lo ARABIC MATHEMATICAL TAILED KHAH +1EE59 ; ALetter # Lo ARABIC MATHEMATICAL TAILED DAD +1EE5B ; ALetter # Lo ARABIC MATHEMATICAL TAILED GHAIN +1EE5D ; ALetter # Lo ARABIC MATHEMATICAL TAILED DOTLESS NOON +1EE5F ; ALetter # Lo ARABIC MATHEMATICAL TAILED DOTLESS QAF +1EE61..1EE62 ; ALetter # Lo [2] ARABIC MATHEMATICAL STRETCHED BEH..ARABIC MATHEMATICAL STRETCHED JEEM +1EE64 ; ALetter # Lo ARABIC MATHEMATICAL STRETCHED HEH +1EE67..1EE6A ; ALetter # Lo [4] ARABIC MATHEMATICAL STRETCHED HAH..ARABIC MATHEMATICAL STRETCHED KAF +1EE6C..1EE72 ; ALetter # Lo [7] ARABIC MATHEMATICAL STRETCHED MEEM..ARABIC MATHEMATICAL STRETCHED QAF +1EE74..1EE77 ; ALetter # Lo [4] ARABIC MATHEMATICAL STRETCHED SHEEN..ARABIC MATHEMATICAL STRETCHED KHAH +1EE79..1EE7C ; ALetter # Lo [4] ARABIC MATHEMATICAL STRETCHED DAD..ARABIC MATHEMATICAL STRETCHED DOTLESS BEH +1EE7E ; ALetter # Lo ARABIC MATHEMATICAL STRETCHED DOTLESS FEH +1EE80..1EE89 ; ALetter # Lo [10] ARABIC MATHEMATICAL LOOPED ALEF..ARABIC MATHEMATICAL LOOPED YEH +1EE8B..1EE9B ; ALetter # Lo [17] ARABIC MATHEMATICAL LOOPED LAM..ARABIC MATHEMATICAL LOOPED GHAIN +1EEA1..1EEA3 ; ALetter # Lo [3] ARABIC MATHEMATICAL DOUBLE-STRUCK BEH..ARABIC MATHEMATICAL DOUBLE-STRUCK DAL +1EEA5..1EEA9 ; ALetter # Lo [5] ARABIC MATHEMATICAL DOUBLE-STRUCK WAW..ARABIC MATHEMATICAL DOUBLE-STRUCK YEH +1EEAB..1EEBB ; ALetter # Lo [17] ARABIC MATHEMATICAL DOUBLE-STRUCK LAM..ARABIC MATHEMATICAL DOUBLE-STRUCK GHAIN +1F130..1F149 ; ALetter # So [26] SQUARED LATIN CAPITAL LETTER A..SQUARED LATIN CAPITAL LETTER Z +1F150..1F169 ; ALetter # So [26] NEGATIVE CIRCLED LATIN CAPITAL LETTER A..NEGATIVE CIRCLED LATIN CAPITAL LETTER Z +1F170..1F189 ; ALetter # So [26] NEGATIVE SQUARED LATIN CAPITAL LETTER A..NEGATIVE SQUARED LATIN CAPITAL LETTER Z + +# Total code points: 33973 + +# ================================================ + +003A ; MidLetter # Po COLON +00B7 ; MidLetter # Po MIDDLE DOT +0387 ; MidLetter # Po GREEK ANO TELEIA +055F ; MidLetter # Po ARMENIAN ABBREVIATION MARK +05F4 ; MidLetter # Po HEBREW PUNCTUATION GERSHAYIM +2027 ; MidLetter # Po HYPHENATION POINT +FE13 ; MidLetter # Po PRESENTATION FORM FOR VERTICAL COLON +FE55 ; MidLetter # Po SMALL COLON +FF1A ; MidLetter # Po FULLWIDTH COLON + +# Total code points: 9 + +# ================================================ + +002C ; MidNum # Po COMMA +003B ; MidNum # Po SEMICOLON +037E ; MidNum # Po GREEK QUESTION MARK +0589 ; MidNum # Po ARMENIAN FULL STOP +060C..060D ; MidNum # Po [2] ARABIC COMMA..ARABIC DATE SEPARATOR +066C ; MidNum # Po ARABIC THOUSANDS SEPARATOR +07F8 ; MidNum # Po NKO COMMA +2044 ; MidNum # Sm FRACTION SLASH +FE50 ; MidNum # Po SMALL COMMA +FE54 ; MidNum # Po SMALL SEMICOLON +FF0C ; MidNum # Po FULLWIDTH COMMA +FF1B ; MidNum # Po FULLWIDTH SEMICOLON + +# Total code points: 13 + +# ================================================ + +002E ; MidNumLet # Po FULL STOP +2018 ; MidNumLet # Pi LEFT SINGLE QUOTATION MARK +2019 ; MidNumLet # Pf RIGHT SINGLE QUOTATION MARK +2024 ; MidNumLet # Po ONE DOT LEADER +FE52 ; MidNumLet # Po SMALL FULL STOP +FF07 ; MidNumLet # Po FULLWIDTH APOSTROPHE +FF0E ; MidNumLet # Po FULLWIDTH FULL STOP + +# Total code points: 7 + +# ================================================ + +0030..0039 ; Numeric # Nd [10] DIGIT ZERO..DIGIT NINE +0600..0605 ; Numeric # Cf [6] ARABIC NUMBER SIGN..ARABIC NUMBER MARK ABOVE +0660..0669 ; Numeric # Nd [10] ARABIC-INDIC DIGIT ZERO..ARABIC-INDIC DIGIT NINE +066B ; Numeric # Po ARABIC DECIMAL SEPARATOR +06DD ; Numeric # Cf ARABIC END OF AYAH +06F0..06F9 ; Numeric # Nd [10] EXTENDED ARABIC-INDIC DIGIT ZERO..EXTENDED ARABIC-INDIC DIGIT NINE +07C0..07C9 ; Numeric # Nd [10] NKO DIGIT ZERO..NKO DIGIT NINE +0890..0891 ; Numeric # Cf [2] ARABIC POUND MARK ABOVE..ARABIC PIASTRE MARK ABOVE +08E2 ; Numeric # Cf ARABIC DISPUTED END OF AYAH +0966..096F ; Numeric # Nd [10] DEVANAGARI DIGIT ZERO..DEVANAGARI DIGIT NINE +09E6..09EF ; Numeric # Nd [10] BENGALI DIGIT ZERO..BENGALI DIGIT NINE +0A66..0A6F ; Numeric # Nd [10] GURMUKHI DIGIT ZERO..GURMUKHI DIGIT NINE +0AE6..0AEF ; Numeric # Nd [10] GUJARATI DIGIT ZERO..GUJARATI DIGIT NINE +0B66..0B6F ; Numeric # Nd [10] ORIYA DIGIT ZERO..ORIYA DIGIT NINE +0BE6..0BEF ; Numeric # Nd [10] TAMIL DIGIT ZERO..TAMIL DIGIT NINE +0C66..0C6F ; Numeric # Nd [10] TELUGU DIGIT ZERO..TELUGU DIGIT NINE +0CE6..0CEF ; Numeric # Nd [10] KANNADA DIGIT ZERO..KANNADA DIGIT NINE +0D66..0D6F ; Numeric # Nd [10] MALAYALAM DIGIT ZERO..MALAYALAM DIGIT NINE +0DE6..0DEF ; Numeric # Nd [10] SINHALA LITH DIGIT ZERO..SINHALA LITH DIGIT NINE +0E50..0E59 ; Numeric # Nd [10] THAI DIGIT ZERO..THAI DIGIT NINE +0ED0..0ED9 ; Numeric # Nd [10] LAO DIGIT ZERO..LAO DIGIT NINE +0F20..0F29 ; Numeric # Nd [10] TIBETAN DIGIT ZERO..TIBETAN DIGIT NINE +1040..1049 ; Numeric # Nd [10] MYANMAR DIGIT ZERO..MYANMAR DIGIT NINE +1090..1099 ; Numeric # Nd [10] MYANMAR SHAN DIGIT ZERO..MYANMAR SHAN DIGIT NINE +17E0..17E9 ; Numeric # Nd [10] KHMER DIGIT ZERO..KHMER DIGIT NINE +1810..1819 ; Numeric # Nd [10] MONGOLIAN DIGIT ZERO..MONGOLIAN DIGIT NINE +1946..194F ; Numeric # Nd [10] LIMBU DIGIT ZERO..LIMBU DIGIT NINE +19D0..19D9 ; Numeric # Nd [10] NEW TAI LUE DIGIT ZERO..NEW TAI LUE DIGIT NINE +19DA ; Numeric # No NEW TAI LUE THAM DIGIT ONE +1A80..1A89 ; Numeric # Nd [10] TAI THAM HORA DIGIT ZERO..TAI THAM HORA DIGIT NINE +1A90..1A99 ; Numeric # Nd [10] TAI THAM THAM DIGIT ZERO..TAI THAM THAM DIGIT NINE +1B50..1B59 ; Numeric # Nd [10] BALINESE DIGIT ZERO..BALINESE DIGIT NINE +1BB0..1BB9 ; Numeric # Nd [10] SUNDANESE DIGIT ZERO..SUNDANESE DIGIT NINE +1C40..1C49 ; Numeric # Nd [10] LEPCHA DIGIT ZERO..LEPCHA DIGIT NINE +1C50..1C59 ; Numeric # Nd [10] OL CHIKI DIGIT ZERO..OL CHIKI DIGIT NINE +A620..A629 ; Numeric # Nd [10] VAI DIGIT ZERO..VAI DIGIT NINE +A8D0..A8D9 ; Numeric # Nd [10] SAURASHTRA DIGIT ZERO..SAURASHTRA DIGIT NINE +A900..A909 ; Numeric # Nd [10] KAYAH LI DIGIT ZERO..KAYAH LI DIGIT NINE +A9D0..A9D9 ; Numeric # Nd [10] JAVANESE DIGIT ZERO..JAVANESE DIGIT NINE +A9F0..A9F9 ; Numeric # Nd [10] MYANMAR TAI LAING DIGIT ZERO..MYANMAR TAI LAING DIGIT NINE +AA50..AA59 ; Numeric # Nd [10] CHAM DIGIT ZERO..CHAM DIGIT NINE +ABF0..ABF9 ; Numeric # Nd [10] MEETEI MAYEK DIGIT ZERO..MEETEI MAYEK DIGIT NINE +FF10..FF19 ; Numeric # Nd [10] FULLWIDTH DIGIT ZERO..FULLWIDTH DIGIT NINE +104A0..104A9 ; Numeric # Nd [10] OSMANYA DIGIT ZERO..OSMANYA DIGIT NINE +10D30..10D39 ; Numeric # Nd [10] HANIFI ROHINGYA DIGIT ZERO..HANIFI ROHINGYA DIGIT NINE +10D40..10D49 ; Numeric # Nd [10] GARAY DIGIT ZERO..GARAY DIGIT NINE +11066..1106F ; Numeric # Nd [10] BRAHMI DIGIT ZERO..BRAHMI DIGIT NINE +110BD ; Numeric # Cf KAITHI NUMBER SIGN +110CD ; Numeric # Cf KAITHI NUMBER SIGN ABOVE +110F0..110F9 ; Numeric # Nd [10] SORA SOMPENG DIGIT ZERO..SORA SOMPENG DIGIT NINE +11136..1113F ; Numeric # Nd [10] CHAKMA DIGIT ZERO..CHAKMA DIGIT NINE +111D0..111D9 ; Numeric # Nd [10] SHARADA DIGIT ZERO..SHARADA DIGIT NINE +112F0..112F9 ; Numeric # Nd [10] KHUDAWADI DIGIT ZERO..KHUDAWADI DIGIT NINE +11450..11459 ; Numeric # Nd [10] NEWA DIGIT ZERO..NEWA DIGIT NINE +114D0..114D9 ; Numeric # Nd [10] TIRHUTA DIGIT ZERO..TIRHUTA DIGIT NINE +11650..11659 ; Numeric # Nd [10] MODI DIGIT ZERO..MODI DIGIT NINE +116C0..116C9 ; Numeric # Nd [10] TAKRI DIGIT ZERO..TAKRI DIGIT NINE +116D0..116E3 ; Numeric # Nd [20] MYANMAR PAO DIGIT ZERO..MYANMAR EASTERN PWO KAREN DIGIT NINE +11730..11739 ; Numeric # Nd [10] AHOM DIGIT ZERO..AHOM DIGIT NINE +118E0..118E9 ; Numeric # Nd [10] WARANG CITI DIGIT ZERO..WARANG CITI DIGIT NINE +11950..11959 ; Numeric # Nd [10] DIVES AKURU DIGIT ZERO..DIVES AKURU DIGIT NINE +11BF0..11BF9 ; Numeric # Nd [10] SUNUWAR DIGIT ZERO..SUNUWAR DIGIT NINE +11C50..11C59 ; Numeric # Nd [10] BHAIKSUKI DIGIT ZERO..BHAIKSUKI DIGIT NINE +11D50..11D59 ; Numeric # Nd [10] MASARAM GONDI DIGIT ZERO..MASARAM GONDI DIGIT NINE +11DA0..11DA9 ; Numeric # Nd [10] GUNJALA GONDI DIGIT ZERO..GUNJALA GONDI DIGIT NINE +11DE0..11DE9 ; Numeric # Nd [10] TOLONG SIKI DIGIT ZERO..TOLONG SIKI DIGIT NINE +11F50..11F59 ; Numeric # Nd [10] KAWI DIGIT ZERO..KAWI DIGIT NINE +16130..16139 ; Numeric # Nd [10] GURUNG KHEMA DIGIT ZERO..GURUNG KHEMA DIGIT NINE +16A60..16A69 ; Numeric # Nd [10] MRO DIGIT ZERO..MRO DIGIT NINE +16AC0..16AC9 ; Numeric # Nd [10] TANGSA DIGIT ZERO..TANGSA DIGIT NINE +16B50..16B59 ; Numeric # Nd [10] PAHAWH HMONG DIGIT ZERO..PAHAWH HMONG DIGIT NINE +16D70..16D79 ; Numeric # Nd [10] KIRAT RAI DIGIT ZERO..KIRAT RAI DIGIT NINE +1CCF0..1CCF9 ; Numeric # Nd [10] OUTLINED DIGIT ZERO..OUTLINED DIGIT NINE +1D7CE..1D7FF ; Numeric # Nd [50] MATHEMATICAL BOLD DIGIT ZERO..MATHEMATICAL MONOSPACE DIGIT NINE +1E140..1E149 ; Numeric # Nd [10] NYIAKENG PUACHUE HMONG DIGIT ZERO..NYIAKENG PUACHUE HMONG DIGIT NINE +1E2F0..1E2F9 ; Numeric # Nd [10] WANCHO DIGIT ZERO..WANCHO DIGIT NINE +1E4F0..1E4F9 ; Numeric # Nd [10] NAG MUNDARI DIGIT ZERO..NAG MUNDARI DIGIT NINE +1E5F1..1E5FA ; Numeric # Nd [10] OL ONAL DIGIT ZERO..OL ONAL DIGIT NINE +1E950..1E959 ; Numeric # Nd [10] ADLAM DIGIT ZERO..ADLAM DIGIT NINE +1FBF0..1FBF9 ; Numeric # Nd [10] SEGMENTED DIGIT ZERO..SEGMENTED DIGIT NINE + +# Total code points: 784 + +# ================================================ + +005F ; ExtendNumLet # Pc LOW LINE +202F ; ExtendNumLet # Zs NARROW NO-BREAK SPACE +203F..2040 ; ExtendNumLet # Pc [2] UNDERTIE..CHARACTER TIE +2054 ; ExtendNumLet # Pc INVERTED UNDERTIE +FE33..FE34 ; ExtendNumLet # Pc [2] PRESENTATION FORM FOR VERTICAL LOW LINE..PRESENTATION FORM FOR VERTICAL WAVY LOW LINE +FE4D..FE4F ; ExtendNumLet # Pc [3] DASHED LOW LINE..WAVY LOW LINE +FF3F ; ExtendNumLet # Pc FULLWIDTH LOW LINE + +# Total code points: 11 + +# ================================================ + +200D ; ZWJ # Cf ZERO WIDTH JOINER + +# Total code points: 1 + +# ================================================ + +0020 ; WSegSpace # Zs SPACE +1680 ; WSegSpace # Zs OGHAM SPACE MARK +2000..2006 ; WSegSpace # Zs [7] EN QUAD..SIX-PER-EM SPACE +2008..200A ; WSegSpace # Zs [3] PUNCTUATION SPACE..HAIR SPACE +205F ; WSegSpace # Zs MEDIUM MATHEMATICAL SPACE +3000 ; WSegSpace # Zs IDEOGRAPHIC SPACE + +# Total code points: 14 + +# EOF diff --git a/src/UnicodeTrieGenerator/UnicodeTrieGenerator.csproj b/src/UnicodeTrieGenerator/UnicodeTrieGenerator.csproj index 07f7d24b..7968581c 100644 --- a/src/UnicodeTrieGenerator/UnicodeTrieGenerator.csproj +++ b/src/UnicodeTrieGenerator/UnicodeTrieGenerator.csproj @@ -19,7 +19,7 @@ - + @@ -33,6 +33,7 @@ + diff --git a/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-ltr-.png b/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-ltr-.png new file mode 100644 index 00000000..32e6fcff --- /dev/null +++ b/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-ltr-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99a8070a7a75978b0c4dddcb6bb416eeeea9d741d75e8bf8f209f4063c02f9ef +size 6408 diff --git a/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-rtl-.png b/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-rtl-.png new file mode 100644 index 00000000..364c0719 --- /dev/null +++ b/tests/Images/ReferenceOutput/CaretPosition_DrawsMovedCarets_-rtl-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0a03900df20f4cae8f5870376e87b1305f634e7870b90737737abca95f11f5ad +size 6488 diff --git a/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-ltr-.png b/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-ltr-.png new file mode 100644 index 00000000..f238c854 --- /dev/null +++ b/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-ltr-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3e11fdb4177142d0713e9591ee7d1600e8b190ddc48cf1009de2174347e3b0dc +size 5710 diff --git a/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-rtl-.png b/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-rtl-.png new file mode 100644 index 00000000..a356012b --- /dev/null +++ b/tests/Images/ReferenceOutput/CaretPosition_DrawsStartAndEndCarets_-rtl-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:14e5a4dcd963af273a21f422047a1c07e098a79d31556df151579fe6f4a20c31 +size 5677 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsBidiDragSelection-.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsBidiDragSelection-.png new file mode 100644 index 00000000..8a42b9db --- /dev/null +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsBidiDragSelection-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d3df6f23740182b25cf463f2fb302c4a1aec118ec0425bf1fe1175687c1b70e0 +size 2895 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalBottomTop.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalBottomTop.png new file mode 100644 index 00000000..4ce5e4ef --- /dev/null +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalBottomTop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1ebfa8fa6f989881bd2710dda4a742d464ecc8572c16881da62e421ba8f1cbc +size 11364 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalTopBottom.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalTopBottom.png new file mode 100644 index 00000000..eeec8bbd --- /dev/null +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_HorizontalTopBottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:01de68e2aad4ada692425d47efb63a86d5ce653302e344013b85a413e96aa702 +size 11324 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalLeftRight.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalLeftRight.png new file mode 100644 index 00000000..d6d3a720 --- /dev/null +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9753ed352d2e8cfc5fc68afd93960fa629901312712044b5315ff5787905afc4 +size 16210 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedLeftRight.png new file mode 100644 index 00000000..de2c5aca --- /dev/null +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3a8eb819115efc22bdfdfa8f2f13a7dd46f540d5c85fcde920beb5cbf083ac0b +size 11889 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedRightLeft.png new file mode 100644 index 00000000..cd1fd67b --- /dev/null +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalMixedRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3df1b4104150e1c149571c0ff3bc1046506a751044b1579a614068745b173385 +size 11772 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalRightLeft.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalRightLeft.png new file mode 100644 index 00000000..fdafdbee --- /dev/null +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections_VerticalRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19d4f5c5ce3f93c17f83afbf40fb458f65a33e7420d9de0b4922bff7b74cb8aa +size 16123 diff --git a/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsSelectionWithBlankLine-.png b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsSelectionWithBlankLine-.png new file mode 100644 index 00000000..c8789ccf --- /dev/null +++ b/tests/Images/ReferenceOutput/GraphemeMetrics_GetSelectionBounds_DrawsSelectionWithBlankLine-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f135536b91c0d6a913b0a02291e38e8e0b0ec00216e321700c6b044134a39413 +size 6746 diff --git a/tests/Images/ReferenceOutput/LineLayoutEnumerator_DrawsManualFlowAroundCircle-.png b/tests/Images/ReferenceOutput/LineLayoutEnumerator_DrawsManualFlowAroundCircle-.png new file mode 100644 index 00000000..0c74c4ac --- /dev/null +++ b/tests/Images/ReferenceOutput/LineLayoutEnumerator_DrawsManualFlowAroundCircle-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fbb7e31cf9ca4920734eb8b336acaf7383895226022709eca1e48dffdbb002ba +size 44419 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalBottomTop.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalBottomTop.png new file mode 100644 index 00000000..04942acb --- /dev/null +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalBottomTop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:19652fdf93a600175cfb7f30f452bff5b3a6de0a5fa272ffddb6bfa0fb33b384 +size 10726 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalTopBottom.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalTopBottom.png new file mode 100644 index 00000000..01ce77a8 --- /dev/null +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_HorizontalTopBottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2395d995077de91bfe2b1e97e5acefc88800b800407864e0d0c653b786e9b693 +size 10757 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalLeftRight.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalLeftRight.png new file mode 100644 index 00000000..fea0a818 --- /dev/null +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11da0a0d5f78b8a4f8e9a38cdccc03160b0a2fbcf1a26434d8599746bf18c0ab +size 16074 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedLeftRight.png new file mode 100644 index 00000000..7e591baa --- /dev/null +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6aadd2e4176b4b5d8c7b56a4c357d407ec9b65b850f092c332d961ef12db04e8 +size 12168 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedRightLeft.png new file mode 100644 index 00000000..0bbdfa4b --- /dev/null +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalMixedRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8c0b96ececc4f37bb98865811c2afa76336f9b7e700897543cf93415140bc2a8 +size 12038 diff --git a/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalRightLeft.png b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalRightLeft.png new file mode 100644 index 00000000..cbdc4ad7 --- /dev/null +++ b/tests/Images/ReferenceOutput/LineMetrics_StartAndExtent_DrawsLineBoxes_VerticalRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e6c31d7517327f65f3126cc043c492e0644eac272192cf2d4c966209f04cc1f6 +size 15802 diff --git a/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png b/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png index aaafdbe7..7b279ff4 100644 --- a/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png +++ b/tests/Images/ReferenceOutput/RenderingTextIncludesAllGlyphs_1900.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:12a29e2de182ea2e24451898ac17b1cde2d0d9c61cb6f15318ea4c06aab446f3 -size 23611 +oid sha256:380d5c5fd37b54c591a9fab3cbd70a41970cf5bcd268691bba87ec8f608b6e57 +size 34790 diff --git a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-normal-.png b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-normal-.png new file mode 100644 index 00000000..882cf757 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-normal-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d023b62cf1c2f48f7c067ac480869eb4f2b23fcf1a3974c2ee37b96dbd50bf79 +size 3530 diff --git a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-override-.png b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-override-.png new file mode 100644 index 00000000..e5459c26 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--ltr-override-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d7df88d6364e48612b872fb7b1ab3040de043ac6e08f77a53d37eaec44e6a24a +size 3550 diff --git a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-normal-.png b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-normal-.png new file mode 100644 index 00000000..aebec3f1 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-normal-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:392aa8124b98f143ded9b7d27cf5d9918591dd39b2f7374a1e85b3915cadcafc +size 3596 diff --git a/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-override-.png b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-override-.png new file mode 100644 index 00000000..820e084c --- /dev/null +++ b/tests/Images/ReferenceOutput/TextBidiMode_DrawsMixedBidiLayout_430--rtl-override-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:59d7cf4e6c2d2e6dd1487123fd1e50f3352564f3320c35457cfbac80184cacbf +size 3528 diff --git a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--custom-.png b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--custom-.png new file mode 100644 index 00000000..1a21e99f --- /dev/null +++ b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--custom-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6e2e49e3cc239c0c91055c3a1aafc6d0bd6e0ac4f01adc17c546c8e5b80a709e +size 2316 diff --git a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--none-.png b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--none-.png new file mode 100644 index 00000000..2987e617 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--none-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a33cc9a3e9c925f58f3ec3822504e1164a58bc5fe7dba6543227557e2bde6696 +size 2110 diff --git a/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--standard-.png b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--standard-.png new file mode 100644 index 00000000..917dd8b0 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextEllipsis_DrawsMaxLinesMarker_210--standard-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a21543a6d391edb6c682b1ae90af8b761d57f6ba3a55e9b8bbdb1fb576d0cfc8 +size 2175 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalBottomTop.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalBottomTop.png new file mode 100644 index 00000000..d1815417 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalBottomTop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7b2b4d5f310eecfae93f9bff0531ab2a7741a24acfe95de30638974467d3d5a3 +size 7486 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalTopBottom.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalTopBottom.png new file mode 100644 index 00000000..b7358fa4 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-HorizontalTopBottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:20aba76bd33e13e45d5822ab8bc014624393ce507ef4790ef737b1d15fa77ead +size 7492 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedLeftRight.png new file mode 100644 index 00000000..92d71d78 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c1712941c5188637c7339ef8765c1322ffbd71d88af2d0a098ae0daed1100c95 +size 7247 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedRightLeft.png new file mode 100644 index 00000000..ec465d6a --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_109.48-VerticalMixedRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:04863a6bc815ec8ad71a551e0f9169031854b84dc42cb366b97da5ba00766bf6 +size 7191 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalLeftRight.png new file mode 100644 index 00000000..5efaa41b --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b01ed4070f9d18b20a885ab1eee5b787b370eef2168963412569d06dc324dfaa +size 9236 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalRightLeft.png new file mode 100644 index 00000000..0b6a37ba --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsBidiSoftHyphenMarker_327.76-VerticalRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0ead5ddc41c56cf3017dd457cec1d078301cfeb1c863a71b0c159f00add85b4d +size 9084 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalLeftRight.png new file mode 100644 index 00000000..a9355ea2 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:89ecaf4da858091b7a07aa9a52a6ba7cec9977b80ac085cf5a98c68ad347eab0 +size 3270 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalRightLeft.png new file mode 100644 index 00000000..7f406695 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_246.16-VerticalRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:71e9fbc70d5f379c6cbf6a39891939c952ed9c382f868b894d451c4aab121457 +size 3275 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalBottomTop.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalBottomTop.png new file mode 100644 index 00000000..4fd0a732 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalBottomTop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:c961b52c626acf4beef744e7606f725f2da9c701c16ceccf8880d23cebecf7c6 +size 3239 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalTopBottom.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalTopBottom.png new file mode 100644 index 00000000..4c346002 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-HorizontalTopBottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f3f943d2c4b1b070787a3814adff064eca0ae87d2817271ed8ef1e913d80ec3a +size 3256 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedLeftRight.png new file mode 100644 index 00000000..08febfb4 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a522dba8cf3fca4b40c411a986a035da73412456c37b1f88a496644b95567e26 +size 3038 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedRightLeft.png new file mode 100644 index 00000000..cc657e6f --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker_89.17-VerticalMixedRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3170a198b6f1749865a6421f84248e6deb5f2f035f9cf0121106558e46ae877e +size 3041 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalBottomTop.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalBottomTop.png new file mode 100644 index 00000000..9a8f0bdc --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalBottomTop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7e8fb48aee2195182d786cdc5b08c5270a377ecccac760c068b54ec01e1878db +size 4285 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalTopBottom.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalTopBottom.png new file mode 100644 index 00000000..f336f628 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-HorizontalTopBottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:355adc5176c29b1e72bd042adc5e440516599c57ef742794037ca3e54e9f9556 +size 4276 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedLeftRight.png new file mode 100644 index 00000000..dae5a7c7 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:83b64489947de6a2279c7fb8ab4fbf70217252245d6b596a3573e7495368e4b4 +size 4041 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedRightLeft.png new file mode 100644 index 00000000..2d27bda8 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_100.48-VerticalMixedRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:51e674f3bc57adff8458bd16bddaad5259594c0503165dadac6cc2b6a244286e +size 4009 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalLeftRight.png new file mode 100644 index 00000000..f86fd31f --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:60a91d50c803a82a521d3443688e1a47c6900976396e1496d7bdb3edf7b3f1fb +size 7966 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalRightLeft.png new file mode 100644 index 00000000..dbd0f8ce --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker_326.19998-VerticalRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:77ce2e45a1ae9f5c4e38227f5c09de6d23e6cbaa4807e220c2c56465cdb5f9ad +size 8109 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalBottomTop.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalBottomTop.png new file mode 100644 index 00000000..b6574eee --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalBottomTop.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6b14fb9547656d10a0686379af6ab33ef6133ad99ee09ba8a7facc810ca8ef15 +size 4131 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalTopBottom.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalTopBottom.png new file mode 100644 index 00000000..e20455ea --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-HorizontalTopBottom.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:d77c56d7ea96cd93034761b14122a19ea76240d02f10c44b6b2272ac39843c2d +size 4132 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedLeftRight.png new file mode 100644 index 00000000..2bab6b19 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f82fd5cf59b10d73256adcfddc36a3d5da64b33123a6a341632e71def458600a +size 4022 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedRightLeft.png new file mode 100644 index 00000000..fbcc0e22 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_101.61001-VerticalMixedRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:11d9bf37b0c3e878684d95db8487af79dbce24aa373837855fed2bfcfd86e852 +size 6353 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalLeftRight.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalLeftRight.png new file mode 100644 index 00000000..4058e45b --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalLeftRight.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e87d2e735f322fd9843b5624566af47d38fe344979f355c356d24a0f67184e8e +size 4402 diff --git a/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalRightLeft.png b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalRightLeft.png new file mode 100644 index 00000000..154becc6 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker_326.76-VerticalRightLeft.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f8554605300873c2a52a90a7bc5dbbb1d8c8c918fb84bb43919757e9d59d0092 +size 4364 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_RightToLeft-TextJustification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_RightToLeft-TextJustification_InterCharacter_.png deleted file mode 100644 index a7352539..00000000 --- a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_RightToLeft-TextJustification_InterCharacter_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:7ff6cc8db20109e41bc7bcf42dd4b553fde97934e239a7b8d7e9f0b738bc7be1 -size 8798 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_LeftToRight-TextJustification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_False_.png similarity index 100% rename from tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_direction_LeftToRight-TextJustification_InterCharacter_.png rename to tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_False_.png diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_True_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_True_.png new file mode 100644 index 00000000..69a7d589 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Horizontal_400-_rtl_True_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:5401404d52aa07eb593e3b43365b78268e759114963ac7e8700d20cad7c5edf6 +size 8667 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_RightToLeft-TextJustification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_RightToLeft-TextJustification_InterCharacter_.png deleted file mode 100644 index 956996ec..00000000 --- a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_RightToLeft-TextJustification_InterCharacter_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:f0c63fc526faeffe4914e2cb83f48dd379eb41591b86c41722e7d628ae2d866e -size 7400 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_LeftToRight-TextJustification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_False_.png similarity index 100% rename from tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_direction_LeftToRight-TextJustification_InterCharacter_.png rename to tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_False_.png diff --git a/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_True_.png b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_True_.png new file mode 100644 index 00000000..a98bbc43 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterCharacter_Vertical_400-_rtl_True_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:22e79da215e7288c4a0874b74ba5e4afdf6efaa584c4e9d48dcd76d67537c558 +size 7448 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_RightToLeft-TextJustification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_RightToLeft-TextJustification_InterWord_.png deleted file mode 100644 index 0b95439b..00000000 --- a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_RightToLeft-TextJustification_InterWord_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:474ca639cd51985cd05a0920cade063dce2f01f6cf47faf6a37d4b8d666dae7d -size 8771 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_LeftToRight-TextJustification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_False_.png similarity index 100% rename from tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_direction_LeftToRight-TextJustification_InterWord_.png rename to tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_False_.png diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_True_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_True_.png new file mode 100644 index 00000000..9a83b516 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Horizontal_400-_rtl_True_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:99dba4be939bf438317c8946adbe64c76bab9679e1bf780bb988857735481119 +size 8694 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_RightToLeft-TextJustification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_RightToLeft-TextJustification_InterWord_.png deleted file mode 100644 index e4287da1..00000000 --- a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_RightToLeft-TextJustification_InterWord_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:ee2fb06ba4a6d6a3dfed2686aad8311099ba08fb510fe3b26a37be1d0ccc6d5f -size 6786 diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_LeftToRight-TextJustification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_False_.png similarity index 100% rename from tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_direction_LeftToRight-TextJustification_InterWord_.png rename to tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_False_.png diff --git a/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_True_.png b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_True_.png new file mode 100644 index 00000000..9c86c064 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_InterWord_Vertical_400-_rtl_True_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f614cbd37029a1901e044c878a36be8c97e17bb83a9d4f6b6ad1b5aa2b45653e +size 6746 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterCharacter_.png deleted file mode 100644 index f0068197..00000000 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterCharacter_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:abc5287220cccd527d74fea9efc1364c2207e8f60e081c3f34342b49373ab57b -size 16157 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterWord_.png deleted file mode 100644 index 3cfc2b4d..00000000 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterWord_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:90382b8bbcad1e176a4c84554d8810616372f1f9f34285d289f369cd695c0369 -size 16064 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_LeftToRight-justification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png similarity index 100% rename from tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_LeftToRight-justification_InterCharacter_.png rename to tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_LeftToRight-justification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png similarity index 100% rename from tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines_400-_direction_LeftToRight-justification_InterWord_.png rename to tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png new file mode 100644 index 00000000..f0cb1d5b --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:753d588cef8ea445b9ac1cc484b6985084db980c9a876439ea1144c709d04f53 +size 16010 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png new file mode 100644 index 00000000..40ee1c04 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Horizontal_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:b32f3586df3d993ebc8a3ce54a644123ad87f26a63fcc55f33b53eae1fad813d +size 16108 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterCharacter_.png deleted file mode 100644 index 2a0109fb..00000000 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterCharacter_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:79e8ce50c657f4e49c8f6e37e51c108fd23135b7891fc5cd3c7c8f018aa53b5e -size 8764 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterWord_.png deleted file mode 100644 index 14bf3110..00000000 --- a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_RightToLeft-justification_InterWord_.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:84fa7591a4868359e09314a1b032f123704d7ca68ca1dd6b1296437fc111393f -size 7824 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_LeftToRight-justification_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png similarity index 100% rename from tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_LeftToRight-justification_InterCharacter_.png rename to tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterCharacter_.png diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_LeftToRight-justification_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png similarity index 100% rename from tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines_400-_direction_LeftToRight-justification_InterWord_.png rename to tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_False-mode_InterWord_.png diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png new file mode 100644 index 00000000..a2c1f685 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterCharacter_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:520858c64ca830f920557e7f42ba6adc666286af195a4b1f3d3983e7b3a4caca +size 9274 diff --git a/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png new file mode 100644 index 00000000..9036df4e --- /dev/null +++ b/tests/Images/ReferenceOutput/TextJustification_MultiParagraph_Vertical_SkipsFinalLines_400-_rtl_True-mode_InterWord_.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:f51ebec9437dfca9d2e5ff7ea5a8650f0bd2f2efc7ff6f18ba2cb2289fe54bbe +size 8393 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-abovebaseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-abovebaseline-.png new file mode 100644 index 00000000..77eae086 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-abovebaseline-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:bb734bcdee5b097d060bc78beaa5db5c47820a6f464c2e01e9a6d2fddaf4e7cf +size 7864 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-baseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-baseline-.png new file mode 100644 index 00000000..9e50e1df --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-baseline-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ad30f855dbfc6b6a54ea9f313fce89b9fb36138bea9c9ff2d08a3103a9284b53 +size 7888 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-belowbaseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-belowbaseline-.png new file mode 100644 index 00000000..c8ebe7ea --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-belowbaseline-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1987a6e29dabb599bbcbe14ed3e223b370f46a75d32b1b77db5bed7023e550f9 +size 8008 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-bottom-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-bottom-.png new file mode 100644 index 00000000..fde11267 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-bottom-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:333ff2fb0ced7ce9f7f16c0cd17e40f6ea6b5395ff5dd10cacfd020d6a6b797b +size 7855 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-middle-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-middle-.png new file mode 100644 index 00000000..ef22b9a3 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-middle-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fb81d8c045318c8ac6ca1b451b869410241a4d1b66b7e960fde9f6ba595192f6 +size 7851 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-top-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-top-.png new file mode 100644 index 00000000..8a7dbf78 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsInlineReservedSpace_-top-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:8cccf405a8bd1e7353c6a944021676e52f5f47b84f194423356e96862c5847c8 +size 7846 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-abovebaseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-abovebaseline-.png new file mode 100644 index 00000000..621b25df --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-abovebaseline-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:e187d9c228fcb8d06ee224c41b09443484dbc1c27a1f343285da21db493ee90e +size 10199 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-baseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-baseline-.png new file mode 100644 index 00000000..0e123ad1 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-baseline-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:24ddd3d09e23531cc48a77fdb3b861a9003642083d310cbb170c3e0a1ed3f189 +size 10607 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-belowbaseline-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-belowbaseline-.png new file mode 100644 index 00000000..688e1657 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-belowbaseline-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:1a48050c20096b5c54a70630d152f84a0e575c699511077a73dbe9c717bf0ae9 +size 10626 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-bottom-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-bottom-.png new file mode 100644 index 00000000..1c11a0ef --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-bottom-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:10382b94871d0644887f9f7677b6e67e59f11a2ed63b671053f67037abf1193e +size 10259 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-middle-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-middle-.png new file mode 100644 index 00000000..ee56e679 --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-middle-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:cc200e382201a33f1e9b5ed20300eb2b6c282e48410a2bd12901448371336502 +size 10507 diff --git a/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-top-.png b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-top-.png new file mode 100644 index 00000000..97e14f9b --- /dev/null +++ b/tests/Images/ReferenceOutput/TextPlaceholder_DrawsOversizedInlineReservedSpace_-top-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:6ca40f2c4c141c2678f0e0b004fd634a7a051ff80e6041bf3cf70e69f2a722cc +size 10521 diff --git a/tests/Images/ReferenceOutput/WordMetrics_GetSelectionBounds_DrawsWordSelections-.png b/tests/Images/ReferenceOutput/WordMetrics_GetSelectionBounds_DrawsWordSelections-.png new file mode 100644 index 00000000..c45be5dd --- /dev/null +++ b/tests/Images/ReferenceOutput/WordMetrics_GetSelectionBounds_DrawsWordSelections-.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:7c7729a5ba9258737ec37e73e854c1b090030e51a1cf7048d5f82392340c51de +size 5188 diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/MeasureTextBenchmark.cs b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/MeasureTextBenchmark.cs index 674cac2b..b5726cd7 100644 --- a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/MeasureTextBenchmark.cs +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/MeasureTextBenchmark.cs @@ -75,7 +75,7 @@ public void Dispose() public string Text { get; set; } = string.Empty; [Benchmark] - public void SixLaborsFonts() => TextMeasurer.MeasureSize(this.Text, this.textOptions); + public void SixLaborsFonts() => TextMeasurer.MeasureBounds(this.Text, this.textOptions); // [Benchmark] // public void SkiaSharp() => this.paint.MeasureText(this.Text); diff --git a/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/TextBlockRepeatedLayoutBenchmark.cs b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/TextBlockRepeatedLayoutBenchmark.cs new file mode 100644 index 00000000..8a1d729f --- /dev/null +++ b/tests/SixLabors.Fonts.Benchmarks/SixLabors.Fonts.Benchmarks/TextBlockRepeatedLayoutBenchmark.cs @@ -0,0 +1,107 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using BenchmarkDotNet.Attributes; + +namespace SixLabors.Fonts.Benchmarks; + +/// +/// Defines the text shape used by . +/// +public enum TextBlockBenchmarkScenario +{ + /// + /// Latin paragraphs with normal wrapping opportunities. + /// + LatinParagraphs, + + /// + /// Mixed left-to-right and right-to-left text with numbers. + /// + MixedBidi +} + +/// +/// Compares repeated layout at several wrapping lengths with and without prepared text reuse. +/// +[MemoryDiagnoser] +[MediumRunJob] +public class TextBlockRepeatedLayoutBenchmark +{ + private readonly float[] wrappingLengths = [240, 320, 400, 520]; + private string text = string.Empty; + private TextOptions textMeasurerOptions = null!; + private TextBlock textBlock = null!; + + /// + /// Gets or sets the text scenario used by the benchmark. + /// + [Params(TextBlockBenchmarkScenario.LatinParagraphs, TextBlockBenchmarkScenario.MixedBidi)] + public TextBlockBenchmarkScenario Scenario { get; set; } + + /// + /// Initializes the input text, options, and prepared block for each scenario. + /// + [GlobalSetup] + public void Setup() + { + Font font = SystemFonts.Get("Arial").CreateFont(16, FontStyle.Regular); + this.text = CreateText(this.Scenario); + this.textMeasurerOptions = new TextOptions(font) { WrappingLength = -1 }; + + TextOptions textBlockOptions = new(font) { WrappingLength = -1 }; + this.textBlock = new TextBlock(this.text, textBlockOptions); + } + + /// + /// Measures each wrapping length through the existing static measurement API. + /// + /// An aggregate value that keeps the measured work observable. + [Benchmark(Baseline = true)] + public float TextMeasurerMeasureBoundsRepeatedWrappingLengths() + { + float result = 0; + for (int i = 0; i < this.wrappingLengths.Length; i++) + { + this.textMeasurerOptions.WrappingLength = this.wrappingLengths[i]; + FontRectangle size = TextMeasurer.MeasureBounds(this.text, this.textMeasurerOptions); + result += size.Width + size.Height; + } + + return result; + } + + /// + /// Measures each wrapping length through one prepared . + /// + /// An aggregate value that keeps the measured work observable. + [Benchmark] + public float TextBlockMeasureBoundsRepeatedWrappingLengths() + { + float result = 0; + for (int i = 0; i < this.wrappingLengths.Length; i++) + { + FontRectangle size = this.textBlock.MeasureBounds(this.wrappingLengths[i]); + result += size.Width + size.Height; + } + + return result; + } + + private static string CreateText(TextBlockBenchmarkScenario scenario) + => scenario switch + { + TextBlockBenchmarkScenario.LatinParagraphs => + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed non risus. " + + "Suspendisse lectus tortor, dignissim sit amet, adipiscing nec, ultricies sed, dolor. " + + "Cras elementum ultrices diam. Maecenas ligula massa, varius a, semper congue, euismod non, mi.\n\n" + + "Praesent blandit odio eu enim. Pellentesque sed dui ut augue blandit sodales. " + + "Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae.", + + // Hebrew and Arabic runs exercise bidi resolution and per-line visual reordering. + _ => + "The paragraph begins in English, then says שלום to the reader, adds the number 456, " + + "and continues with Arabic مرحبا before returning to Latin text.\n\n" + + "A second paragraph mentions דוד and فاطمة so line breaking crosses mixed scripts naturally." + }; +} diff --git a/tests/SixLabors.Fonts.Tests/Accents.cs b/tests/SixLabors.Fonts.Tests/Accents.cs index 3395abf4..8f0a3b1a 100644 --- a/tests/SixLabors.Fonts.Tests/Accents.cs +++ b/tests/SixLabors.Fonts.Tests/Accents.cs @@ -18,7 +18,7 @@ public void MeasuringAccentedCharacterDoesNotThrow(char c) { Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 1f); - _ = TextMeasurer.MeasureSize(c.ToString(), new TextOptions(font)); + _ = TextMeasurer.MeasureBounds(c.ToString(), new TextOptions(font)); } [Theory] @@ -34,6 +34,6 @@ public void MeasuringWordWithAccentedCharacterDoesNotThrow(char c) { Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 1f); - _ = TextMeasurer.MeasureSize($"abc{c}def", new TextOptions(font)); + _ = TextMeasurer.MeasureBounds($"abc{c}def", new TextOptions(font)); } } diff --git a/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs b/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs index 2d231994..0578a034 100644 --- a/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontCodePointsTests.cs @@ -12,11 +12,12 @@ public void TtfTest() { Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); - IReadOnlyList codePoints = font.FontMetrics.GetAvailableCodePoints(); - IEnumerable codePointValues = codePoints.Select(x => x.Value); + ReadOnlyMemory codePoints = font.FontMetrics.GetAvailableCodePoints(); + CodePoint[] codePointArray = codePoints.ToArray(); + IEnumerable codePointValues = codePointArray.Select(x => x.Value); // Compare with https://everythingfonts.com/ttfdump - Assert.Equal(257, codePoints.Count); + Assert.Equal(257, codePoints.Length); // Compare with https://fontdrop.info/ Assert.Contains(0x0000, codePointValues); @@ -29,7 +30,7 @@ public void TtfTest() Assert.Contains(0xFFFF, codePointValues); HashSet glyphIds = []; - foreach (CodePoint codePoint in codePoints) + foreach (CodePoint codePoint in codePoints.Span) { Assert.True(font.TryGetGlyphs(codePoint, out Glyph? glyph)); glyphIds.Add(glyph.Value.GlyphMetrics.GlyphId); @@ -44,11 +45,12 @@ public void WoffTest() { Font font = TestFonts.GetFont(TestFonts.SimpleFontFileWoff, 12); - IReadOnlyList codePoints = font.FontMetrics.GetAvailableCodePoints(); - IEnumerable codePointValues = codePoints.Select(x => x.Value); + ReadOnlyMemory codePoints = font.FontMetrics.GetAvailableCodePoints(); + CodePoint[] codePointArray = codePoints.ToArray(); + IEnumerable codePointValues = codePointArray.Select(x => x.Value); // Compare with https://everythingfonts.com/ttfdump - Assert.Equal(257, codePoints.Count); + Assert.Equal(257, codePoints.Length); // Compare with https://fontdrop.info/ Assert.Contains(0x0000, codePointValues); @@ -61,7 +63,7 @@ public void WoffTest() Assert.Contains(0xFFFF, codePointValues); HashSet glyphIds = []; - foreach (CodePoint codePoint in codePoints) + foreach (CodePoint codePoint in codePoints.Span) { Assert.True(font.TryGetGlyphs(codePoint, out Glyph? glyph)); glyphIds.Add(glyph.Value.GlyphMetrics.GlyphId); diff --git a/tests/SixLabors.Fonts.Tests/FontDescriptionTests.cs b/tests/SixLabors.Fonts.Tests/FontDescriptionTests.cs index 72706ebf..e815249f 100644 --- a/tests/SixLabors.Fonts.Tests/FontDescriptionTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontDescriptionTests.cs @@ -115,17 +115,19 @@ public void LoadFontDescription_CultureNamePriority_ExactMatch() [Fact] public void CanLoadFontCollectionDescriptionsFromPath() { - FontDescription[] descriptions = FontDescription.LoadFontCollectionDescriptions(TestFonts.SimpleTrueTypeCollection); - Assert.NotNull(descriptions); - Assert.Equal(2, descriptions.Length); + ReadOnlyMemory descriptions = FontDescription.LoadFontCollectionDescriptions(TestFonts.SimpleTrueTypeCollection); + ReadOnlySpan descriptionSpan = descriptions.Span; - FontDescription description = descriptions[0]; + Assert.False(descriptions.IsEmpty); + Assert.Equal(2, descriptionSpan.Length); + + FontDescription description = descriptionSpan[0]; Assert.Equal("SixLaborsSampleAB", description.FontFamilyInvariantCulture); Assert.Equal("SixLaborsSampleAB regular", description.FontNameInvariantCulture); Assert.Equal("Regular", description.FontSubFamilyNameInvariantCulture); Assert.Equal(FontStyle.Regular, description.Style); - description = descriptions[1]; + description = descriptionSpan[1]; Assert.Equal("Open Sans", description.FontFamilyInvariantCulture); Assert.Equal("Open Sans", description.FontNameInvariantCulture); Assert.Equal("Regular", description.FontSubFamilyNameInvariantCulture); diff --git a/tests/SixLabors.Fonts.Tests/FontFamilyTests.cs b/tests/SixLabors.Fonts.Tests/FontFamilyTests.cs index 5f2e59bc..45b2b1c8 100644 --- a/tests/SixLabors.Fonts.Tests/FontFamilyTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontFamilyTests.cs @@ -46,15 +46,17 @@ public void NotEqualTests() [ClassData(typeof(SystemFontFamiliesTheoryData))] public void HasPathTests(FontFamily family) { - Assert.True(family.TryGetPaths(out IEnumerable familyPaths)); - Assert.NotNull(familyPaths); - Assert.True(familyPaths.Any()); + Assert.True(family.TryGetPaths(out ReadOnlyMemory familyPaths)); + Assert.False(familyPaths.IsEmpty); - foreach (FontStyle style in family.GetAvailableStyles()) + string[] familyPathArray = familyPaths.ToArray(); + ReadOnlySpan styles = family.GetAvailableStyles().Span; + + foreach (FontStyle style in styles) { Font font = family.CreateFont(12, style); Assert.True(font.TryGetPath(out string fontPath)); - Assert.Contains(fontPath, familyPaths); + Assert.Contains(fontPath, familyPathArray); } } @@ -64,7 +66,7 @@ public void Throws_FontException_CreateFont_WhenDefault() [Fact] public void Throws_FontException_TryGetPath_WhenDefault() - => Assert.Throws(() => default(FontFamily).TryGetPaths(out IEnumerable _)); + => Assert.Throws(() => default(FontFamily).TryGetPaths(out ReadOnlyMemory _)); [Fact] public void Throws_FontException_TryGetMetrics_WhenDefault() diff --git a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs index 9894c976..577b3cec 100644 --- a/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontLoaderTests.cs @@ -19,7 +19,7 @@ public void Issue21_LoopDetectedLoadingGlyphs() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out GlyphMetrics _)); + out FontGlyphMetrics _)); } [Fact] diff --git a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs index 7d19a71d..be357a0e 100644 --- a/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs +++ b/tests/SixLabors.Fonts.Tests/FontMetricsTests.cs @@ -150,7 +150,7 @@ public void GlyphMetricsMatchesReference() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out GlyphMetrics metrics)); + out FontGlyphMetrics metrics)); Assert.Equal(codePoint, metrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, metrics.UnitsPerEm); @@ -179,7 +179,7 @@ public void GlyphMetricsMatchesReference_WithWoff1format() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out GlyphMetrics metrics)); + out FontGlyphMetrics metrics)); Assert.Equal(codePoint, metrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, metrics.UnitsPerEm); @@ -208,7 +208,7 @@ public void GlyphMetricsMatchesReference_WithWoff2format() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out GlyphMetrics metrics)); + out FontGlyphMetrics metrics)); Assert.Equal(codePoint, metrics.CodePoint); Assert.Equal(font.FontMetrics.UnitsPerEm, metrics.UnitsPerEm); @@ -237,7 +237,7 @@ public void GlyphMetricsVerticalMatchesReference() TextDecorations.None, LayoutMode.HorizontalTopBottom, ColorFontSupport.None, - out GlyphMetrics metrics)); + out FontGlyphMetrics metrics)); // Position 0. Assert.Equal(codePoint, metrics.CodePoint); diff --git a/tests/SixLabors.Fonts.Tests/GlyphTests.cs b/tests/SixLabors.Fonts.Tests/GlyphTests.cs index 5d744623..aec0cf2a 100644 --- a/tests/SixLabors.Fonts.Tests/GlyphTests.cs +++ b/tests/SixLabors.Fonts.Tests/GlyphTests.cs @@ -149,8 +149,8 @@ public void EmojiWidthIsComputedCorrectlyWithSubstitutionOnZwj() const string text = "\U0001F469\U0001F3FB\u200D\U0001F91D\u200D\U0001F469\U0001F3FC"; // women holding hands: light skin tone, medium-light skin tone const string text2 = "\U0001F46D\U0001F3FB"; // women holding hands: light skin tone - FontRectangle size = TextMeasurer.MeasureSize(text, new TextOptions(font)); - FontRectangle size2 = TextMeasurer.MeasureSize(text2, new TextOptions(font)); + FontRectangle size = TextMeasurer.MeasureBounds(text, new TextOptions(font)); + FontRectangle size2 = TextMeasurer.MeasureBounds(text2, new TextOptions(font)); Assert.Equal(55.652F, size.Width, Comparer); Assert.Equal(55.617F, size2.Width, Comparer); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs index 78e56f26..a876ad43 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_180.cs @@ -11,7 +11,7 @@ public void CorrectlySetsHeightMetrics() // Whitney-book has invalid hhea values. Font font = TestFonts.GetFont(TestFonts.WhitneyBookFile, 25); - FontRectangle size = TextMeasurer.MeasureSize("H", new TextOptions(font)); + FontRectangle size = TextMeasurer.MeasureBounds("H", new TextOptions(font)); Assert.Equal(14, size.Width, 1F); Assert.Equal(17, size.Height, 1F); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs index 9e99274a..19d51a42 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_191.cs @@ -10,9 +10,32 @@ public class Issues_191 [Fact] public void CanLoadMacintoshGlyphs() { - Font font = new FontCollection() - .AddCollection(TestFonts.HelveticaTTCFile) - .First(x => x.GetAvailableStyles().Contains(FontStyle.Regular)).CreateFont(12); + ReadOnlyMemory families = new FontCollection() + .AddCollection(TestFonts.HelveticaTTCFile); + + FontFamily family = families.Span[0]; + foreach (FontFamily candidate in families.Span) + { + ReadOnlySpan styles = candidate.GetAvailableStyles().Span; + bool hasRegular = false; + + foreach (FontStyle style in styles) + { + if (style == FontStyle.Regular) + { + hasRegular = true; + break; + } + } + + if (hasRegular) + { + family = candidate; + break; + } + } + + Font font = family.CreateFont(12); const ColorFontSupport support = ColorFontSupport.None; diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_269.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_269.cs index f08c1514..d3df8650 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_269.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_269.cs @@ -13,7 +13,7 @@ public void CorrectlySetsMetricsForFontsNotAdheringToSpec() // AliceFrancesHMK has invalid subtables. Font font = TestFonts.GetFont(TestFonts.AliceFrancesHMKRegularFile, 25); - FontRectangle size = TextMeasurer.MeasureSize("H", new TextOptions(font)); + FontRectangle size = TextMeasurer.MeasureBounds("H", new TextOptions(font)); Assert.Equal(30.6000004F, size.Width, Comparer); Assert.Equal(24.75F, size.Height, Comparer); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_298.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_298.cs index 5b006553..52ee6efd 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_298.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_298.cs @@ -21,7 +21,7 @@ public void DoesNotThrowOutOfBounds() KerningMode = KerningMode.Auto }; - FontRectangle bounds = TextMeasurer.MeasureSize(content.AsSpan(), renderOptions); + FontRectangle bounds = TextMeasurer.MeasureBounds(content.AsSpan(), renderOptions); Assert.NotEqual(default, bounds); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_302.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_302.cs index 766c750a..25f58c3f 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_302.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_302.cs @@ -14,7 +14,7 @@ public void DoesNotThrowOutOfBounds() Font font = fontFamily.CreateFont(16, FontStyle.Regular); TextOptions renderOptions = new(font); - Assert.True(TextMeasurer.TryMeasureCharacterBounds(content, renderOptions, out ReadOnlySpan _)); + Assert.False(TextMeasurer.GetGlyphMetrics(content, renderOptions).IsEmpty); } #endif } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_32.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_32.cs index d0e7f966..25e4093f 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_32.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_32.cs @@ -13,7 +13,7 @@ public void TabWidth0CausesBadTabRendering() { const string text = "Hello\tworld"; Font font = CreateFont(text); - FontRectangle size = TextMeasurer.MeasureSize(text, new TextOptions(font) + FontRectangle size = TextMeasurer.MeasureBounds(text, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor, TabWidth = 0 diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs index 6a82a728..0434a451 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_33.cs @@ -9,8 +9,8 @@ namespace SixLabors.Fonts.Tests.Issues; public class Issues_33 { [Theory] - [InlineData("\naaaabbbbccccddddeeee\n\t\t\t3 tabs\n\t\t\t\t\t5 tabs", 580, 120)] - [InlineData("\n\tHelloworld", 310, 60)] + [InlineData("\naaaabbbbccccddddeeee\n\t\t\t3 tabs\n\t\t\t\t\t5 tabs", 580, 100)] + [InlineData("\n\tHelloworld", 310, 40)] [InlineData("\tHelloworld", 310, 10)] [InlineData(" Helloworld", 340, 10)] [InlineData("Hell owor ld\t", 340, 10)] @@ -24,9 +24,31 @@ public void WhiteSpaceAtStartOfLineNotMeasured(string text, float width, float h Assert.Equal(width, size.Width, 2F); } + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom, 310, 40)] + [InlineData(LayoutMode.VerticalLeftRight, 40, 310)] + [InlineData(LayoutMode.VerticalMixedLeftRight, 50, 310)] + public void LeadingLineBreakContributesToWhitespaceBounds(LayoutMode layoutMode, float width, float height) + { + const string text = "\n\tHelloworld"; + Font font = CreateFont(text); + TextOptions options = new(font) + { + Dpi = font.FontMetrics.ScaleFactor, + LayoutMode = layoutMode + }; + + FontRectangle size = TextMeasurer.MeasureBounds(text, options); + + // Whitespace has measurable bounds even though it does not render ink. + // Hard breaks still cannot preserve the fake font's old negative top. + Assert.Equal(height, size.Height, 2F); + Assert.Equal(width, size.Width, 2F); + } + public static Font CreateFont(string text) { - IFontMetricsCollection fc = (IFontMetricsCollection)new FontCollection(); + IFontMetricsCollection fc = new FontCollection(); Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12); return new Font(d, 1F); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_333.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_333.cs index e207478e..122723de 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_333.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_333.cs @@ -10,6 +10,6 @@ public void DoesNotThrowMissingTableException() { const string text = "文字測試文字測試文字測試文字測試文字測試"; Font font = TestFonts.GetFont(TestFonts.PMINGLIUFile, 1024); - TextMeasurer.MeasureSize(text, new TextOptions(font)); + TextMeasurer.MeasureBounds(text, new TextOptions(font)); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_353.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_353.cs index 020fd7e4..15cf736e 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_353.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_353.cs @@ -22,7 +22,7 @@ public void Test_Issue_353() LineSpacing = 1.6f }; - LineMetrics[] l = TextMeasurer.GetLineMetrics(text, options); + LineMetrics[] l = TextMeasurer.GetLineMetrics(text, options).ToArray(); // Numeric assertions for line metrics to complement the visual test. Assert.NotEmpty(l); @@ -38,34 +38,30 @@ public void Test_Issue_353() Assert.InRange(m.Baseline, m.Ascender, m.LineHeight); Assert.InRange(m.Descender, m.Baseline, m.LineHeight); - // Horizontal metrics should describe a valid span. - Assert.True(m.Extent > m.Start, "Extent should be greater than Start."); + // Horizontal metrics should describe a valid line box. + Assert.True(m.Extent.X > 0, "Extent.X should be positive."); + Assert.True(m.Extent.Y > 0, "Extent.Y should be positive."); } - void DrawLines(Image image) + void DrawLineMetrics(Image image) { // Draw four separate lines for ascender(orange), baseline (red), descender (blue), // and line bottom (green). - // - // `offset` represents the Y coordinate of the top of the current line box. - // It is advanced by `m.LineHeight` after each iteration. - float offset = 0; for (int i = 0; i < l.Length; i++) { LineMetrics m = l[i]; - float ascent = offset + m.Ascender; - float baseline = offset + m.Baseline; - float descender = offset + m.Descender; - float lineBottom = offset + m.LineHeight; + float ascent = m.Start.Y + m.Ascender; + float baseline = m.Start.Y + m.Baseline; + float descender = m.Start.Y + m.Descender; + float lineBottom = m.Start.Y + m.LineHeight; + float start = m.Start.X; + float end = m.Start.X + m.Extent.X; - image.Mutate(x => x.DrawLine(Color.Orange, 1, new(m.Start, ascent), new(m.Start + m.Extent, ascent))); - image.Mutate(x => x.DrawLine(Color.Red, 1, new(m.Start, baseline), new(m.Start + m.Extent, baseline))); - image.Mutate(x => x.DrawLine(Color.Blue, 1, new(m.Start, descender), new(m.Start + m.Extent, descender))); - image.Mutate(x => x.DrawLine(Color.Green, 1, new(m.Start, lineBottom), new(m.Start + m.Extent, lineBottom))); - - // Advance to the next line's top-of-line-box. - offset += m.LineHeight; + image.Mutate(x => x.DrawLine(Color.Orange, 1, new(start, ascent), new(end, ascent))); + image.Mutate(x => x.DrawLine(Color.Red, 1, new(start, baseline), new(end, baseline))); + image.Mutate(x => x.DrawLine(Color.Blue, 1, new(start, descender), new(end, descender))); + image.Mutate(x => x.DrawLine(Color.Green, 1, new(start, lineBottom), new(end, lineBottom))); } } @@ -73,6 +69,6 @@ void DrawLines(Image image) text, options, includeGeometry: false, - beforeAction: DrawLines); + beforeAction: DrawLineMetrics); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_375.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_375.cs index 467c51be..e0d7c327 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_375.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_375.cs @@ -13,7 +13,7 @@ public void DiacriticsAreMeasuredCorrectly() TextOptions options = new(font); FontRectangle bounds = TextMeasurer.MeasureBounds("È", options); - FontRectangle size = TextMeasurer.MeasureSize("È", options); + FontRectangle size = TextMeasurer.MeasureBounds("È", options); FontRectangle advance = TextMeasurer.MeasureAdvance("È", options); Font fontWoff = TestFonts.GetFont(TestFonts.PermanentMarkerRegularWoff2File, 142); @@ -21,7 +21,7 @@ public void DiacriticsAreMeasuredCorrectly() TextOptions optionsWoff = new(fontWoff); FontRectangle boundsWoff = TextMeasurer.MeasureBounds("È", optionsWoff); - FontRectangle sizeWoff = TextMeasurer.MeasureSize("È", optionsWoff); + FontRectangle sizeWoff = TextMeasurer.MeasureBounds("È", optionsWoff); FontRectangle advanceWoff = TextMeasurer.MeasureAdvance("È", optionsWoff); Assert.Equal(bounds, boundsWoff); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_378.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_378.cs index f8147c16..00eb0fa2 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_378.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_378.cs @@ -11,9 +11,9 @@ public void DoesNotBreakIncorrectly() Font font = TestFonts.GetFont(TestFonts.PlantinStdRegularFile, 2048); TextOptions options = new(font) { WrappingLength = float.MaxValue }; - FontRectangle size = TextMeasurer.MeasureSize("D\r\nD", options); + FontRectangle size = TextMeasurer.MeasureBounds("D\r\nD", options); - FontRectangle size2 = TextMeasurer.MeasureSize("D D", options); + FontRectangle size2 = TextMeasurer.MeasureBounds("D D", options); Assert.NotEqual(size, size2); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs index ce2b5653..aa8d1ae5 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_39.cs @@ -20,7 +20,7 @@ public void RenderingEmptyString_DoesNotThrow() public static Font CreateFont(string text) { - IFontMetricsCollection fc = (IFontMetricsCollection)new FontCollection(); + IFontMetricsCollection fc = new FontCollection(); Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12); return new Font(d, 1F); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs index 32b9efba..d2729834 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_400.cs @@ -3,6 +3,14 @@ using System.Text; +#if SUPPORTS_DRAWING +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +#endif + namespace SixLabors.Fonts.Tests.Issues; public class Issues_400 @@ -16,14 +24,62 @@ public void RenderingTextIncludesAllGlyphs() }; StringBuilder stringBuilder = new(); - stringBuilder + string text = stringBuilder .AppendLine() .AppendLine(" NEWS_CATEGORY=EWF&NEWS_HASH=4b298ff9277ef9fdf515356be95ea3caf57cd36&OFFSET=0&SEARCH_VALUE=CA88105E1088&ID_NEWS") - .Append(" "); + .Append(" ") + .ToString(); - int lineCount = TextMeasurer.CountLines(stringBuilder.ToString(), options); + int lineCount = TextMeasurer.CountLines(text, options); Assert.Equal(4, lineCount); - TextLayoutTestUtilities.TestLayout(stringBuilder.ToString(), options); +#if SUPPORTS_DRAWING + TextLayoutTestUtilities.TestLayout( + text, + options, + afterAction: image => DrawBoundsOverlay(image, text, options)); +#else + TextLayoutTestUtilities.TestLayout(text, options); +#endif + } + +#if SUPPORTS_DRAWING + private static void DrawBoundsOverlay(Image image, string text, TextOptions options) + { + FontRectangle advance = TextMeasurer.MeasureAdvance(text, options); + FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); + FontRectangle renderableBounds = TextMeasurer.MeasureRenderableBounds(text, options); + + image.Mutate(x => + { + DrawRectangle(x, renderableBounds, Color.Magenta, 3); + DrawRectangle(x, advance, Color.DeepSkyBlue, 2); + DrawRectangle(x, bounds, Color.Lime, 2); + }); + + ReadOnlySpan measuredGlyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; + GlyphMetrics[] glyphs = measuredGlyphs.ToArray(); + + image.Mutate(x => + { + for (int i = 0; i < glyphs.Length; i++) + { + DrawRectangle(x, glyphs[i].Bounds, Color.Orange, 1); + } + }); + } + + private static void DrawRectangle(IImageProcessingContext context, FontRectangle rectangle, Color color, float thickness) + { + if (rectangle.IsEmpty) + { + return; + } + + context.Draw( + color, + thickness, + new RectangularPolygon(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height)); } +#endif } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_403.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_403.cs index 645ccffe..c0cfd9a0 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_403.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_403.cs @@ -12,11 +12,11 @@ public void DoesNotKernIncorrectly() Font font = TestFonts.GetFont(TestFonts.KellySlabFile, 2048); TextOptions options = new(font) { KerningMode = KerningMode.Auto }; - FontRectangle kerned = TextMeasurer.MeasureSize("AY", options); + FontRectangle kerned = TextMeasurer.MeasureBounds("AY", options); options.KerningMode = KerningMode.None; - FontRectangle unkerned = TextMeasurer.MeasureSize("AY", options); + FontRectangle unkerned = TextMeasurer.MeasureBounds("AY", options); Assert.Equal(kerned.Left, unkerned.Left); Assert.Equal(kerned.Top, unkerned.Top); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_412.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_412.cs index 692cde8b..482f23d9 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_412.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_412.cs @@ -19,6 +19,7 @@ public void ShouldCreateCorrectTextRunCount() }; IReadOnlyList runs = TextLayout.BuildTextRuns("abcde", options); + Assert.Equal(2, runs.Count); TextRun run = runs[0]; diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs index 01a9ebd8..073a2f75 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_417.cs @@ -17,15 +17,15 @@ public void DoesNotThrow_InvalidAnchor() TextOptions options = new(font); - // References values generated using. + // Crowbar reports the positioned advance widths, not tight rendered bounds. // https://www.corvelsoftware.co.uk/crowbar/ - TextMeasurer.TryMeasureCharacterAdvances("Text", options, out ReadOnlySpan advances); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics("Text", options).Span; - Assert.Equal(4, advances.Length); - Assert.Equal(486, advances[0].Bounds.Width); - Assert.Equal(544, advances[1].Bounds.Width); - Assert.Equal(529, advances[2].Bounds.Width); - Assert.Equal(361, advances[3].Bounds.Width); + Assert.Equal(4, glyphs.Length); + Assert.Equal(486, glyphs[0].Advance.Width); + Assert.Equal(544, glyphs[1].Advance.Width); + Assert.Equal(529, glyphs[2].Advance.Width); + Assert.Equal(361, glyphs[3].Advance.Width); GlyphRenderer renderer = new(); TextRenderer.RenderTextTo(renderer, "Text", new TextOptions(font)); diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs index 5632ea34..97921432 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_429.cs @@ -1,6 +1,8 @@ // Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +using SixLabors.Fonts.Rendering; + namespace SixLabors.Fonts.Tests.Issues; public class Issues_429 @@ -20,23 +22,22 @@ public void VerticalMixedLayout_ExpectedRotation() LayoutMode = LayoutMode.VerticalMixedRightLeft }; - IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); + GlyphRenderer renderer = new(); + TextRenderer.RenderTextTo(renderer, text, options); // Only the Latin glyph + space should be rotated. // Any other glyphs that appear rotated have actually been substituted by the font. int[] rotatedGlyphs = [20, 21, 22, 23, 24, 25, 26, 27]; - for (int i = 0; i < glyphs.Count; i++) + for (int i = 0; i < renderer.GlyphKeys.Count; i++) { - GlyphLayout glyph = glyphs[i]; - if (rotatedGlyphs.Contains(i)) { - Assert.Equal(GlyphLayoutMode.VerticalRotated, glyph.LayoutMode); + Assert.Equal(GlyphLayoutMode.VerticalRotated, renderer.GlyphKeys[i].LayoutMode); } else { - Assert.Equal(GlyphLayoutMode.Vertical, glyph.LayoutMode); + Assert.Equal(GlyphLayoutMode.Vertical, renderer.GlyphKeys[i].LayoutMode); } } } @@ -56,11 +57,9 @@ public void VerticalMixedLayout_ExpectedBounds() }; FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); - FontRectangle size = TextMeasurer.MeasureSize(text, options); FontRectangle renderable = TextMeasurer.MeasureRenderableBounds(text, options); Assert.Equal(new FontRectangle(0.83496094F, 2.8417969F, 28.31543F, 834.9464F), bounds, Comparer); - Assert.Equal(new FontRectangle(0, 0, 28.31543F, 834.9464F), size, Comparer); Assert.Equal(new FontRectangle(0, 0, 30F, 839.3556F), renderable, Comparer); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs index a5db104e..3b8c9084 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_431.cs @@ -26,8 +26,11 @@ public void ShouldNotInsertExtraLineBreaks() int lineCount = TextMeasurer.CountLines(text, options); Assert.Equal(4, lineCount); - IReadOnlyList layout = TextLayout.GenerateLayout(text, options); - Assert.Equal(46, layout.Count); + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // Hard breaks that terminate non-empty lines are trimmed from the visual + // glyph stream, so the glyph count returns to the visible source glyphs. + Assert.Equal(46, metrics.GetGlyphMetrics().Length); } } @@ -50,8 +53,11 @@ public void ShouldNotInsertExtraLineBreaks_2() int lineCount = TextMeasurer.CountLines(text, options); Assert.Equal(4, lineCount); - IReadOnlyList layout = TextLayout.GenerateLayout(text, options); - Assert.Equal(46, layout.Count); + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // Hard breaks that terminate non-empty lines are trimmed from the visual + // glyph stream, so the glyph count returns to the visible source glyphs. + Assert.Equal(46, metrics.GetGlyphMetrics().Length); } } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs index 6475667d..d9c65663 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_434.cs @@ -25,8 +25,12 @@ public void ShouldInsertExtraLineBreaksA(string text, int expectedLineCount) int lineCount = TextMeasurer.CountLines(text, options); Assert.Equal(expectedLineCount, lineCount); - IReadOnlyList layout = TextLayout.GenerateLayout(text, options); - Assert.Equal(47, layout.Count); + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // Hard breaks that terminate visual lines are trimmed from the glyph stream. + // The line count assertion above owns the regression; this count verifies + // that trimming no longer exposes those break glyphs as measured advances. + Assert.Equal(47, metrics.GetGlyphMetrics().Length); } } @@ -48,8 +52,12 @@ public void ShouldInsertExtraLineBreaksB(string text, int expectedLineCount) int lineCount = TextMeasurer.CountLines(text, options); Assert.Equal(expectedLineCount, lineCount); - IReadOnlyList layout = TextLayout.GenerateLayout(text, options); - Assert.Equal(48, layout.Count); + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // Hard breaks that terminate visual lines are trimmed from the glyph stream. + // The line count assertion above owns the regression; this count verifies + // that trimming no longer exposes those break glyphs as measured advances. + Assert.Equal(48, metrics.GetGlyphMetrics().Length); } } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_47.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_47.cs index 82e40a59..625fa8d0 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_47.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_47.cs @@ -3,6 +3,7 @@ using System.Globalization; using SixLabors.Fonts.Tests.Fakes; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tests.Issues; @@ -14,21 +15,18 @@ public void LeftAlignedTextNewLineShouldNotStartWithWhiteSpace(string text) { Font font = CreateFont("\t x"); - GlyphRenderer r = new(); - - IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) + TextMetrics metrics = TextMeasurer.Measure(text, new TextOptions(new Font(font, 30)) { WrappingLength = 350, HorizontalAlignment = HorizontalAlignment.Left }); - float lineYPos = layout[0].PenLocation.Y; - foreach (GlyphLayout glyph in layout) + for (int lineIndex = 0; lineIndex < metrics.LineMetrics.Length; lineIndex++) { - if (lineYPos != glyph.PenLocation.Y) + if (lineIndex > 0) { - Assert.False(glyph.IsWhiteSpace()); - lineYPos = glyph.PenLocation.Y; + CodePoint lineStart = CodePoint.DecodeFromUtf16At(text.AsSpan(), metrics.LineMetrics[lineIndex].StringIndex); + Assert.False(CodePoint.IsWhiteSpace(lineStart)); } } } @@ -38,27 +36,22 @@ public void LeftAlignedTextNewLineShouldNotStartWithWhiteSpace(string text) [InlineData("hello world hello world hello world hello world", HorizontalAlignment.Right)] [InlineData("hello world hello world hello world hello world", HorizontalAlignment.Center)] [InlineData("hello world hello world hello hello world", HorizontalAlignment.Left)] - public void NewWrappedLinesShouldNotStartOrEndWithWhiteSpace(string text, HorizontalAlignment horizontalAlignment) + public void NewWrappedLineMetricsShouldNotStartWithWhiteSpace(string text, HorizontalAlignment horizontalAlignment) { Font font = CreateFont("\t x"); - GlyphRenderer r = new(); - - IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) + TextMetrics metrics = TextMeasurer.Measure(text, new TextOptions(new Font(font, 30)) { WrappingLength = 350, HorizontalAlignment = horizontalAlignment }); - float lineYPos = layout[0].PenLocation.Y; - for (int i = 0; i < layout.Count; i++) + for (int lineIndex = 0; lineIndex < metrics.LineMetrics.Length; lineIndex++) { - GlyphLayout glyph = layout[i]; - if (lineYPos != glyph.PenLocation.Y) + if (lineIndex > 0) { - Assert.False(glyph.IsWhiteSpace()); - Assert.False(layout[i - 1].IsWhiteSpace()); - lineYPos = glyph.PenLocation.Y; + CodePoint lineStart = CodePoint.DecodeFromUtf16At(text.AsSpan(), metrics.LineMetrics[lineIndex].StringIndex); + Assert.False(CodePoint.IsWhiteSpace(lineStart)); } } } @@ -69,39 +62,43 @@ public void WhiteSpaceAtStartOfTextShouldNotBeTrimmed() Font font = CreateFont("\t x"); string text = " hello world hello world hello world"; - GlyphRenderer r = new(); - - IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) + TextMetrics metrics = TextMeasurer.Measure(text, new TextOptions(new Font(font, 30)) { WrappingLength = 350 }); - Assert.True(layout[0].IsWhiteSpace()); - Assert.True(layout[1].IsWhiteSpace()); - Assert.True(layout[2].IsWhiteSpace()); + Assert.True(CodePoint.IsWhiteSpace(metrics.GetGlyphMetrics().Span[0].CodePoint)); + Assert.True(CodePoint.IsWhiteSpace(metrics.GetGlyphMetrics().Span[1].CodePoint)); + Assert.True(CodePoint.IsWhiteSpace(metrics.GetGlyphMetrics().Span[2].CodePoint)); } [Fact] - public void WhiteSpaceAtTheEndOfTextShouldBeTrimmed() + public void WhiteSpaceAtTheEndOfTextShouldNotContributeToMeasurement() { Font font = CreateFont("\t x"); string text = "hello world hello world hello world "; + string trimmed = "hello world hello world hello world"; - GlyphRenderer r = new(); - - IReadOnlyList layout = TextLayout.GenerateLayout(text.AsSpan(), new TextOptions(new Font(font, 30)) + TextOptions options = new(new Font(font, 30)) { WrappingLength = 350 - }); + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + TextMetrics trimmedMetrics = TextMeasurer.Measure(trimmed, options); + + Assert.Equal(trimmedMetrics.Advance, metrics.Advance); + Assert.Equal(trimmedMetrics.Bounds, metrics.Bounds); - Assert.False(layout[layout.Count - 1].IsWhiteSpace()); - Assert.False(layout[layout.Count - 2].IsWhiteSpace()); - Assert.False(layout[layout.Count - 3].IsWhiteSpace()); + // Trailing breaking whitespace is trimmed from the visual glyph stream, + // so the measured glyphs match the explicitly trimmed input. + Assert.Equal(trimmedMetrics.GetGlyphMetrics().Length, metrics.GetGlyphMetrics().Length); + Assert.False(CodePoint.IsWhiteSpace(metrics.GetGlyphMetrics().Span[^1].CodePoint)); } public static Font CreateFont(string text) { - IFontMetricsCollection fc = (IFontMetricsCollection)new FontCollection(); + IFontMetricsCollection fc = new FontCollection(); Font d = fc.AddMetrics(new FakeFontInstance(text), CultureInfo.InvariantCulture).CreateFont(12); return new Font(d, 1F); } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_512.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_512.cs index cd9ce910..fd39be90 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_512.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_512.cs @@ -16,7 +16,7 @@ public void MissingCffTableThrowsInvalidFontFileExceptionInsteadOfNullReferenceE TextOptions options = new(font); - InvalidFontFileException exception = Assert.Throws(() => TextMeasurer.MeasureSize(text, options)); + InvalidFontFileException exception = Assert.Throws(() => TextMeasurer.MeasureBounds(text, options)); Assert.Equal("Missing required CFF table.", exception.Message); } } diff --git a/tests/SixLabors.Fonts.Tests/Issues/Issues_514.cs b/tests/SixLabors.Fonts.Tests/Issues/Issues_514.cs index e36a44b5..9502bc7b 100644 --- a/tests/SixLabors.Fonts.Tests/Issues/Issues_514.cs +++ b/tests/SixLabors.Fonts.Tests/Issues/Issues_514.cs @@ -10,7 +10,7 @@ public void LookupType2Format1SubTable() { Font font = TestFonts.GetFont(TestFonts.Issues.Issue514, 12); - FontRectangle size = TextMeasurer.MeasureSize("ABCabc123", new TextOptions(font)); + FontRectangle size = TextMeasurer.MeasureBounds("ABCabc123", new TextOptions(font)); Assert.NotEqual(FontRectangle.Empty, size); } diff --git a/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs b/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs index fa1a0514..6077ec20 100644 --- a/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs +++ b/tests/SixLabors.Fonts.Tests/SystemFontCollectionTests.cs @@ -123,10 +123,10 @@ public void CanGetAllMetricsByCulture() public void CanGetAllStylesByCulture() { FontFamily family = SysFontCollection.Families.First(); - IEnumerable styles = ((IReadOnlyFontMetricsCollection)SysFontCollection).GetAllStyles(family.Name, family.Culture); + ReadOnlyMemory styles = ((IReadOnlyFontMetricsCollection)SysFontCollection).GetAllStyles(family.Name, family.Culture); - Assert.True(styles.Any()); - Assert.Equal(family.GetAvailableStyles(), styles); + Assert.False(styles.IsEmpty); + Assert.Equal(family.GetAvailableStyles().ToArray(), styles.ToArray()); } [Fact] diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs index 2b3bf273..84c7a568 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/GPos/GPosTableTests.cs @@ -125,10 +125,14 @@ public void CursiveAttachmentPositioning_Format1_Works() ColorGlyphRenderer renderer = new(); string testStr = "\u0012\u0012"; // "\u0012\u0012" characters should overlap. int[] expectedGlyphIndices = [20, 20]; + + // The test font's cursive lookup has no RightToLeft flag. HarfBuzz treats + // that as attaching the following glyph back to the current glyph, so the + // second glyph carries the minor-axis offset from entry(100,150)-exit(200,250). FontRectangle[] expectedFontRectangles = [ - new(475, 1185.9999F, 825, 719), - new(575, 1085.9999F, 825, 719), + new(475, 1085.9999F, 825, 719), + new(575, 985.9999F, 825, 719), ]; // act diff --git a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs index 6582bdcf..70f06279 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/AdvancedTypographic/Gsub/GSubTableTests.cs @@ -375,6 +375,6 @@ public void BillionLaughsAttackDoesNotThrowException() Font font = TestFonts.GetFont(TestFonts.GSubLookupType2BillionLaughs, 12); // Act - TextMeasurer.MeasureSize("lol", new TextOptions(font)); + TextMeasurer.MeasureBounds("lol", new TextOptions(font)); } } diff --git a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationFontTests.cs b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationFontTests.cs index 6041767a..5e1536c8 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationFontTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationFontTests.cs @@ -81,13 +81,13 @@ public void CanLoadVariationAxes_RobotoFlex() FontFamily family = TestFonts.GetFontFamily(TestFonts.RobotoFlex); Font font = family.CreateFont(12); - Assert.True(font.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes)); + Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(13, axes.Length); - Assert.Equal("wght", axes[0].Tag); - Assert.Equal(100, axes[0].Min); - Assert.Equal(1000, axes[0].Max); - Assert.Equal(400, axes[0].Default); + Assert.Equal("wght", axes.Span[0].Tag); + Assert.Equal(100, axes.Span[0].Min); + Assert.Equal(1000, axes.Span[0].Max); + Assert.Equal(400, axes.Span[0].Default); } [Fact] @@ -96,14 +96,14 @@ public void CanLoadVariationAxes_AdobeVFPrototype() FontFamily family = TestFonts.GetFontFamily(TestFonts.AdobeVFPrototype); Font font = family.CreateFont(12); - Assert.True(font.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes)); + Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(2, axes.Length); - Assert.Equal("wght", axes[0].Tag); - Assert.Equal(200, axes[0].Min); - Assert.Equal(900, axes[0].Max); + Assert.Equal("wght", axes.Span[0].Tag); + Assert.Equal(200, axes.Span[0].Min); + Assert.Equal(900, axes.Span[0].Max); - Assert.Equal("CNTR", axes[1].Tag); + Assert.Equal("CNTR", axes.Span[1].Tag); } [Fact] @@ -207,8 +207,8 @@ public void HVAR_DefaultWeightPreservesOriginalWidth() FontFamily family = TestFonts.GetFontFamily(TestFonts.TestGVARFour); Font defaultFont = family.CreateFont(12); - Assert.True(defaultFont.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes)); - VariationAxis wghtAxis = Assert.Single(axes, a => a.Tag == "wght"); + Assert.True(defaultFont.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); + VariationAxis wghtAxis = Assert.Single(axes.ToArray(), a => a.Tag == "wght"); Font variedFont = family.CreateFont(12, new FontVariation("wght", wghtAxis.Default)); @@ -301,9 +301,9 @@ public void CFF2_CanLoadVariationAxes() FontFamily family = TestFonts.GetFontFamily(TestFonts.AdobeVFPrototypeSubset); Font font = family.CreateFont(12); - Assert.True(font.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes)); + Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(2, axes.Length); - Assert.Equal("wght", axes[0].Tag); + Assert.Equal("wght", axes.Span[0].Tag); } [Fact] @@ -666,11 +666,11 @@ public void CVar_CanLoadFontWithCvarTable() Font font = family.CreateFont(12); Assert.NotNull(font.FontMetrics); - Assert.True(font.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes)); + Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(3, axes.Length); - Assert.Equal("wght", axes[0].Tag); - Assert.Equal("wdth", axes[1].Tag); - Assert.Equal("opsz", axes[2].Tag); + Assert.Equal("wght", axes.Span[0].Tag); + Assert.Equal("wdth", axes.Span[1].Tag); + Assert.Equal("opsz", axes.Span[2].Tag); } [Fact] @@ -681,7 +681,7 @@ public void CVar_NoShared_CanLoadFontWithCvarTable() Font font = family.CreateFont(12); Assert.NotNull(font.FontMetrics); - Assert.True(font.FontMetrics.TryGetVariationAxes(out VariationAxis[] axes)); + Assert.True(font.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory axes)); Assert.Equal(3, axes.Length); } diff --git a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs index 8b5bf4e5..b68b6c3a 100644 --- a/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs +++ b/tests/SixLabors.Fonts.Tests/Tables/Variations/VariationsTests.cs @@ -17,104 +17,104 @@ private static Font CreateFont(string testFont) [Fact] public void CanLoadVariationTables_RobotoFlex() { - Assert.True(RobotoFlexTTF.FontMetrics.TryGetVariationAxes(out VariationAxis[] variationAxes)); + Assert.True(RobotoFlexTTF.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory variationAxes)); Assert.Equal(13, variationAxes.Length); - Assert.Equal("wght", variationAxes[0].Name); - Assert.Equal("wght", variationAxes[0].Tag); - Assert.Equal(100, variationAxes[0].Min); - Assert.Equal(1000, variationAxes[0].Max); - Assert.Equal(400, variationAxes[0].Default); - - Assert.Equal("wdth", variationAxes[1].Name); - Assert.Equal("wdth", variationAxes[1].Tag); - Assert.Equal(25, variationAxes[1].Min); - Assert.Equal(151, variationAxes[1].Max); - Assert.Equal(100, variationAxes[1].Default); - - Assert.Equal("opsz", variationAxes[2].Name); - Assert.Equal("opsz", variationAxes[2].Tag); - Assert.Equal(8, variationAxes[2].Min); - Assert.Equal(144, variationAxes[2].Max); - Assert.Equal(14, variationAxes[2].Default); - - Assert.Equal("GRAD", variationAxes[3].Name); - Assert.Equal("GRAD", variationAxes[3].Tag); - Assert.Equal(-200, variationAxes[3].Min); - Assert.Equal(150, variationAxes[3].Max); - Assert.Equal(0, variationAxes[3].Default); - - Assert.Equal("slnt", variationAxes[4].Name); - Assert.Equal("slnt", variationAxes[4].Tag); - Assert.Equal(-10, variationAxes[4].Min); - Assert.Equal(0, variationAxes[4].Max); - Assert.Equal(0, variationAxes[4].Default); - - Assert.Equal("XTRA", variationAxes[5].Name); - Assert.Equal("XTRA", variationAxes[5].Tag); - Assert.Equal(323, variationAxes[5].Min); - Assert.Equal(603, variationAxes[5].Max); - Assert.Equal(468, variationAxes[5].Default); - - Assert.Equal("XOPQ", variationAxes[6].Name); - Assert.Equal("XOPQ", variationAxes[6].Tag); - Assert.Equal(27, variationAxes[6].Min); - Assert.Equal(175, variationAxes[6].Max); - Assert.Equal(96, variationAxes[6].Default); - - Assert.Equal("YOPQ", variationAxes[7].Name); - Assert.Equal("YOPQ", variationAxes[7].Tag); - Assert.Equal(25, variationAxes[7].Min); - Assert.Equal(135, variationAxes[7].Max); - Assert.Equal(79, variationAxes[7].Default); - - Assert.Equal("YTLC", variationAxes[8].Name); - Assert.Equal("YTLC", variationAxes[8].Tag); - Assert.Equal(416, variationAxes[8].Min); - Assert.Equal(570, variationAxes[8].Max); - Assert.Equal(514, variationAxes[8].Default); - - Assert.Equal("YTUC", variationAxes[9].Name); - Assert.Equal("YTUC", variationAxes[9].Tag); - Assert.Equal(528, variationAxes[9].Min); - Assert.Equal(760, variationAxes[9].Max); - Assert.Equal(712, variationAxes[9].Default); - - Assert.Equal("YTAS", variationAxes[10].Name); - Assert.Equal("YTAS", variationAxes[10].Tag); - Assert.Equal(649, variationAxes[10].Min); - Assert.Equal(854, variationAxes[10].Max); - Assert.Equal(750, variationAxes[10].Default); - - Assert.Equal("YTDE", variationAxes[11].Name); - Assert.Equal("YTDE", variationAxes[11].Tag); - Assert.Equal(-305, variationAxes[11].Min); - Assert.Equal(-98, variationAxes[11].Max); - Assert.Equal(-203, variationAxes[11].Default); - - Assert.Equal("YTFI", variationAxes[12].Name); - Assert.Equal("YTFI", variationAxes[12].Tag); - Assert.Equal(560, variationAxes[12].Min); - Assert.Equal(788, variationAxes[12].Max); - Assert.Equal(738, variationAxes[12].Default); + Assert.Equal("wght", variationAxes.Span[0].Name); + Assert.Equal("wght", variationAxes.Span[0].Tag); + Assert.Equal(100, variationAxes.Span[0].Min); + Assert.Equal(1000, variationAxes.Span[0].Max); + Assert.Equal(400, variationAxes.Span[0].Default); + + Assert.Equal("wdth", variationAxes.Span[1].Name); + Assert.Equal("wdth", variationAxes.Span[1].Tag); + Assert.Equal(25, variationAxes.Span[1].Min); + Assert.Equal(151, variationAxes.Span[1].Max); + Assert.Equal(100, variationAxes.Span[1].Default); + + Assert.Equal("opsz", variationAxes.Span[2].Name); + Assert.Equal("opsz", variationAxes.Span[2].Tag); + Assert.Equal(8, variationAxes.Span[2].Min); + Assert.Equal(144, variationAxes.Span[2].Max); + Assert.Equal(14, variationAxes.Span[2].Default); + + Assert.Equal("GRAD", variationAxes.Span[3].Name); + Assert.Equal("GRAD", variationAxes.Span[3].Tag); + Assert.Equal(-200, variationAxes.Span[3].Min); + Assert.Equal(150, variationAxes.Span[3].Max); + Assert.Equal(0, variationAxes.Span[3].Default); + + Assert.Equal("slnt", variationAxes.Span[4].Name); + Assert.Equal("slnt", variationAxes.Span[4].Tag); + Assert.Equal(-10, variationAxes.Span[4].Min); + Assert.Equal(0, variationAxes.Span[4].Max); + Assert.Equal(0, variationAxes.Span[4].Default); + + Assert.Equal("XTRA", variationAxes.Span[5].Name); + Assert.Equal("XTRA", variationAxes.Span[5].Tag); + Assert.Equal(323, variationAxes.Span[5].Min); + Assert.Equal(603, variationAxes.Span[5].Max); + Assert.Equal(468, variationAxes.Span[5].Default); + + Assert.Equal("XOPQ", variationAxes.Span[6].Name); + Assert.Equal("XOPQ", variationAxes.Span[6].Tag); + Assert.Equal(27, variationAxes.Span[6].Min); + Assert.Equal(175, variationAxes.Span[6].Max); + Assert.Equal(96, variationAxes.Span[6].Default); + + Assert.Equal("YOPQ", variationAxes.Span[7].Name); + Assert.Equal("YOPQ", variationAxes.Span[7].Tag); + Assert.Equal(25, variationAxes.Span[7].Min); + Assert.Equal(135, variationAxes.Span[7].Max); + Assert.Equal(79, variationAxes.Span[7].Default); + + Assert.Equal("YTLC", variationAxes.Span[8].Name); + Assert.Equal("YTLC", variationAxes.Span[8].Tag); + Assert.Equal(416, variationAxes.Span[8].Min); + Assert.Equal(570, variationAxes.Span[8].Max); + Assert.Equal(514, variationAxes.Span[8].Default); + + Assert.Equal("YTUC", variationAxes.Span[9].Name); + Assert.Equal("YTUC", variationAxes.Span[9].Tag); + Assert.Equal(528, variationAxes.Span[9].Min); + Assert.Equal(760, variationAxes.Span[9].Max); + Assert.Equal(712, variationAxes.Span[9].Default); + + Assert.Equal("YTAS", variationAxes.Span[10].Name); + Assert.Equal("YTAS", variationAxes.Span[10].Tag); + Assert.Equal(649, variationAxes.Span[10].Min); + Assert.Equal(854, variationAxes.Span[10].Max); + Assert.Equal(750, variationAxes.Span[10].Default); + + Assert.Equal("YTDE", variationAxes.Span[11].Name); + Assert.Equal("YTDE", variationAxes.Span[11].Tag); + Assert.Equal(-305, variationAxes.Span[11].Min); + Assert.Equal(-98, variationAxes.Span[11].Max); + Assert.Equal(-203, variationAxes.Span[11].Default); + + Assert.Equal("YTFI", variationAxes.Span[12].Name); + Assert.Equal("YTFI", variationAxes.Span[12].Tag); + Assert.Equal(560, variationAxes.Span[12].Min); + Assert.Equal(788, variationAxes.Span[12].Max); + Assert.Equal(738, variationAxes.Span[12].Default); } [Fact] public void CanLoadVariationTables_AdobeVFPrototype() { - Assert.True(AdobeVFPrototype.FontMetrics.TryGetVariationAxes(out VariationAxis[] variationAxes)); + Assert.True(AdobeVFPrototype.FontMetrics.TryGetVariationAxes(out ReadOnlyMemory variationAxes)); Assert.Equal(2, variationAxes.Length); - Assert.Equal("Weight", variationAxes[0].Name); - Assert.Equal("wght", variationAxes[0].Tag); - Assert.Equal(200, variationAxes[0].Min); - Assert.Equal(900, variationAxes[0].Max); - Assert.Equal(389.344, Math.Round(variationAxes[0].Default, 3)); - - Assert.Equal("Contrast", variationAxes[1].Name); - Assert.Equal("CNTR", variationAxes[1].Tag); - Assert.Equal(0, variationAxes[1].Min); - Assert.Equal(100, variationAxes[1].Max); - Assert.Equal(0, variationAxes[1].Default); + Assert.Equal("Weight", variationAxes.Span[0].Name); + Assert.Equal("wght", variationAxes.Span[0].Tag); + Assert.Equal(200, variationAxes.Span[0].Min); + Assert.Equal(900, variationAxes.Span[0].Max); + Assert.Equal(389.344, Math.Round(variationAxes.Span[0].Default, 3)); + + Assert.Equal("Contrast", variationAxes.Span[1].Name); + Assert.Equal("CNTR", variationAxes.Span[1].Tag); + Assert.Equal(0, variationAxes.Span[1].Min); + Assert.Equal(100, variationAxes.Span[1].Max); + Assert.Equal(0, variationAxes.Span[1].Default); } } diff --git a/tests/SixLabors.Fonts.Tests/TextAlignmentTests.cs b/tests/SixLabors.Fonts.Tests/TextAlignmentTests.cs index 6e2c9dbe..bf3f834a 100644 --- a/tests/SixLabors.Fonts.Tests/TextAlignmentTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextAlignmentTests.cs @@ -155,9 +155,9 @@ private static void Draw( private sealed class BoundsRenderer : IGlyphRenderer { - private readonly List glyphBounds = []; + private readonly List glyphRectangles = []; - public IPathCollection Boxes => new PathCollection(this.glyphBounds.Select(x => new RectangularPolygon(x.X, x.Y, x.Width, x.Height))); + public IPathCollection Boxes => new PathCollection(this.glyphRectangles.Select(x => new RectangularPolygon(x.X, x.Y, x.Width, x.Height))); public IPath TextBox { get; private set; } @@ -166,7 +166,7 @@ public void BeginText(in FontRectangle bounds) public bool BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) { - this.glyphBounds.Add(bounds); + this.glyphRectangles.Add(bounds); return true; } diff --git a/tests/SixLabors.Fonts.Tests/TextBlockTests.cs b/tests/SixLabors.Fonts.Tests/TextBlockTests.cs new file mode 100644 index 00000000..2443ce89 --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/TextBlockTests.cs @@ -0,0 +1,1502 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Numerics; +using SixLabors.Fonts.Unicode; + +namespace SixLabors.Fonts.Tests; + +public class TextBlockTests +{ + private static readonly ApproximateFloatComparer Comparer = new(0.001F); + + private static Font Font => TextLayoutTests.CreateRenderingFont(); + + [Fact] + public void Constructor_IgnoresWrappingLength() + { + const string text = "The quick brown fox jumps over the lazy dog."; + TextOptions preparedOptions = Options(35); + TextBlock block = new(text, preparedOptions); + + TextMetrics expected = TextMeasurer.Measure(text, Options(-1)); + TextMetrics actual = block.Measure(-1); + + AssertTextMetricsEqual(expected, actual); + } + + [Theory] + [InlineData(90)] + [InlineData(160)] + [InlineData(-1)] + public void Measure_ReusesPreparedText_ForDifferentWrappingLengths(float wrappingLength) + { + const string text = "The paragraph begins in English, then says שלום and مرحبا before returning to Latin text."; + TextBlock block = new(text, Options(-1)); + + TextMetrics expected = TextMeasurer.Measure(text, Options(wrappingLength)); + TextMetrics actual = block.Measure(wrappingLength); + + AssertTextMetricsEqual(expected, actual); + } + + [Fact] + public void GranularMeasurements_MatchTextMeasurer() + { + const string text = "Hello world\nSecond line"; + const float wrappingLength = 95; + TextBlock block = new(text, Options(-1)); + + Assert.Equal( + TextMeasurer.MeasureAdvance(text, Options(wrappingLength)), + block.MeasureAdvance(wrappingLength), + Comparer); + + Assert.Equal( + TextMeasurer.MeasureBounds(text, Options(wrappingLength)), + block.MeasureBounds(wrappingLength), + Comparer); + + Assert.Equal( + TextMeasurer.MeasureRenderableBounds(text, Options(wrappingLength)), + block.MeasureRenderableBounds(wrappingLength), + Comparer); + + Assert.Equal(TextMeasurer.CountLines(text, Options(wrappingLength)), block.CountLines(wrappingLength)); + AssertLineMetricsEqual(TextMeasurer.GetLineMetrics(text, Options(wrappingLength)).Span, block.GetLineMetrics(wrappingLength).Span); + } + + [Fact] + public void GetLineMetrics_IncludesSourceMapping() + { + const string firstLine = "Hello world\n"; + const string text = firstLine + "Second line"; + TextBlock block = new(text, Options(-1)); + + ReadOnlySpan metrics = block.Measure(-1).LineMetrics; + + Assert.Equal(2, metrics.Length); + Assert.Equal(0, metrics[0].StringIndex); + Assert.Equal(firstLine.Length, metrics[1].StringIndex); + Assert.Equal(0, metrics[0].GraphemeIndex); + + // The newline that ends the first line is trimmed from visual metrics, so + // the next line starts after a source-position gap. + Assert.Equal(firstLine.Length, metrics[1].GraphemeIndex); + } + + [Fact] + public void GetLineLayouts_ReturnsMetricsAndGraphemeMetricsPerLine() + { + const string firstLine = "Hello world\n"; + const string text = firstLine + "Second line"; + TextBlock block = new(text, Options(-1)); + + ReadOnlySpan lines = block.GetLineLayouts(-1).Span; + ReadOnlySpan metrics = block.GetLineMetrics(-1).Span; + + Assert.Equal(2, lines.Length); + AssertLineMetricsEqual(metrics, lines); + + Assert.Equal(firstLine.Length - 1, lines[0].GraphemeMetrics.Length); + Assert.Equal("Second line".Length, lines[1].GraphemeMetrics.Length); + Assert.Equal(0, lines[0].GraphemeMetrics[0].StringIndex); + Assert.Equal(firstLine.Length - 2, lines[0].GraphemeMetrics[^1].StringIndex); + Assert.Equal(firstLine.Length, lines[1].GraphemeMetrics[0].StringIndex); + } + + [Fact] + public void GetLineLayouts_LeadingHardBreak_IncludesHardBreakGrapheme() + { + const string text = "\n\tHelloworld"; + TextBlock block = new(text, Options(-1)); + + ReadOnlySpan lines = block.GetLineLayouts(-1).Span; + + Assert.Equal(2, lines.Length); + Assert.Equal(1, lines[0].GraphemeMetrics.Length); + Assert.Equal(0, lines[0].GraphemeMetrics[0].StringIndex); + Assert.False(lines[1].GraphemeMetrics.IsEmpty); + Assert.Equal(1, lines[1].GraphemeMetrics[0].StringIndex); + } + + [Fact] + public void TextInteractionMode_Paragraph_TrimsTrailingSpacesAtLineEnd() + { + const string text = "Alpha Beta"; + float wrappingLength = GetWrappingLengthAfterTrailingSpaces(text); + TextOptions options = Options(wrappingLength); + TextBlock block = new(text, options); + + ReadOnlySpan lines = block.GetLineLayouts(wrappingLength).Span; + + // Paragraph interaction follows normal paragraph layout: the line breaks + // after the spaces, then those spaces stop contributing interaction stops. + Assert.Equal("Alpha".Length, lines[0].GraphemeMetrics.Length); + Assert.Equal("Alpha".Length - 1, lines[0].GraphemeMetrics[^1].StringIndex); + } + + [Fact] + public void TextInteractionMode_Editor_PreservesTrailingSpacesAtLineEnd() + { + const string text = "Alpha Beta"; + float wrappingLength = GetWrappingLengthAfterTrailingSpaces(text); + TextOptions options = Options(wrappingLength); + options.TextInteractionMode = TextInteractionMode.Editor; + TextBlock block = new(text, options); + + ReadOnlySpan lines = block.GetLineLayouts(wrappingLength).Span; + + // Editor interaction keeps typed trailing spaces in the line so caret + // movement can step through them before wrapping to the next visual line. + Assert.Equal("Alpha ".Length, lines[0].GraphemeMetrics.Length); + Assert.Equal("Alpha ".Length - 1, lines[0].GraphemeMetrics[^1].StringIndex); + } + + [Fact] + public void TextInteractionMode_Editor_TrimsHardBreakMarker() + { + const string text = "Alpha \nBeta"; + TextOptions options = Options(-1); + options.TextInteractionMode = TextInteractionMode.Editor; + TextBlock block = new(text, options); + + ReadOnlySpan lines = block.GetLineLayouts(-1).Span; + + // Editor interaction preserves the spaces before the forced break, but + // the hard-break marker itself is still a separator rather than line content. + Assert.Equal("Alpha ".Length, lines[0].GraphemeMetrics.Length); + Assert.Equal("Alpha ".Length - 1, lines[0].GraphemeMetrics[^1].StringIndex); + Assert.Equal(text.IndexOf('B'), lines[1].GraphemeMetrics[0].StringIndex); + } + + [Fact] + public void TextInteractionMode_Editor_TerminalHardBreakCreatesBlankLine() + { + const string text = "Alpha\n"; + TextOptions options = Options(-1); + options.TextInteractionMode = TextInteractionMode.Editor; + TextBlock block = new(text, options); + + ReadOnlySpan lines = block.GetLineLayouts(-1).Span; + + // Editor interaction needs a terminal Enter to create the next editable + // visual line. The hard break is therefore moved to its own blank line. + Assert.Equal(2, lines.Length); + Assert.Equal("Alpha".Length, lines[0].GraphemeMetrics.Length); + Assert.Single(lines[1].GraphemeMetrics.ToArray()); + Assert.True(lines[1].GraphemeMetrics[0].IsLineBreak); + } + + [Fact] + public void TextInteractionMode_Editor_CaretAfterTerminalHardBreakUsesBlankLineStart() + { + const string text = "Alpha\n"; + TextOptions options = Options(-1); + options.TextInteractionMode = TextInteractionMode.Editor; + TextMetrics metrics = TextMeasurer.Measure(text, options); + + CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); + for (int i = 0; i < text.Length; i++) + { + caret = metrics.MoveCaret(caret, CaretMovement.Next); + } + + LineMetrics blankLine = metrics.LineMetrics[1]; + + // The terminal newline owns the blank line in editor mode, but the caret + // after Enter should appear where typed text will begin on that new line. + Assert.Equal(text.Length, caret.GraphemeIndex); + Assert.Equal(new Vector2(blankLine.Start.X, blankLine.Start.Y), caret.Start, Comparer); + Assert.Equal(new Vector2(blankLine.Start.X, blankLine.Start.Y + blankLine.Extent.Y), caret.End, Comparer); + } + + [Fact] + public void TextInteractionMode_Editor_ConsecutiveTerminalHardBreaksCreateBlankLines() + { + const string text = "Alpha\n\n"; + TextOptions options = Options(-1); + options.TextInteractionMode = TextInteractionMode.Editor; + TextMetrics metrics = TextMeasurer.Measure(text, options); + + CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); + for (int i = 0; i < text.Length; i++) + { + caret = metrics.MoveCaret(caret, CaretMovement.Next); + } + + // Each terminal Enter creates its own editable blank line. The caret after + // two hard breaks should therefore land on the third visual line immediately. + Assert.Equal(3, metrics.LineMetrics.Length); + + LineMetrics finalBlankLine = metrics.LineMetrics[2]; + + Assert.Equal(text.Length, caret.GraphemeIndex); + Assert.Equal(new Vector2(finalBlankLine.Start.X, finalBlankLine.Start.Y), caret.Start, Comparer); + Assert.Equal(new Vector2(finalBlankLine.Start.X, finalBlankLine.Start.Y + finalBlankLine.Extent.Y), caret.End, Comparer); + } + + [Fact] + public void EnumerateLineLayouts_EmptyText_ReturnsNoLines() + { + TextBlock block = new(string.Empty, Options(-1)); + LineLayoutEnumerator enumerator = block.EnumerateLineLayouts(); + + Assert.False(enumerator.MoveNext(100)); + } + + [Fact] + public void EnumerateLineLayouts_MaxLinesZero_ReturnsNoLines() + { + TextOptions options = Options(100); + options.MaxLines = 0; + TextBlock block = new("Alpha beta gamma", options); + LineLayoutEnumerator enumerator = block.EnumerateLineLayouts(); + + Assert.False(enumerator.MoveNext(100)); + } + + [Fact] + public void EnumerateLineLayouts_MatchesFixedWidthLineSourceRanges() + { + const string text = "Alpha beta gamma delta epsilon."; + const float wrappingLength = 90; + TextOptions options = Options(-1); + TextBlock block = new(text, options); + + ReadOnlySpan expected = block.GetLineLayouts(wrappingLength).Span; + LineLayoutEnumerator enumerator = block.EnumerateLineLayouts(); + + for (int i = 0; i < expected.Length; i++) + { + Assert.True(enumerator.MoveNext(wrappingLength)); + + LineLayout actual = enumerator.Current; + Assert.Equal(expected[i].LineMetrics.StringIndex, actual.LineMetrics.StringIndex); + Assert.Equal(expected[i].LineMetrics.GraphemeIndex, actual.LineMetrics.GraphemeIndex); + Assert.Equal(expected[i].LineMetrics.GraphemeCount, actual.LineMetrics.GraphemeCount); + Assert.Equal(expected[i].LineMetrics.Extent, actual.LineMetrics.Extent, Comparer); + + // The enumerator returns each line as an independently placed layout so + // custom-flow callers can choose their own column or shape position. + Assert.Equal(options.Origin, actual.LineMetrics.Start); + AssertGraphemeSourceMappingEqual(expected[i].GraphemeMetrics, actual.GraphemeMetrics); + } + + Assert.False(enumerator.MoveNext(wrappingLength)); + } + + [Fact] + public void EnumerateLineLayouts_UsesSuppliedWidthPerMove() + { + const string text = "Alpha beta gamma delta epsilon."; + TextBlock block = new(text, Options(-1)); + TextOptions measureOptions = Options(-1); + + float firstBreakWidth = TextMeasurer.MeasureAdvance("Alpha beta", measureOptions).Width; + float nextBreakWidth = TextMeasurer.MeasureAdvance("Alpha beta gamma", measureOptions).Width; + float narrowWidth = firstBreakWidth + ((nextBreakWidth - firstBreakWidth) * 0.5F); + float wideWidth = TextMeasurer.MeasureAdvance(text, measureOptions).Width + 1; + + LineLayoutEnumerator enumerator = block.EnumerateLineLayouts(); + Assert.True(enumerator.MoveNext(narrowWidth)); + LineLayout narrowLine = enumerator.Current; + + Assert.True(enumerator.MoveNext(wideWidth)); + LineLayout wideLine = enumerator.Current; + + // The second call gets a wider line after the first break has consumed + // "Alpha beta", which is the behavior needed for manual flow shapes. + Assert.True(wideLine.LineMetrics.GraphemeCount > narrowLine.LineMetrics.GraphemeCount); + Assert.False(enumerator.MoveNext(wideWidth)); + } + + [Fact] + public void LineLayout_RenderTo_RendersOnlyThatLine() + { + const string text = "Alpha beta gamma delta epsilon."; + const float wrappingLength = 90; + TextBlock block = new(text, Options(-1)); + + ReadOnlySpan lines = block.GetLineLayouts(wrappingLength).Span; + LineLayout line = lines[1]; + GlyphRenderer lineRenderer = new(); + GlyphRenderer blockRenderer = new(); + + line.RenderTo(lineRenderer); + block.RenderTo(blockRenderer, wrappingLength); + + // LineLayout rendering must be usable for manual-flow consumers that + // render only the current enumerated line rather than the whole block. + Assert.Equal(line.GetGlyphMetrics().Length, lineRenderer.GlyphRects.Count); + Assert.True(blockRenderer.GlyphRects.Count > lineRenderer.GlyphRects.Count); + } + + [Fact] + public void CharacterMeasurements_MatchTextMeasurer() + { + const string text = "A quick test."; + const float wrappingLength = 70; + TextBlock block = new(text, Options(-1)); + + ReadOnlySpan expectedGlyphMetrics = TextMeasurer.GetGlyphMetrics(text, Options(wrappingLength)).Span; + ReadOnlySpan actualGlyphMetrics = block.GetGlyphMetrics(wrappingLength).Span; + + AssertGlyphMetricsEqual(expectedGlyphMetrics, actualGlyphMetrics); + + ReadOnlySpan expectedGraphemeMetrics = TextMeasurer.GetGraphemeMetrics(text, Options(wrappingLength)).Span; + ReadOnlySpan actualGraphemeMetrics = block.GetGraphemeMetrics(wrappingLength).Span; + + AssertGraphemeMetricsEqual(expectedGraphemeMetrics, actualGraphemeMetrics); + } + + [Fact] + public void TextHyphenation_None_IgnoresSoftHyphenBreak() + { + const string text = "extra\u00ADordinary"; + TextOptions measureOptions = Options(-1); + float markerBreakAdvance = TextMeasurer.MeasureAdvance("extra-", measureOptions).Width; + float fullAdvance = TextMeasurer.MeasureAdvance("extraordinary", measureOptions).Width; + + TextOptions options = Options(markerBreakAdvance + ((fullAdvance - markerBreakAdvance) * 0.5F)); + options.TextHyphenation = TextHyphenation.None; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // The wrapping width is deliberately narrow enough that "extra-" would fit and the + // full word would not. With hyphenation disabled, U+00AD remains source-mapping data + // only and must not become a line break or a visible marker. + Assert.Equal(1, metrics.LineCount); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('-'))); + } + + [Fact] + public void TextHyphenation_Custom_InsertsMarkerWhenSoftHyphenBreakIsSelected() + { + const string text = "extra\u00ADordinary"; + TextOptions measureOptions = Options(-1); + float markerBreakAdvance = TextMeasurer.MeasureAdvance("extra*", measureOptions).Width; + float fullAdvance = TextMeasurer.MeasureAdvance("extraordinary", measureOptions).Width; + + TextOptions options = Options(markerBreakAdvance + ((fullAdvance - markerBreakAdvance) * 0.5F)); + options.TextHyphenation = TextHyphenation.Custom; + options.CustomHyphen = new('*'); + + TextMetrics metrics = TextMeasurer.Measure(text, options); + GraphemeMetrics softHyphen = FindGrapheme(metrics.GraphemeMetrics, 5); + + // The source soft hyphen is a real grapheme at index 5, but it is only rendered + // when its discretionary break is selected. The generated marker uses the caller's + // custom codepoint while keeping that same source mapping, so selection and caret + // APIs still see one grapheme. + Assert.Equal(2, metrics.LineCount); + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('*'))); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('-'))); + Assert.Equal(5, softHyphen.StringIndex); + } + + [Fact] + public void TextHyphenation_Custom_DoesNotInsertMarkerBeforeHardBreak() + { + const string text = "extra\u00AD\nordinary"; + TextOptions options = Options(1000); + options.TextHyphenation = TextHyphenation.Custom; + options.CustomHyphen = new('*'); + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // The required newline is the selected break. A preceding U+00AD that was not + // selected must stay invisible; otherwise hard-break layout would grow a marker + // that the source text did not ask to display at that point. + Assert.Equal(2, metrics.LineCount); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('*'))); + } + + [Fact] + public void TextHyphenation_Custom_AccountsForMarkerAdvanceWhenChoosingBreak() + { + const string text = "a extra\u00ADordinary"; + TextOptions measureOptions = Options(-1); + float softBreakWithoutMarker = TextMeasurer.MeasureAdvance("a extra", measureOptions).Width; + float softBreakWithMarker = TextMeasurer.MeasureAdvance("a extra*", measureOptions).Width; + + TextOptions options = Options(softBreakWithoutMarker + ((softBreakWithMarker - softBreakWithoutMarker) * 0.5F)); + options.TextHyphenation = TextHyphenation.Custom; + options.CustomHyphen = new('*'); + + TextBlock block = new(text, options); + ReadOnlySpan lines = block.GetLineLayouts(options.WrappingLength).Span; + + // Without the generated marker advance, the soft-hyphen break after "extra" + // would appear to fit. Including that advance keeps the earlier space break, + // so the second line starts after "a " rather than after the soft hyphen. + Assert.True(lines.Length >= 2); + Assert.Equal(0, lines[0].LineMetrics.StringIndex); + Assert.Equal(2, lines[1].LineMetrics.StringIndex); + } + + [Fact] + public void TextEllipsis_Standard_InsertsMarkerWhenMaxLinesHidesText() + { + const string text = "one two three four five"; + TextOptions options = Options(TextMeasurer.MeasureAdvance("one two", Options(-1)).Width); + options.MaxLines = 1; + options.TextEllipsis = TextEllipsis.Standard; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // The source would wrap onto later lines, but MaxLines exposes only the + // first visual line. Standard ellipsis should replace the tail of that + // final visible line with one U+2026 marker. + Assert.Equal(1, metrics.LineCount); + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2026))); + } + + [Fact] + public void TextEllipsis_Custom_InsertsConfiguredMarkerWhenMaxLinesHidesText() + { + const string text = "one two three four five"; + TextOptions options = Options(TextMeasurer.MeasureAdvance("one two", Options(-1)).Width); + options.MaxLines = 1; + options.TextEllipsis = TextEllipsis.Custom; + options.CustomEllipsis = new('*'); + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // Custom ellipsis uses the supplied codepoint instead of the standard + // marker while preserving the same max-lines truncation behavior. + Assert.Equal(1, metrics.LineCount); + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('*'))); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2026))); + } + + [Fact] + public void TextEllipsis_None_LimitsLinesWithoutMarker() + { + const string text = "one two three four five"; + TextOptions options = Options(TextMeasurer.MeasureAdvance("one two", Options(-1)).Width); + options.MaxLines = 1; + options.TextEllipsis = TextEllipsis.None; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // MaxLines still hides later lines when ellipsis is disabled; the final + // visible line is simply clipped at the selected line boundary with no + // generated marker. + Assert.Equal(1, metrics.LineCount); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2026))); + } + + [Fact] + public void TextEllipsis_Standard_DoesNotInsertMarkerWhenTextFitsMaxLines() + { + const string text = "one two"; + TextOptions options = Options(1000); + options.MaxLines = 2; + options.TextEllipsis = TextEllipsis.Standard; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // The marker is only generated when MaxLines actually hides source text. + // A fitting paragraph keeps its original glyph stream unchanged. + Assert.Equal(1, metrics.LineCount); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2026))); + } + + [Fact] + public void TextBidiMode_Normal_KeepsLatinRunsInSourceOrder() + { + const string text = "abc def"; + TextOptions options = Options(-1); + options.TextDirection = TextDirection.RightToLeft; + options.TextBidiMode = TextBidiMode.Normal; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + ReadOnlySpan glyphs = metrics.GetGlyphMetrics().Span; + + // Normal bidi uses RTL as the paragraph direction, but Latin remains a + // strong LTR run. The run may be right-aligned by layout, but its glyph + // order is still the readable source order. + Assert.Equal(new CodePoint('a'), glyphs[0].CodePoint); + Assert.Equal(new CodePoint('b'), glyphs[1].CodePoint); + Assert.Equal(new CodePoint('c'), glyphs[2].CodePoint); + } + + [Fact] + public void TextBidiMode_Override_ReversesLatinRunsInResolvedDirection() + { + const string text = "abc def"; + TextOptions options = Options(-1); + options.TextDirection = TextDirection.RightToLeft; + options.TextBidiMode = TextBidiMode.Override; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + ReadOnlySpan glyphs = metrics.GetGlyphMetrics().Span; + + // Override feeds the bidi algorithm with the requested strong direction + // for real text. Under RTL override, Latin no longer forms an LTR run, + // so the final visual glyph order is reversed. + Assert.Equal(new CodePoint('f'), glyphs[0].CodePoint); + Assert.Equal(new CodePoint('e'), glyphs[1].CodePoint); + Assert.Equal(new CodePoint('d'), glyphs[2].CodePoint); + } + + [Fact] + public void TextBidiMode_Normal_KeepsHebrewRunsInResolvedRightToLeftOrder() + { + const string text = "אבג דהו"; + TextOptions options = new(TextLayoutTests.CreateFont(text)) + { + TextDirection = TextDirection.LeftToRight, + TextBidiMode = TextBidiMode.Normal + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + ReadOnlySpan glyphs = metrics.GetGlyphMetrics().Span; + + // Normal bidi keeps Hebrew as a strong RTL run inside the LTR paragraph. + // The first visual glyph is therefore the final Hebrew grapheme in the + // source phrase, not the first source grapheme. + Assert.Equal(new CodePoint('ו'), glyphs[0].CodePoint); + Assert.Equal(new CodePoint('ה'), glyphs[1].CodePoint); + Assert.Equal(new CodePoint('ד'), glyphs[2].CodePoint); + } + + [Fact] + public void TextBidiMode_Override_ReversesHebrewRunsInResolvedDirection() + { + const string text = "אבג דהו"; + TextOptions options = new(TextLayoutTests.CreateFont(text)) + { + TextDirection = TextDirection.LeftToRight, + TextBidiMode = TextBidiMode.Override + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + ReadOnlySpan glyphs = metrics.GetGlyphMetrics().Span; + + // LTR override forces the Hebrew letters through LTR bidi resolution, + // visibly reversing the normal RTL run order. + Assert.Equal(new CodePoint('א'), glyphs[0].CodePoint); + Assert.Equal(new CodePoint('ב'), glyphs[1].CodePoint); + Assert.Equal(new CodePoint('ג'), glyphs[2].CodePoint); + } + + [Fact] + public void GetLineLayouts_GetGlyphMetrics_MatchBlockSlices() + { + const string text = "Hello\nWorld"; + TextBlock block = new(text, Options(-1)); + + ReadOnlySpan lines = block.GetLineLayouts(-1).Span; + ReadOnlySpan expectedGlyphMetrics = block.GetGlyphMetrics(-1).Span; + + int glyphIndex = 0; + for (int i = 0; i < lines.Length; i++) + { + ReadOnlySpan lineGlyphMetrics = lines[i].GetGlyphMetrics().Span; + + AssertGlyphMetricsEqual(expectedGlyphMetrics.Slice(glyphIndex, lineGlyphMetrics.Length), lineGlyphMetrics); + + glyphIndex += lineGlyphMetrics.Length; + } + + Assert.Equal(expectedGlyphMetrics.Length, glyphIndex); + } + + [Fact] + public void HitTest_UsesGraphemeAdvanceMidpoint() + { + const string text = "Hi"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + GraphemeMetrics grapheme = metrics.GraphemeMetrics[0]; + Vector2 leadingPoint = new(grapheme.Advance.Left, grapheme.Advance.Top); + Vector2 trailingPoint = new(grapheme.Advance.Left + (grapheme.Advance.Width * 0.75F), grapheme.Advance.Top); + + TextHit leading = metrics.HitTest(leadingPoint); + TextHit trailing = metrics.HitTest(trailingPoint); + + Assert.Equal(0, leading.LineIndex); + Assert.Equal(grapheme.GraphemeIndex, leading.GraphemeIndex); + Assert.Equal(grapheme.StringIndex, leading.StringIndex); + Assert.False(leading.IsTrailing); + Assert.Equal(grapheme.GraphemeIndex, leading.GraphemeInsertionIndex); + Assert.True(trailing.IsTrailing); + Assert.Equal(grapheme.GraphemeIndex + 1, trailing.GraphemeInsertionIndex); + } + + [Fact] + public void GetCaretPosition_UsesGraphemeAdvanceEdges() + { + const string text = "H"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + GraphemeMetrics grapheme = metrics.GraphemeMetrics[0]; + LineMetrics line = metrics.LineMetrics[0]; + + CaretPosition leading = metrics.GetCaret(CaretPlacement.Start); + CaretPosition trailing = metrics.GetCaret(CaretPlacement.End); + + Assert.Equal(new Vector2(grapheme.Advance.Left, line.Start.Y), leading.Start, Comparer); + Assert.Equal(new Vector2(grapheme.Advance.Left, line.Start.Y + line.Extent.Y), leading.End, Comparer); + Assert.Equal(new Vector2(grapheme.Advance.Right, line.Start.Y), trailing.Start, Comparer); + Assert.Equal(new Vector2(grapheme.Advance.Right, line.Start.Y + line.Extent.Y), trailing.End, Comparer); + Assert.False(leading.HasSecondary); + Assert.False(trailing.HasSecondary); + } + + [Fact] + public void GetCaretPosition_UsesResolvedDirectionForRtlStartAndEnd() + { + const string text = "אבג"; + Font font = TextLayoutTests.CreateFont(text); + TextOptions options = new(font) + { + Dpi = font.FontMetrics.ScaleFactor, + TextDirection = TextDirection.RightToLeft + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + LineMetrics line = metrics.LineMetrics[0]; + + // In RTL text, the source start maps to the physical end of the line box, + // and the source end maps to the physical start of that same line box. + CaretPosition start = metrics.GetCaret(CaretPlacement.Start); + CaretPosition end = metrics.GetCaret(CaretPlacement.End); + + float startX = line.Start.X + line.Extent.X; + float endX = line.Start.X; + + Assert.Equal(new Vector2(startX, line.Start.Y), start.Start, Comparer); + Assert.Equal(new Vector2(startX, line.Start.Y + line.Extent.Y), start.End, Comparer); + Assert.Equal(new Vector2(endX, line.Start.Y), end.Start, Comparer); + Assert.Equal(new Vector2(endX, line.Start.Y + line.Extent.Y), end.End, Comparer); + } + + [Fact] + public void MoveCaret_PreviousAndNext_MoveByGraphemeInsertionIndex() + { + const string text = "ABC"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + CaretPosition caret = metrics.MoveCaret(metrics.GetCaret(CaretPlacement.Start), CaretMovement.Next); + + Assert.Equal(0, metrics.MoveCaret(caret, CaretMovement.Previous).GraphemeIndex); + Assert.Equal(2, metrics.MoveCaret(caret, CaretMovement.Next).GraphemeIndex); + } + + [Fact] + public void MoveCaret_PreviousAndNext_UseSourceOrderThroughBidiText() + { + const string text = "abc אבג"; + Font font = TextLayoutTests.CreateFont(text); + TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // Previous/Next model keyboard movement through the source string. Inside + // a bidi run that means the caret may jump visually, but the source + // insertion indices still advance one grapheme boundary at a time. + CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); + for (int i = 0; i < 5; i++) + { + caret = metrics.MoveCaret(caret, CaretMovement.Next); + } + + Assert.Equal(5, caret.GraphemeIndex); + Assert.Equal(4, metrics.MoveCaret(caret, CaretMovement.Previous).GraphemeIndex); + Assert.Equal(6, metrics.MoveCaret(caret, CaretMovement.Next).GraphemeIndex); + } + + [Fact] + public void GetWordMetrics_UsesUnicodeWordBoundarySegments() + { + const string text = "can't stop"; + TextBlock block = new(text, Options(-1)); + TextMetrics metrics = block.Measure(-1); + + ReadOnlySpan blockMetrics = block.GetWordMetrics(-1).Span; + ReadOnlySpan metricWords = metrics.WordMetrics; + + // UAX #29 keeps the apostrophe inside "can't", but the space remains its own + // word-boundary segment. That matters because browser-style double-click and + // word navigation can land on separator segments as well as letter segments. + Assert.Equal(3, blockMetrics.Length); + AssertWordMetrics(blockMetrics[0], 0, 5, 0, 5); + AssertWordMetrics(blockMetrics[1], 5, 6, 5, 6); + AssertWordMetrics(blockMetrics[2], 6, 10, 6, 10); + + // TextBlock.GetWordMetrics uses the direct word-only path, while Measure exposes + // the word metrics gathered alongside full grapheme metrics. The public rectangles + // and source ranges must match so callers can choose either entry point freely. + AssertWordMetrics(blockMetrics[0], metricWords[0]); + AssertWordMetrics(blockMetrics[1], metricWords[1]); + AssertWordMetrics(blockMetrics[2], metricWords[2]); + + // Whitespace is a word-boundary segment and it has measurable advance/bounds in + // this API, so the space word metrics should be exactly the space grapheme metrics. + GraphemeMetrics space = FindGrapheme(metrics.GraphemeMetrics, 5); + Assert.Equal(space.Advance, metricWords[1].Advance, Comparer); + Assert.Equal(space.Bounds, metricWords[1].Bounds, Comparer); + Assert.Equal(space.RenderableBounds, metricWords[1].RenderableBounds, Comparer); + } + + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void GetWordMetrics_MatchesMeasureWordMetrics_ForComplexLayout(LayoutMode layoutMode) + { + const string text = "can't e\u0301 שלום\nwrap אבג stop"; + Font font = TextLayoutTests.CreateFont(text); + TextOptions options = new(font) + { + Dpi = font.FontMetrics.ScaleFactor, + LayoutMode = layoutMode, + WrappingLength = 110 + }; + + TextBlock block = new(text, options); + + // Measure builds grapheme metrics and word metrics in the same visitor. GetWordMetrics + // uses the allocation-saving word-only visitor, so this text deliberately mixes word + // separators, a multi-codepoint grapheme, bidi runs, a hard break, and wrapping to pin + // every flush path against the full measurement pipeline. + ReadOnlySpan expected = block.Measure(options.WrappingLength).WordMetrics; + ReadOnlySpan actual = block.GetWordMetrics(options.WrappingLength).Span; + + AssertWordMetricsEqual(expected, actual); + } + + [Fact] + public void GetWordMetrics_UsesHitGraphemeForTrailingSide() + { + const string text = "can't stop"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + GraphemeMetrics finalWordGrapheme = FindGrapheme(metrics.GraphemeMetrics, 4); + Vector2 trailingPoint = new( + finalWordGrapheme.Advance.Right - (finalWordGrapheme.Advance.Width * 0.25F), + FontRectangle.Center(finalWordGrapheme.Advance).Y); + + TextHit hit = metrics.HitTest(trailingPoint); + WordMetrics word = metrics.GetWordMetrics(hit); + + // The hit is on the trailing side of "t", so its insertion index is after the word. + // Word selection still uses the hit grapheme itself, not the following space segment. + Assert.Equal(5, hit.GraphemeInsertionIndex); + AssertWordMetrics(word, 0, 5, 0, 5); + } + + [Fact] + public void MoveCaret_PreviousWordAndNextWord_MoveByUnicodeWordBoundaries() + { + const string text = "can't stop"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + + // UAX #29 word boundaries produce three segments here: "can't", " ", and "stop". + // Word movement walks those boundaries in source order rather than skipping separators. + CaretPosition caret = metrics.GetCaret(CaretPlacement.Start); + caret = metrics.MoveCaret(caret, CaretMovement.NextWord); + Assert.Equal(5, caret.GraphemeIndex); + + caret = metrics.MoveCaret(caret, CaretMovement.NextWord); + Assert.Equal(6, caret.GraphemeIndex); + + caret = metrics.MoveCaret(caret, CaretMovement.NextWord); + Assert.Equal(10, caret.GraphemeIndex); + + caret = metrics.MoveCaret(caret, CaretMovement.PreviousWord); + Assert.Equal(6, caret.GraphemeIndex); + + caret = metrics.MoveCaret(caret, CaretMovement.PreviousWord); + Assert.Equal(5, caret.GraphemeIndex); + + caret = metrics.MoveCaret(caret, CaretMovement.PreviousWord); + Assert.Equal(0, caret.GraphemeIndex); + } + + [Fact] + public void GetSelectionBounds_UsesWordMetrics() + { + const string text = "can't stop"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + GraphemeMetrics wordGrapheme = FindGrapheme(metrics.GraphemeMetrics, 2); + TextHit hit = metrics.HitTest(FontRectangle.Center(wordGrapheme.Advance)); + WordMetrics word = metrics.GetWordMetrics(hit); + + ReadOnlySpan actual = metrics.GetSelectionBounds(word).Span; + + Assert.Equal(1, actual.Length); + AssertWordMetrics(word, 0, 5, 0, 5); + Assert.True(SelectionContains(actual, FontRectangle.Center(wordGrapheme.Advance))); + } + + [Fact] + public void GetSelectionBounds_UsesCaretPositionsMovedByWord() + { + const string text = "can't stop"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); + + // This mimics Shift+Ctrl+Right on Windows-style editors: the anchor stays where + // selection began, while the focus caret advances by Unicode word-boundary segments. + // The first move selects "can't"; the second also selects the separator segment + // because our word movement intentionally exposes spaces as selectable segments. + CaretPosition focus = metrics.MoveCaret(anchor, CaretMovement.NextWord); + AssertSelectionBoundsEqual(metrics.GetSelectionBounds(metrics.GetWordMetrics(anchor)).Span, metrics.GetSelectionBounds(anchor, focus).Span); + + focus = metrics.MoveCaret(focus, CaretMovement.NextWord); + ReadOnlyMemory firstTwoWords = metrics.GetSelectionBounds(anchor, focus); + + Assert.True(SelectionContains(firstTwoWords.Span, FontRectangle.Center(FindGrapheme(metrics.GraphemeMetrics, 0).Advance))); + Assert.True(SelectionContains(firstTwoWords.Span, FontRectangle.Center(FindGrapheme(metrics.GraphemeMetrics, 5).Advance))); + + // Reverse word selection should produce the same rectangles for the same insertion + // range even though the caret movement started at the far end of the text. + CaretPosition reverseAnchor = metrics.GetCaret(CaretPlacement.End); + CaretPosition reverseFocus = metrics.MoveCaret(reverseAnchor, CaretMovement.PreviousWord); + + AssertSelectionBoundsEqual(metrics.GetSelectionBounds(metrics.GetWordMetrics(reverseFocus)).Span, metrics.GetSelectionBounds(reverseAnchor, reverseFocus).Span); + } + + [Fact] + public void MoveCaret_StartAndEnd_MovesWithinLineAndText() + { + const string text = "Hi\nYo"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + CaretPosition firstLineCaret = metrics.MoveCaret(metrics.GetCaret(CaretPlacement.Start), CaretMovement.Next); + CaretPosition secondLineCaret = metrics.MoveCaret(metrics.GetCaret(CaretPlacement.End), CaretMovement.Previous); + + // LineEnd stops at the source gap left by the trimmed hard break. + // TextEnd moves to the final source insertion position of the measured text block. + Assert.Equal(0, metrics.MoveCaret(firstLineCaret, CaretMovement.LineStart).GraphemeIndex); + Assert.Equal(2, metrics.MoveCaret(firstLineCaret, CaretMovement.LineEnd).GraphemeIndex); + Assert.Equal(0, metrics.MoveCaret(secondLineCaret, CaretMovement.TextStart).GraphemeIndex); + Assert.Equal(5, metrics.MoveCaret(secondLineCaret, CaretMovement.TextEnd).GraphemeIndex); + } + + [Fact] + public void MoveCaret_LineStartAndLineEnd_UseResolvedDirectionForRtlLine() + { + const string text = "abc\nאבג"; + Font font = TextLayoutTests.CreateFont(text); + TextOptions options = new(font) + { + Dpi = font.FontMetrics.ScaleFactor, + TextDirection = TextDirection.RightToLeft + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + GraphemeMetrics middleHebrew = FindGrapheme(metrics.GraphemeMetrics, 5); + CaretPosition caret = metrics.GetCaretPosition(metrics.HitTest(FontRectangle.Center(middleHebrew.Advance))); + LineMetrics line = metrics.LineMetrics[1]; + + // Home/End style movement uses the line's source boundaries. For RTL, + // LineStart maps to the physical end of the line box, and LineEnd maps + // to the physical start of that same line box. + CaretPosition lineStart = metrics.MoveCaret(caret, CaretMovement.LineStart); + CaretPosition lineEnd = metrics.MoveCaret(caret, CaretMovement.LineEnd); + + float lineStartX = line.Start.X + line.Extent.X; + float lineEndX = line.Start.X; + + Assert.Equal(new Vector2(lineStartX, line.Start.Y), lineStart.Start, Comparer); + Assert.Equal(new Vector2(lineStartX, line.Start.Y + line.Extent.Y), lineStart.End, Comparer); + Assert.Equal(new Vector2(lineEndX, line.Start.Y), lineEnd.Start, Comparer); + Assert.Equal(new Vector2(lineEndX, line.Start.Y + line.Extent.Y), lineEnd.End, Comparer); + } + + [Fact] + public void MoveCaret_LineDown_PreservesPositionAcrossShortLine() + { + const string text = "Hello\nA\nHello"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + CaretPosition firstLineEnd = metrics.MoveCaret(metrics.GetCaret(CaretPlacement.Start), CaretMovement.LineEnd); + + // The first LineDown clamps to the end of the short middle line. The second + // LineDown should still use the original "Hello" end position, not the + // clamped middle-line position, so it reaches the end of the final line. + CaretPosition middleLineEnd = metrics.MoveCaret(firstLineEnd, CaretMovement.LineDown); + CaretPosition finalLineEnd = metrics.MoveCaret(middleLineEnd, CaretMovement.LineDown); + + Assert.Equal(7, middleLineEnd.GraphemeIndex); + Assert.Equal(13, finalLineEnd.GraphemeIndex); + Assert.Equal(firstLineEnd.Start.X, finalLineEnd.Start.X, Comparer); + } + + [Fact] + public void MoveCaret_LineUp_PreservesPositionAcrossShortLine() + { + const string text = "Hello\nA\nHello"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + CaretPosition finalLineEnd = metrics.MoveCaret(metrics.GetCaret(CaretPlacement.End), CaretMovement.LineEnd); + + // The first LineUp clamps to the end of the short middle line. The second + // LineUp should still use the original "Hello" end position, not the + // clamped middle-line position, so it reaches the end of the first line. + CaretPosition middleLineEnd = metrics.MoveCaret(finalLineEnd, CaretMovement.LineUp); + CaretPosition firstLineEnd = metrics.MoveCaret(middleLineEnd, CaretMovement.LineUp); + + Assert.Equal(7, middleLineEnd.GraphemeIndex); + Assert.Equal(5, firstLineEnd.GraphemeIndex); + Assert.Equal(finalLineEnd.Start.X, firstLineEnd.Start.X, Comparer); + } + + [Fact] + public void HitTest_UsesBidiLogicalTrailingSide() + { + const string text = "abc אבג def"; + Font font = TextLayoutTests.CreateFont(text); + TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; + TextMetrics metrics = TextMeasurer.Measure(text, options); + GraphemeMetrics grapheme = FindGrapheme(metrics.GraphemeMetrics, 4); + Vector2 center = FontRectangle.Center(grapheme.Advance); + + // Source order is "abc " then the Hebrew run "אבג" then " def". + // Grapheme index 4 is "א", the first Hebrew grapheme in logical order. + // Because the Hebrew run is RTL, "א" is painted at the right-hand side of + // that run: a point near its visual right edge is before the grapheme + // logically, while a point near its visual left edge is after it. + Vector2 leadingPoint = new(grapheme.Advance.Right - (grapheme.Advance.Width * 0.25F), center.Y); + Vector2 trailingPoint = new(grapheme.Advance.Left + (grapheme.Advance.Width * 0.25F), center.Y); + + TextHit leading = metrics.HitTest(leadingPoint); + TextHit trailing = metrics.HitTest(trailingPoint); + + Assert.Equal(grapheme.GraphemeIndex, leading.GraphemeIndex); + Assert.False(leading.IsTrailing); + Assert.Equal(grapheme.GraphemeIndex, leading.GraphemeInsertionIndex); + Assert.True(trailing.IsTrailing); + Assert.Equal(grapheme.GraphemeIndex + 1, trailing.GraphemeInsertionIndex); + } + + [Fact] + public void GetSelectionBounds_UsesHitInsertionIndexesForBidiDragSelection() + { + const string text = "abc אבג def"; + Font font = TextLayoutTests.CreateFont(text); + TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; + TextMetrics metrics = TextMeasurer.Measure(text, options); + GraphemeMetrics first = FindGrapheme(metrics.GraphemeMetrics, 0); + GraphemeMetrics selectedRtl = FindGrapheme(metrics.GraphemeMetrics, 4); + GraphemeMetrics unselectedRtl = FindGrapheme(metrics.GraphemeMetrics, 5); + + // The drag starts at the leading edge of "a", giving insertion index 0. + // The focus point is on the trailing side of logical "א". Since "א" is + // in an RTL run, that trailing side is its visual left side, so callers + // should still be able to pass the raw hit without applying bidi rules. + Vector2 anchorPoint = new(first.Advance.Left, FontRectangle.Center(first.Advance).Y); + Vector2 focusPoint = new( + selectedRtl.Advance.Left + (selectedRtl.Advance.Width * 0.25F), + FontRectangle.Center(selectedRtl.Advance).Y); + + TextHit anchor = metrics.HitTest(anchorPoint); + TextHit focus = metrics.HitTest(focusPoint); + + Assert.Equal(0, anchor.GraphemeInsertionIndex); + Assert.Equal(5, focus.GraphemeInsertionIndex); + + ReadOnlySpan selection = metrics.GetSelectionBounds(anchor, focus).Span; + + Assert.Equal(2, selection.Length); + Assert.True(SelectionContains(selection, FontRectangle.Center(selectedRtl.Advance))); + Assert.False(SelectionContains(selection, FontRectangle.Center(unselectedRtl.Advance))); + } + + [Fact] + public void GetSelectionBounds_UsesCaretPositions() + { + const string text = "ABC"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); + CaretPosition focus = metrics.MoveCaret(anchor, CaretMovement.Next); + + ReadOnlySpan actual = metrics.GetSelectionBounds(anchor, focus).Span; + + Assert.Single(actual.ToArray()); + Assert.Equal(metrics.GetSelectionBounds(metrics.GraphemeMetrics[0]).Span[0], actual[0], Comparer); + } + + [Fact] + public void GetCaretPosition_ExposesSecondaryCaretAtBidiBoundary() + { + const string text = "abc אבג def"; + Font font = TextLayoutTests.CreateFont(text); + TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; + TextMetrics metrics = TextMeasurer.Measure(text, options); + LineMetrics line = metrics.LineMetrics[0]; + GraphemeMetrics previous = FindGrapheme(metrics.GraphemeMetrics, 3); + GraphemeMetrics next = FindGrapheme(metrics.GraphemeMetrics, 4); + + // Grapheme index 3 is the LTR space before the Hebrew run, and grapheme + // index 4 is "א", the first Hebrew grapheme in source order. The logical + // insertion position 4 is therefore both after the LTR space and before + // the RTL Hebrew run. Those are different visual edges: the normal caret + // is at the leading edge of "א" (its visual right edge), while the + // secondary caret is at the trailing edge of the preceding space. + CaretPosition caret = metrics.GetCaretPosition(metrics.HitTest(FontRectangle.Center(next.Advance))); + + Assert.Equal(4, caret.GraphemeIndex); + Assert.True(caret.HasSecondary); + Assert.Equal(new Vector2(next.Advance.Right, line.Start.Y), caret.Start, Comparer); + Assert.Equal(new Vector2(next.Advance.Right, line.Start.Y + line.Extent.Y), caret.End, Comparer); + Assert.Equal(new Vector2(previous.Advance.Right, line.Start.Y), caret.SecondaryStart, Comparer); + Assert.Equal(new Vector2(previous.Advance.Right, line.Start.Y + line.Extent.Y), caret.SecondaryEnd, Comparer); + } + + [Fact] + public void GetSelectionBounds_ReturnsOneRectanglePerVisualLineForContinuousSelection() + { + const string text = "Hi\nYo"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + + // The selected range covers both visual lines. Since each line is visually + // continuous, each line should produce one selection rectangle spanning + // the selected grapheme advances while using the full line box height. + CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); + CaretPosition focus = metrics.MoveCaret(anchor, CaretMovement.TextEnd); + ReadOnlySpan selection = metrics.GetSelectionBounds(anchor, focus).Span; + + Assert.Equal(metrics.LineMetrics.Length, selection.Length); + int graphemeOffset = 0; + for (int i = 0; i < metrics.LineMetrics.Length; i++) + { + LineMetrics line = metrics.LineMetrics[i]; + ReadOnlySpan lineGraphemes = metrics.GraphemeMetrics.Slice(graphemeOffset, line.GraphemeCount); + FontRectangle advance = lineGraphemes[0].Advance; + float start = advance.Left; + float end = advance.Right; + for (int j = 1; j < lineGraphemes.Length; j++) + { + advance = lineGraphemes[j].Advance; + start = Math.Min(start, advance.Left); + end = Math.Max(end, advance.Right); + } + + FontRectangle expected = FontRectangle.FromLTRB(start, line.Start.Y, end, line.Start.Y + line.Extent.Y); + Assert.Equal(expected, selection[i], Comparer); + graphemeOffset += line.GraphemeCount; + } + } + + [Fact] + public void GetSelectionBounds_UsesLineMetricsStartForLineBox() + { + const string text = "Hi"; + TextOptions options = Options(-1); + options.Origin = new(10, 20); + TextMetrics metrics = TextMeasurer.Measure(text, options); + LineMetrics line = metrics.LineMetrics[0]; + FontRectangle first = metrics.GraphemeMetrics[0].Advance; + FontRectangle second = metrics.GraphemeMetrics[1].Advance; + + // Selection rectangles use the line box for the cross-axis extent, not the + // individual grapheme bounds. The origin therefore shifts the rectangle's + // Y coordinates even though the horizontal range comes from grapheme advances. + ReadOnlySpan selection = metrics.GetSelectionBounds(metrics.GetCaret(CaretPlacement.Start), metrics.GetCaret(CaretPlacement.End)).Span; + + FontRectangle expected = FontRectangle.FromLTRB( + Math.Min(first.Left, second.Left), + line.Start.Y, + Math.Max(first.Right, second.Right), + line.Start.Y + line.Extent.Y); + + Assert.Equal(expected, selection[0], Comparer); + } + + [Fact] + public void GetSelectionBounds_SplitsVisuallyDiscontinuousBidiRange() + { + const string text = "abc אבג def"; + Font font = TextLayoutTests.CreateFont(text); + TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; + TextMetrics metrics = TextMeasurer.Measure(text, options); + GraphemeMetrics selectedBeforeGap = FindGrapheme(metrics.GraphemeMetrics, 5); + GraphemeMetrics unselectedGap = FindGrapheme(metrics.GraphemeMetrics, 6); + + // Source graphemes 2..5 are selected: "c", the following space, and + // Hebrew "א" + "ב". The Hebrew run is visually reversed, so unselected + // "ג" (grapheme index 6) is painted between the selected LTR fragment + // and the selected Hebrew fragment. The result should therefore be two + // selection rectangles, and neither may cover the center of "ג". + CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); + anchor = metrics.MoveCaret(anchor, CaretMovement.Next); + anchor = metrics.MoveCaret(anchor, CaretMovement.Next); + CaretPosition focus = anchor; + for (int i = 0; i < 4; i++) + { + focus = metrics.MoveCaret(focus, CaretMovement.Next); + } + + ReadOnlySpan selection = metrics.GetSelectionBounds(anchor, focus).Span; + + Assert.Equal(2, selection.Length); + Assert.True(SelectionContains(selection, FontRectangle.Center(selectedBeforeGap.Advance))); + Assert.False(SelectionContains(selection, FontRectangle.Center(unselectedGap.Advance))); + } + + [Fact] + public void GetSelectionBounds_IgnoresTrimmedHardBreak() + { + const string text = "A\nB"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + + // The hard break ends a non-empty line, so it is trimmed with trailing + // breaking whitespace. Selecting only that source grapheme therefore + // has no measured grapheme to paint. + Assert.DoesNotContain(metrics.GraphemeMetrics.ToArray(), x => x.GraphemeIndex == 1); + + CaretPosition anchor = metrics.MoveCaret(metrics.GetCaret(CaretPlacement.Start), CaretMovement.Next); + CaretPosition focus = metrics.MoveCaret(anchor, CaretMovement.Next); + ReadOnlySpan selection = metrics.GetSelectionBounds(anchor, focus).Span; + + Assert.True(selection.IsEmpty); + } + + [Fact] + public void GetSelectionBounds_IncludesBlankLineHardBreak() + { + const string text = "\nA"; + TextMetrics metrics = TextMeasurer.Measure(text, Options(-1)); + GraphemeMetrics hardBreak = FindGrapheme(metrics.GraphemeMetrics, 0); + + // A leading hard break is the only grapheme on its line. It is preserved + // so the blank line has real line geometry for selection. + Assert.True(hardBreak.IsLineBreak); + + ReadOnlySpan selection = metrics.GetSelectionBounds(hardBreak).Span; + + Assert.Equal(1, selection.Length); + Assert.True(SelectionContains(selection, FontRectangle.Center(hardBreak.Advance))); + } + + [Theory] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void GetSelectionBounds_UsesOwningLineInReverseLineOrder(LayoutMode layoutMode) + { + const string text = "Hi\nYo"; + TextOptions options = Options(-1); + options.Origin = new(13, 29); + options.LayoutMode = layoutMode; + TextBlock block = new(text, options); + + TextMetrics metrics = block.Measure(-1); + ReadOnlySpan lines = block.GetLineLayouts(-1).Span; + + for (int i = 0; i < lines.Length; i++) + { + GraphemeMetrics grapheme = lines[i].GraphemeMetrics[0]; + + // Reverse line-order modes emit grapheme metrics in visual order, but line metrics + // retain their source line index. Full-text selection must still find the same + // owning line slice that the line-local API already has. + ReadOnlySpan expected = lines[i].GetSelectionBounds(grapheme).Span; + ReadOnlySpan actual = metrics.GetSelectionBounds(grapheme).Span; + + Assert.Single(actual.ToArray()); + Assert.Equal(expected[0], actual[0], Comparer); + } + } + + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void GetLineMetrics_StartMatchesPositionedGraphemeAdvances(LayoutMode layoutMode) + { + const string text = "Hi\nYo"; + TextOptions options = Options(-1); + options.Origin = new(13, 29); + options.LayoutMode = layoutMode; + TextBlock block = new(text, options); + + ReadOnlySpan lines = block.GetLineLayouts(-1).Span; + + Assert.Equal(2, lines.Length); + for (int i = 0; i < lines.Length; i++) + { + LineMetrics line = lines[i].LineMetrics; + FontRectangle advance = lines[i].GraphemeMetrics[0].Advance; + if (layoutMode.IsHorizontal()) + { + Assert.Equal(advance.Top, line.Start.Y, Comparer); + Assert.Equal(advance.Height, line.Extent.Y, Comparer); + continue; + } + + Assert.Equal(advance.Left, line.Start.X, Comparer); + Assert.Equal(advance.Width, line.Extent.X, Comparer); + } + } + + [Fact] + public void LineLayoutInteractionMethods_MatchTextMetrics() + { + const string text = "Hi\nYo"; + TextBlock block = new(text, Options(-1)); + TextMetrics metrics = block.Measure(-1); + ReadOnlySpan lines = block.GetLineLayouts(-1).Span; + GraphemeMetrics grapheme = lines[1].GraphemeMetrics[0]; + Vector2 point = FontRectangle.Center(grapheme.Advance); + + TextHit lineHit = lines[1].HitTest(point); + TextHit metricsHit = metrics.HitTest(point); + TextHit lineCaretHit = lines[1].HitTest(point); + TextHit metricsCaretHit = metrics.HitTest(point); + CaretPosition lineCaret = lines[1].GetCaretPosition(lineCaretHit); + CaretPosition metricsCaret = metrics.GetCaretPosition(metricsCaretHit); + + Assert.Equal(metricsHit.LineIndex, lineHit.LineIndex); + Assert.Equal(metricsHit.GraphemeIndex, lineHit.GraphemeIndex); + Assert.Equal(metricsHit.StringIndex, lineHit.StringIndex); + Assert.Equal(metricsHit.IsTrailing, lineHit.IsTrailing); + Assert.Equal(metricsCaret.Start, lineCaret.Start); + Assert.Equal(metricsCaret.End, lineCaret.End); + Assert.Equal(metricsCaret.HasSecondary, lineCaret.HasSecondary); + Assert.Equal(metricsCaret.SecondaryStart, lineCaret.SecondaryStart); + Assert.Equal(metricsCaret.SecondaryEnd, lineCaret.SecondaryEnd); + Assert.Equal(metricsCaret.LineNavigationPosition, lineCaret.LineNavigationPosition, Comparer); + AssertWordMetrics(metrics.GetWordMetrics(metricsHit), lines[1].GetWordMetrics(lineHit)); + + CaretPosition nextMetricsCaret = metrics.MoveCaret(metricsCaret, CaretMovement.Next); + CaretPosition nextLineCaret = lines[1].MoveCaret(lineCaret, CaretMovement.Next); + CaretPosition nextWordMetricsCaret = metrics.MoveCaret(metricsCaret, CaretMovement.NextWord); + CaretPosition nextWordLineCaret = lines[1].MoveCaret(lineCaret, CaretMovement.NextWord); + + Assert.Equal(nextMetricsCaret.GraphemeIndex, nextLineCaret.GraphemeIndex); + Assert.Equal(nextWordMetricsCaret.GraphemeIndex, nextWordLineCaret.GraphemeIndex); + + Assert.Equal( + metrics.GetSelectionBounds(grapheme).Span[0], + lines[1].GetSelectionBounds(grapheme).Span[0], + Comparer); + + Assert.Equal( + metrics.GetSelectionBounds(metricsCaret, nextMetricsCaret).Span[0], + lines[1].GetSelectionBounds(lineCaret, nextLineCaret).Span[0], + Comparer); + } + + [Fact] + public void EmptyText_ReturnsEmptyMeasurements() + { + TextBlock block = new(string.Empty, Options(-1)); + + TextMetrics metrics = block.Measure(100); + + Assert.Equal(FontRectangle.Empty, metrics.Advance, Comparer); + Assert.Equal(FontRectangle.Empty, metrics.Bounds, Comparer); + Assert.Equal(FontRectangle.Empty, metrics.RenderableBounds, Comparer); + Assert.Equal(0, metrics.LineCount); + Assert.True(metrics.GetGlyphMetrics().IsEmpty); + Assert.True(metrics.GraphemeMetrics.IsEmpty); + Assert.True(metrics.LineMetrics.IsEmpty); + Assert.True(metrics.WordMetrics.IsEmpty); + Assert.Equal(FontRectangle.Empty, block.MeasureAdvance(100), Comparer); + Assert.Equal(FontRectangle.Empty, block.MeasureBounds(100), Comparer); + Assert.Equal(FontRectangle.Empty, block.MeasureRenderableBounds(100), Comparer); + Assert.Equal(0, block.CountLines(100)); + Assert.True(block.GetLineMetrics(100).IsEmpty); + Assert.True(block.GetWordMetrics(100).IsEmpty); + + Assert.True(block.GetGlyphMetrics(100).IsEmpty); + + Assert.True(block.GetGraphemeMetrics(100).IsEmpty); + } + + private static TextOptions Options(float wrappingLength) + => new(Font) { WrappingLength = wrappingLength }; + + private static float GetWrappingLengthAfterTrailingSpaces(string text) + { + TextOptions options = Options(-1); + int firstNonSpaceAfterBreak = text.IndexOf('B'); + string lineWithSpaces = text[..firstNonSpaceAfterBreak]; + string lineWithNextGrapheme = text[..(firstNonSpaceAfterBreak + 1)]; + float widthBeforeNextGrapheme = TextMeasurer.MeasureAdvance(lineWithSpaces, options).Width; + float widthAfterNextGrapheme = TextMeasurer.MeasureAdvance(lineWithNextGrapheme, options).Width; + + // The width is after the trailing-space break opportunity but before the next + // grapheme, forcing the first line to own only "Alpha " before finalization. + return widthBeforeNextGrapheme + ((widthAfterNextGrapheme - widthBeforeNextGrapheme) * 0.5F); + } + + private static GraphemeMetrics FindGrapheme(ReadOnlySpan graphemes, int graphemeIndex) + { + for (int i = 0; i < graphemes.Length; i++) + { + if (graphemes[i].GraphemeIndex == graphemeIndex) + { + return graphemes[i]; + } + } + + throw new InvalidOperationException("The expected grapheme was not measured."); + } + + private static bool SelectionContains(ReadOnlySpan selection, Vector2 point) + { + for (int i = 0; i < selection.Length; i++) + { + if (selection[i].Contains(point)) + { + return true; + } + } + + return false; + } + + private static int CountGlyphs(ReadOnlySpan glyphs, CodePoint codePoint) + { + int count = 0; + for (int i = 0; i < glyphs.Length; i++) + { + if (glyphs[i].CodePoint == codePoint) + { + count++; + } + } + + return count; + } + + private static void AssertSelectionBoundsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i], actual[i], Comparer); + } + } + + private static void AssertTextMetricsEqual(TextMetrics expected, TextMetrics actual) + { + Assert.Equal(expected.Advance, actual.Advance, Comparer); + Assert.Equal(expected.Bounds, actual.Bounds, Comparer); + Assert.Equal(expected.RenderableBounds, actual.RenderableBounds, Comparer); + Assert.Equal(expected.LineCount, actual.LineCount); + AssertGlyphMetricsEqual(expected.GetGlyphMetrics().Span, actual.GetGlyphMetrics().Span); + AssertGraphemeMetricsEqual(expected.GraphemeMetrics, actual.GraphemeMetrics); + AssertLineMetricsEqual(expected.LineMetrics, actual.LineMetrics); + AssertWordMetricsEqual(expected.WordMetrics, actual.WordMetrics); + } + + private static void AssertGlyphMetricsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + AssertGlyphMetricsEqual(expected[i], actual[i]); + } + } + + private static void AssertGlyphMetricsEqual(GlyphMetrics expected, GlyphMetrics actual) + { + Assert.Equal(expected.CodePoint, actual.CodePoint); + Assert.Equal(expected.Advance, actual.Advance, Comparer); + Assert.Equal(expected.Bounds, actual.Bounds, Comparer); + Assert.Equal(expected.RenderableBounds, actual.RenderableBounds, Comparer); + Assert.Equal(expected.GraphemeIndex, actual.GraphemeIndex); + Assert.Equal(expected.StringIndex, actual.StringIndex); + } + + private static void AssertGraphemeMetricsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].Advance, actual[i].Advance, Comparer); + Assert.Equal(expected[i].Bounds, actual[i].Bounds, Comparer); + Assert.Equal(expected[i].RenderableBounds, actual[i].RenderableBounds, Comparer); + Assert.Equal(expected[i].GraphemeIndex, actual[i].GraphemeIndex); + Assert.Equal(expected[i].StringIndex, actual[i].StringIndex); + Assert.Equal(expected[i].BidiLevel, actual[i].BidiLevel); + Assert.Equal(expected[i].IsLineBreak, actual[i].IsLineBreak); + } + } + + private static void AssertGraphemeSourceMappingEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].GraphemeIndex, actual[i].GraphemeIndex); + Assert.Equal(expected[i].StringIndex, actual[i].StringIndex); + Assert.Equal(expected[i].BidiLevel, actual[i].BidiLevel); + Assert.Equal(expected[i].IsLineBreak, actual[i].IsLineBreak); + } + } + + private static void AssertWordMetricsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + AssertWordMetrics(expected[i], actual[i]); + } + } + + private static void AssertWordMetrics(WordMetrics actual, WordMetrics expected) + { + Assert.Equal(expected.Advance, actual.Advance, Comparer); + Assert.Equal(expected.Bounds, actual.Bounds, Comparer); + Assert.Equal(expected.RenderableBounds, actual.RenderableBounds, Comparer); + AssertWordMetrics( + actual, + expected.GraphemeStart, + expected.GraphemeEnd, + expected.StringStart, + expected.StringEnd); + } + + private static void AssertWordMetrics( + WordMetrics actual, + int graphemeStart, + int graphemeEnd, + int stringStart, + int stringEnd) + { + Assert.Equal(graphemeStart, actual.GraphemeStart); + Assert.Equal(graphemeEnd, actual.GraphemeEnd); + Assert.Equal(stringStart, actual.StringStart); + Assert.Equal(stringEnd, actual.StringEnd); + } + + private static void AssertLineMetricsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].Ascender, actual[i].Ascender, Comparer); + Assert.Equal(expected[i].Baseline, actual[i].Baseline, Comparer); + Assert.Equal(expected[i].Descender, actual[i].Descender, Comparer); + Assert.Equal(expected[i].LineHeight, actual[i].LineHeight, Comparer); + Assert.Equal(expected[i].Start, actual[i].Start, Comparer); + Assert.Equal(expected[i].Extent, actual[i].Extent, Comparer); + Assert.Equal(expected[i].StringIndex, actual[i].StringIndex); + Assert.Equal(expected[i].GraphemeIndex, actual[i].GraphemeIndex); + Assert.Equal(expected[i].GraphemeCount, actual[i].GraphemeCount); + } + } + + private static void AssertLineMetricsEqual(ReadOnlySpan expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + Assert.Equal(expected[i].Ascender, actual[i].LineMetrics.Ascender, Comparer); + Assert.Equal(expected[i].Baseline, actual[i].LineMetrics.Baseline, Comparer); + Assert.Equal(expected[i].Descender, actual[i].LineMetrics.Descender, Comparer); + Assert.Equal(expected[i].LineHeight, actual[i].LineMetrics.LineHeight, Comparer); + Assert.Equal(expected[i].Start, actual[i].LineMetrics.Start, Comparer); + Assert.Equal(expected[i].Extent, actual[i].LineMetrics.Extent, Comparer); + Assert.Equal(expected[i].StringIndex, actual[i].LineMetrics.StringIndex); + Assert.Equal(expected[i].GraphemeIndex, actual[i].LineMetrics.GraphemeIndex); + Assert.Equal(expected[i].GraphemeCount, actual[i].LineMetrics.GraphemeCount); + } + } +} diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs index 9f6ad813..cfb677da 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTestUtilities.cs @@ -3,13 +3,14 @@ using System.Runtime.CompilerServices; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.PixelFormats; + #if SUPPORTS_DRAWING using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tests.TestUtilities; -using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.Drawing.Text; -using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; #endif @@ -40,10 +41,8 @@ public static void TestLayout( bool includeGeometry = false, bool customDecorations = false, [CallerMemberName] string test = "", -#if SUPPORTS_DRAWING Action> beforeAction = null, Action> afterAction = null, -#endif params object[] properties) { #if SUPPORTS_DRAWING @@ -134,8 +133,15 @@ private static RichTextOptions FromTextOptions(TextOptions options, bool customD LineSpacing = options.LineSpacing, Origin = options.Origin, WrappingLength = options.WrappingLength, + MaxLines = options.MaxLines, WordBreaking = options.WordBreaking, + TextHyphenation = options.TextHyphenation, + CustomHyphen = options.CustomHyphen, + TextEllipsis = options.TextEllipsis, + CustomEllipsis = options.CustomEllipsis, TextDirection = options.TextDirection, + TextBidiMode = options.TextBidiMode, + TextInteractionMode = options.TextInteractionMode, TextAlignment = options.TextAlignment, TextJustification = options.TextJustification, HorizontalAlignment = options.HorizontalAlignment, diff --git a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs index 60fc9dc3..9d20745d 100644 --- a/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextLayoutTests.cs @@ -4,8 +4,15 @@ using System.Globalization; using System.Numerics; using SixLabors.Fonts.Rendering; +using SixLabors.Fonts.Tables.AdvancedTypographic; using SixLabors.Fonts.Tests.Fakes; using SixLabors.Fonts.Unicode; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.Drawing.Text; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; namespace SixLabors.Fonts.Tests; @@ -113,8 +120,7 @@ public void CanAlignText( VerticalAlignment = vertical }; - IReadOnlyList glyphsToRender = TextLayout.GenerateLayout(text.AsSpan(), options); - FontRectangle bound = TextMeasurer.GetBounds(glyphsToRender, options.Dpi); + FontRectangle bound = TextMeasurer.MeasureBounds(text.AsSpan(), options); Assert.Equal(310, bound.Width, 3F); Assert.Equal(40, bound.Height, 3F); @@ -188,8 +194,7 @@ public void CanAlignWithWrapping( TextAlignment = TextAlignment.End }; - IReadOnlyList glyphsToRender = TextLayout.GenerateLayout(text.AsSpan(), options); - FontRectangle bound = TextMeasurer.GetBounds(glyphsToRender, options.Dpi); + FontRectangle bound = TextMeasurer.MeasureBounds(text.AsSpan(), options); Assert.Equal(310, bound.Width, 3F); Assert.Equal(40, bound.Height, 3F); @@ -228,30 +233,33 @@ public void MeasureText(string text, float height, float width) } [Fact] - public void TryMeasureCharacterBounds() + public void GetGlyphMetrics() { const string text = "a b\nc"; - GlyphBounds[] expectedGlyphMetrics = + GlyphMetrics[] expectedGlyphMetrics = [ - new(new CodePoint('a'), new FontRectangle(10, 0, 10, 10), 0, 0), - new(new CodePoint(' '), new FontRectangle(40, 0, 30, 10), 1, 1), - new(new CodePoint('b'), new FontRectangle(70, 0, 10, 10), 2, 2), - new(new CodePoint('c'), new FontRectangle(10, 30, 10, 10), 3, 3), + new(new CodePoint('a'), FontRectangle.Empty, new FontRectangle(10, 0, 10, 10), FontRectangle.Empty, 0, 0), + new(new CodePoint(' '), FontRectangle.Empty, new FontRectangle(40, 0, 30, 10), FontRectangle.Empty, 1, 1), + new(new CodePoint('b'), FontRectangle.Empty, new FontRectangle(70, 0, 10, 10), FontRectangle.Empty, 2, 2), + new(new CodePoint('c'), FontRectangle.Empty, new FontRectangle(10, 30, 10, 10), FontRectangle.Empty, 4, 4), ]; + Font font = CreateFont(text); - Assert.True(TextMeasurer.TryMeasureCharacterBounds( + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics( text.AsSpan(), - new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor }, - out ReadOnlySpan bounds)); + new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor }).Span; + + // The hard break ends a non-empty line, so it is trimmed from visual + // glyph bounds. The following glyph still carries its original source + // string index. + Assert.Equal(expectedGlyphMetrics.Length, glyphs.Length); - // Newline should not be returned. - Assert.Equal(text.Length - 1, bounds.Length); - for (int i = 0; i < bounds.Length; i++) + for (int i = 0; i < expectedGlyphMetrics.Length; i++) { - GlyphBounds expected = expectedGlyphMetrics[i]; - GlyphBounds actual = bounds[i]; - Assert.Equal(expected.Codepoint, actual.Codepoint); + GlyphMetrics expected = expectedGlyphMetrics[i]; + GlyphMetrics actual = glyphs[i]; + Assert.Equal(expected.CodePoint, actual.CodePoint); // 4 dp as there is minor offset difference in the float values Assert.Equal(expected.Bounds.X, actual.Bounds.X, 4F); @@ -261,6 +269,31 @@ public void TryMeasureCharacterBounds() } } + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void GetGlyphMetrics_HardBreakBoundsAreNotNegative(LayoutMode layoutMode) + { + const string text = "\n"; + Font font = CreateFont(text); + + TextOptions options = new(font) + { + Dpi = font.FontMetrics.ScaleFactor, + LayoutMode = layoutMode + }; + + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; + Assert.Equal(1, glyphs.Length); + Assert.True(CodePoint.IsNewLine(glyphs[0].CodePoint)); + Assert.True(glyphs[0].Bounds.X >= 0); + Assert.True(glyphs[0].Bounds.Y >= 0); + } + [Theory] [InlineData("hello world", 10, 87.125F)] [InlineData("hello world hello world hello world", 11.438F, 279.13F)] @@ -479,160 +512,1546 @@ public void KeepAllAllowsBreaksAtWordSeparators() Font font = TestFonts.GetFont(TestFonts.OpenSansFile, 20); float wrappingLength = TextMeasurer.MeasureAdvance("hello", new TextOptions(font)).Width * .95F; - TextOptions options = new(font) + TextOptions options = new(font) + { + WrappingLength = wrappingLength, + WordBreaking = WordBreaking.KeepAll + }; + + Assert.Equal(2, TextMeasurer.CountLines(text, options)); + } + + [Fact] + public void StandardWordBreakingAllowsUrlBreakAfterNumericPathSegment() + { + const string text = "https://a/2024/05"; + const string expectedFirstLine = "https://a/2024/"; + Font font = CreateFont(text); + TextOptions noWrap = new(font); + float expectedWidth = TextMeasurer.MeasureAdvance(expectedFirstLine, noWrap).Width; + + TextOptions options = new(font) + { + WrappingLength = expectedWidth + 1.1F, + WordBreaking = WordBreaking.Standard + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + Assert.Equal(2, metrics.LineCount); + Assert.Equal(expectedFirstLine, GetSourceTextForLine(text, metrics, 0)); + Assert.Equal("05", GetSourceTextForLine(text, metrics, 1)); + Assert.Equal(expectedWidth, metrics.Advance.Width, Comparer); + } + + [Fact] + public void StandardWordBreakingDoesNotTreatNumericFractionAsUrl() + { + const string text = "1/2/3"; + Font font = CreateFont(text); + TextOptions noWrap = new(font); + float fullWidth = TextMeasurer.MeasureAdvance(text, noWrap).Width; + float wrappingWidth = TextMeasurer.MeasureAdvance("1/2/", noWrap).Width + .01F; + + TextOptions options = new(font) + { + WrappingLength = wrappingWidth, + WordBreaking = WordBreaking.Standard + }; + + FontRectangle size = TextMeasurer.MeasureAdvance(text, options); + + Assert.Equal(fullWidth, size.Width, Comparer); + } + + [Fact] + public void StandardWordBreakingKeepsNonUrlSolidusRunTogether() + { + const string text = "bbbbb/ccccc"; + Font font = CreateFont(text); + TextOptions noWrap = new(font); + float fullWidth = TextMeasurer.MeasureAdvance(text, noWrap).Width; + float wrappingWidth = TextMeasurer.MeasureAdvance("bbbbb/", noWrap).Width + 1.1F; + + TextOptions options = new(font) + { + WrappingLength = wrappingWidth, + WordBreaking = WordBreaking.Standard + }; + + FontRectangle size = TextMeasurer.MeasureAdvance(text, options); + + Assert.Equal(fullWidth, size.Width, Comparer); + } + + [Theory] + [InlineData("ab", 477, 1081, false)] // no kerning rules defined for lowercase ab so widths should stay the same + [InlineData("ab", 477, 1081, true)] + [InlineData("AB", 465, 1033, false)] // width changes between kerning enabled or not + [InlineData("AB", 465, 654, true)] + public void MeasureTextWithKerning(string text, float height, float width, bool applyKerning) + { + Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); + FontRectangle size = TextMeasurer.MeasureBounds( + text, + new TextOptions(new Font(font, 1)) + { + Dpi = font.FontMetrics.ScaleFactor, + KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, + }); + + Assert.Equal(height, size.Height, 4F); + Assert.Equal(width, size.Width, 4F); + } + + [Theory] + [InlineData("a", 100, 100, 125, 396)] + public void LayoutWithLocation(string text, float x, float y, float expectedX, float expectedY) + { + Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); + + GlyphRenderer glyphRenderer = new(); + TextRenderer renderer = new(glyphRenderer); + renderer.RenderText( + text, + new TextOptions(new Font(font, 1)) + { + Dpi = font.FontMetrics.ScaleFactor, + Origin = new Vector2(x, y) + }); + + Assert.Equal(expectedX, glyphRenderer.GlyphRects[0].Location.X, 2F); + Assert.Equal(expectedY, glyphRenderer.GlyphRects[0].Location.Y, 2F); + } + + // https://github.com/SixLabors/Fonts/issues/244 + [Fact] + public void MeasureTextLeadingFraction() + { + Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); + TextOptions textOptions = new(font); + FontRectangle measurement = TextMeasurer.MeasureBounds("/ This will fail", textOptions); + + Assert.NotEqual(FontRectangle.Empty, measurement); + } + + [Theory] + [InlineData("hello world", 1)] + [InlineData("hello world\nhello world", 2)] + [InlineData("hello world\nhello world\nhello world", 3)] + public void CountLines(string text, int usedLineMetrics) + { + Font font = CreateFont(text); + int count = TextMeasurer.CountLines(text, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor }); + + Assert.Equal(usedLineMetrics, count); + } + + [Fact] + public void CountLinesWithSpan() + { + Font font = CreateFont("hello\n!"); + + Span text = + [ + 'h', + 'e', + 'l', + 'l', + 'o', + '\n', + '!' + ]; + int count = TextMeasurer.CountLines(text, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor }); + + Assert.Equal(2, count); + } + + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void TextHyphenation_Custom_DrawsSelectedSoftHyphenMarker(LayoutMode layoutMode) + { + const string text = "extra\u00ADordinary next"; + Font font = TestFonts.GetFont(TestFonts.NotoSansRegular, 30); + Vector2 origin = new(24, 28); + + TextOptions measureOptions = new(font) + { + LayoutMode = layoutMode, + Origin = origin, + TextHyphenation = TextHyphenation.Custom, + CustomHyphen = new('*') + }; + + TextOptions hyphenMeasureOptions = new(font) + { + LayoutMode = layoutMode, + TextHyphenation = TextHyphenation.Custom, + CustomHyphen = new('*') + }; + + FontRectangle beforeSoftHyphen = TextMeasurer.MeasureAdvance("extra", measureOptions); + FontRectangle customHyphen = TextMeasurer.MeasureAdvance("*", hyphenMeasureOptions); + float softHyphenBreakAdvance = layoutMode.IsHorizontal() + ? beforeSoftHyphen.Width + customHyphen.Width + : beforeSoftHyphen.Height + customHyphen.Height; + + TextOptions options = new(font) + { + LayoutMode = layoutMode, + Origin = origin, + TextHyphenation = TextHyphenation.Custom, + CustomHyphen = new('*'), + WrappingLength = softHyphenBreakAdvance + 1F + }; + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + properties: layoutMode); + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // The wrap is based on the soft-hyphen break candidate: source prefix plus + // marker. BreakLines rejects exact equality as overflow, so the extra pixel + // lets this candidate fit without admitting the following grapheme. + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('*'))); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('-'))); + } + + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void TextHyphenation_Custom_DrawsBidiSoftHyphenMarker(LayoutMode layoutMode) + { + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); + + Font font = latin.CreateFont(30); + const string text = "Tall של\u00ADומשלוםשלוםשלום extra عرب next"; + Vector2 origin = new(24, 28); + + // Forced vertical layout intentionally does not enable generic horizontal features. + // Request cursive positioning explicitly so the Arabic run remains a useful bidi + // visual check in every layout mode. + Tag[] featureTags = layoutMode.IsVertical() + ? [KnownFeatureTags.CursivePositioning] + : []; + + TextOptions measureOptions = new(font) + { + FeatureTags = featureTags, + FallbackFontFamilies = [hebrew, arabic], + LayoutMode = layoutMode, + Origin = origin, + TextHyphenation = TextHyphenation.Custom, + CustomHyphen = new('*') + }; + + TextOptions hyphenMeasureOptions = new(font) + { + FeatureTags = featureTags, + FallbackFontFamilies = [hebrew, arabic], + LayoutMode = layoutMode, + TextHyphenation = TextHyphenation.Custom, + CustomHyphen = new('*') + }; + + FontRectangle beforeSoftHyphen = TextMeasurer.MeasureAdvance("Tall של", measureOptions); + FontRectangle customHyphen = TextMeasurer.MeasureAdvance("*", hyphenMeasureOptions); + float softHyphenBreakAdvance = layoutMode.IsHorizontal() + ? beforeSoftHyphen.Width + customHyphen.Width + : beforeSoftHyphen.Height + customHyphen.Height; + + TextOptions options = new(font) + { + FeatureTags = featureTags, + FallbackFontFamilies = [hebrew, arabic], + LayoutMode = layoutMode, + Origin = origin, + TextHyphenation = TextHyphenation.Custom, + CustomHyphen = new('*'), + WrappingLength = softHyphenBreakAdvance + 1F + }; + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + properties: layoutMode); + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // The line contains LTR Latin, a selected soft-hyphen break inside a long + // RTL Hebrew fallback word, and RTL Arabic after the break. The marker + // should still materialize exactly once after bidi reordering and fallback shaping. + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('*'))); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('-'))); + } + + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void TextHyphenation_Standard_DrawsFallbackSoftHyphenMarker(LayoutMode layoutMode) + { + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + + Font font = latin.CreateFont(30); + const string text = "Tall של\u00ADומשלוםשלוםשלום extra next"; + Vector2 origin = new(24, 28); + + TextOptions measureOptions = new(font) + { + FallbackFontFamilies = [hebrew], + LayoutMode = layoutMode, + Origin = origin, + TextHyphenation = TextHyphenation.Standard + }; + + TextOptions hyphenMeasureOptions = new(font) + { + FallbackFontFamilies = [hebrew], + LayoutMode = layoutMode, + TextHyphenation = TextHyphenation.Standard + }; + + FontRectangle beforeSoftHyphen = TextMeasurer.MeasureAdvance("Tall של", measureOptions); + FontRectangle hardHyphen = TextMeasurer.MeasureAdvance("\u2010", hyphenMeasureOptions); + float softHyphenBreakAdvance = layoutMode.IsHorizontal() + ? beforeSoftHyphen.Width + hardHyphen.Width + : beforeSoftHyphen.Height + hardHyphen.Height; + + TextOptions options = new(font) + { + FallbackFontFamilies = [hebrew], + LayoutMode = layoutMode, + Origin = origin, + TextHyphenation = TextHyphenation.Standard, + WrappingLength = softHyphenBreakAdvance + }; + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + properties: layoutMode); + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // The selected U+00AD break sits inside the Hebrew word, which is drawn + // through the fallback family. The standard marker must be visible at + // that fallback-run break location, not only in Latin text. + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2010))); + } + + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void TextHyphenation_None_DoesNotDrawFallbackSoftHyphenMarker(LayoutMode layoutMode) + { + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + + Font font = latin.CreateFont(30); + const string text = "Tall של\u00ADומשלוםשלוםשלום extra next"; + Vector2 origin = new(24, 28); + + TextOptions measureOptions = new(font) + { + FallbackFontFamilies = [hebrew], + LayoutMode = layoutMode, + Origin = origin, + TextHyphenation = TextHyphenation.None + }; + + FontRectangle firstGraphemeAfterSoftHyphen = TextMeasurer.MeasureAdvance("Tall שלו", measureOptions); + float firstGraphemeAdvance = layoutMode.IsHorizontal() + ? firstGraphemeAfterSoftHyphen.Width + : firstGraphemeAfterSoftHyphen.Height; + + TextOptions options = new(font) + { + FallbackFontFamilies = [hebrew], + LayoutMode = layoutMode, + Origin = origin, + TextHyphenation = TextHyphenation.None, + WrappingLength = firstGraphemeAdvance - 0.5F + }; + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + properties: layoutMode); + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // The source contains U+00AD inside the Hebrew fallback run. With hyphenation + // disabled it remains non-rendering source text; no visible hyphen marker + // should be emitted at that fallback-script location. + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2010))); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('-'))); + } + + [Theory] + [InlineData(TextEllipsis.Standard)] + [InlineData(TextEllipsis.Custom)] + [InlineData(TextEllipsis.None)] + public void TextEllipsis_DrawsMaxLinesMarker(TextEllipsis ellipsis) + { + Font font = CreateRenderingFont(34); + const string text = "one two three four five six"; + + TextOptions options = new(font) + { + Origin = new(24, 42), + WrappingLength = 210, + MaxLines = 1, + TextEllipsis = ellipsis, + CustomEllipsis = new('*'), + LayoutMode = LayoutMode.HorizontalTopBottom + }; + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + properties: ellipsis.ToString().ToLowerInvariant()); + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + // These assertions document the visible marker behavior shown by the + // reference images: standard uses U+2026, custom uses the configured + // marker, and none only clamps the visible line count. + if (ellipsis == TextEllipsis.Standard) + { + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2026))); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('*'))); + } + else if (ellipsis == TextEllipsis.Custom) + { + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2026))); + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('*'))); + } + else + { + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint(0x2026))); + Assert.Equal(0, CountGlyphs(metrics.GetGlyphMetrics().Span, new CodePoint('*'))); + } + } + + [Fact] + public void TextPlaceholder_SharesInsertionCodePointOffset() + { + const string text = "a\u0301b"; + Font font = CreateFont(text); + TextOptions options = new(font) + { + Dpi = font.FontMetrics.ScaleFactor, + TextRuns = + [ + new() + { + Start = 1, + End = 1, + Placeholder = new(40, 24, TextPlaceholderAlignment.Baseline, 18) + } + ] + }; + + ShapedText shapedText = TextLayout.ShapeText(text.AsSpan(), options); + LogicalTextLine logicalLine = TextLayout.ComposeLogicalLine(shapedText, text.AsSpan(), options); + + GlyphLayoutData placeholder = default; + GlyphLayoutData following = default; + for (int i = 0; i < logicalLine.TextLine.Count; i++) + { + GlyphLayoutData current = logicalLine.TextLine[i]; + if (current.CodePoint == CodePoint.ObjectReplacementChar) + { + placeholder = current; + continue; + } + + if (current.CodePoint == new CodePoint('b')) + { + following = current; + } + } + + // The placeholder is inserted after a grapheme made from two source + // codepoints. It must therefore share the codepoint and UTF-16 offset + // of the following source glyph instead of using the grapheme run index. + Assert.Equal(2, placeholder.CodePointIndex); + Assert.Equal(2, placeholder.StringIndex); + Assert.Equal(following.CodePointIndex, placeholder.CodePointIndex); + Assert.Equal(following.StringIndex, placeholder.StringIndex); + } + + [Fact] + public void TextPlaceholder_AddsInlineAdvanceWithoutConsumingSourceText() + { + const string text = "ab"; + Font font = CreateFont(text); + TextOptions baselineOptions = new(font) + { + Dpi = font.FontMetrics.ScaleFactor + }; + + TextOptions placeholderOptions = new(font) + { + Dpi = font.FontMetrics.ScaleFactor, + TextRuns = + [ + new() + { + Start = 1, + End = 1, + Placeholder = new(40, 24, TextPlaceholderAlignment.Baseline, 18) + } + ] + }; + + FontRectangle baseline = TextMeasurer.MeasureAdvance(text, baselineOptions); + FontRectangle withPlaceholder = TextMeasurer.MeasureAdvance(text, placeholderOptions); + GlyphMetrics[] glyphs = TextMeasurer.GetGlyphMetrics(text, placeholderOptions).ToArray(); + + // The placeholder contributes its own inline advance, but the source + // text still has only the two original graphemes around the inserted object. + Assert.Equal(baseline.Width + 40, withPlaceholder.Width, 0.1F); + Assert.Equal(1, CountGlyphs(glyphs, CodePoint.ObjectReplacementChar)); + Assert.Equal(1, CountGlyphs(glyphs, new CodePoint('a'))); + Assert.Equal(1, CountGlyphs(glyphs, new CodePoint('b'))); + } + + [Theory] + [InlineData(TextPlaceholderAlignment.Baseline)] + [InlineData(TextPlaceholderAlignment.AboveBaseline)] + [InlineData(TextPlaceholderAlignment.BelowBaseline)] + [InlineData(TextPlaceholderAlignment.Top)] + [InlineData(TextPlaceholderAlignment.Bottom)] + [InlineData(TextPlaceholderAlignment.Middle)] + public void TextPlaceholderAlignment_PositionsPlaceholderBounds(TextPlaceholderAlignment alignment) + { + const string text = "Alpha Omega"; + const float width = 56; + const float height = 30; + const float baselineOffset = 23; + Font font = CreateRenderingFont(34); + TextOptions surroundingOptions = new(font); + + TextOptions options = new(font) + { + TextRuns = + [ + new() + { + Start = 6, + End = 6, + Placeholder = new(width, height, alignment, baselineOffset) + } + ] + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + LineMetrics surroundingLine = TextMeasurer.Measure(text, surroundingOptions).LineMetrics[0]; + ReadOnlySpan glyphs = metrics.GetGlyphMetrics().Span; + FontRectangle placeholderBounds = FontRectangle.Empty; + for (int i = 0; i < glyphs.Length; i++) + { + if (glyphs[i].CodePoint == CodePoint.ObjectReplacementChar) + { + placeholderBounds = glyphs[i].Bounds; + break; + } + } + + float baseline = surroundingLine.Start.Y + surroundingLine.Baseline; + float lineTop = surroundingLine.Start.Y; + float lineBottom = surroundingLine.Start.Y + surroundingLine.LineHeight; + float expectedTop; + float expectedBottom; + + switch (alignment) + { + case TextPlaceholderAlignment.AboveBaseline: + expectedTop = baseline - height; + expectedBottom = baseline; + break; + + case TextPlaceholderAlignment.BelowBaseline: + expectedTop = baseline; + expectedBottom = baseline + height; + break; + + case TextPlaceholderAlignment.Top: + expectedTop = lineTop; + expectedBottom = expectedTop + height; + break; + + case TextPlaceholderAlignment.Bottom: + expectedBottom = lineBottom; + expectedTop = expectedBottom - height; + break; + + case TextPlaceholderAlignment.Middle: + float center = (lineTop + lineBottom) * .5F; + expectedTop = center - (height * .5F); + expectedBottom = center + (height * .5F); + break; + + default: + expectedTop = baseline - baselineOffset; + expectedBottom = baseline + height - baselineOffset; + break; + } + + // Each alignment changes only the placeholder's vertical placement. The + // inline width remains the caller-provided atomic object width. + Assert.Equal(width, placeholderBounds.Width, 0.1F); + Assert.Equal(height, placeholderBounds.Height, 0.1F); + Assert.Equal(expectedTop, placeholderBounds.Top, 0.1F); + Assert.Equal(expectedBottom, placeholderBounds.Bottom, 0.1F); + } + + [Fact] + public void TextPlaceholder_RunMustBeInsertionPoint() + { + const string text = "ab"; + Font font = CreateFont(text); + TextOptions options = new(font) + { + TextRuns = + [ + new() + { + Start = 1, + End = 2, + Placeholder = new(40, 24, TextPlaceholderAlignment.Baseline, 18) + } + ] + }; + + // Placeholder runs represent an inserted object at a source position. + // Covering real source graphemes would make the object consume text. + Assert.Throws(() => TextMeasurer.MeasureAdvance(text, options)); + } + + [Theory] + [InlineData(TextPlaceholderAlignment.Baseline)] + [InlineData(TextPlaceholderAlignment.AboveBaseline)] + [InlineData(TextPlaceholderAlignment.BelowBaseline)] + [InlineData(TextPlaceholderAlignment.Top)] + [InlineData(TextPlaceholderAlignment.Bottom)] + [InlineData(TextPlaceholderAlignment.Middle)] + public void TextPlaceholder_DrawsInlineReservedSpace(TextPlaceholderAlignment alignment) + { + const string text = "Alpha Omega\nAlpha Omega"; + Font font = CreateRenderingFont(34); + TextPlaceholder placeholder = new(56, 30, alignment, 23); + Vector2 origin = new(24, 58); + + TextOptions optionsBase = new(font) + { + Origin = origin, + }; + + TextOptions options = new(optionsBase) + { + TextRuns = + [ + new() + { + Start = 6, + End = 6, + Placeholder = placeholder + } + ] + }; + + IReadOnlyList glyphs = TextBuilder.GenerateGlyphs(text, options); + TextMetrics metrics = TextMeasurer.Measure(text, options); + + LineMetrics line = metrics.LineMetrics[0]; + LineMetrics lineBase = TextMeasurer.Measure(text, optionsBase).LineMetrics[0]; + + // Expected output: + // - Baseline aligns the placeholder's internal baseline offset to the red baseline. + // - AboveBaseline places the object immediately above the red baseline. + // - BelowBaseline places the object immediately below the red baseline. + // - Top, middle, and bottom align the object against the blue surrounding font line box. + // - The green placeholder-layout line box may grow when the object extends beyond that blue box. + ReadOnlySpan measuredGlyphs = metrics.GetGlyphMetrics().Span; + FontRectangle placeholderBounds = FontRectangle.Empty; + for (int i = 0; i < measuredGlyphs.Length; i++) + { + if (measuredGlyphs[i].CodePoint == CodePoint.ObjectReplacementChar) + { + placeholderBounds = measuredGlyphs[i].Bounds; + break; + } + } + + TextLayoutTestUtilities.TestImage( + 340, + 180, + image => image.Mutate(x => + { + RectangularPolygon lineBox = new( + 0, + line.Start.Y, + 340, + line.LineHeight); + + RectangularPolygon lineBoxBase = new( + 0, + lineBase.Start.Y, + 340, + lineBase.LineHeight); + + // Green is the actual line box for the layout that contains the + // placeholder. It shows whether the object caused this line to + // reserve more vertical space. + x.Fill(Color.Green.WithAlpha(.15F), lineBox); + + // Blue is the same text measured without the placeholder. This + // gives a stable reference for the surrounding font line box + // that top/middle/bottom alignment should use. + x.Fill(Color.LightBlue.WithAlpha(.95F), lineBoxBase); + + x.Draw(Color.Gray, 1, lineBox); + x.Fill(Color.Black, glyphs); + + // The black outline is the caller-owned inline object bounds + // returned by the public glyph-bounds API. + RectangularPolygon box = new( + placeholderBounds.X, + placeholderBounds.Y, + placeholderBounds.Width, + placeholderBounds.Height); + + x.Draw(Color.Black, 1, box); + + // Red is the baseline for the surrounding text without the + // placeholder. Baseline-relative modes should align to this. + float baseline = lineBase.Start.Y + lineBase.Baseline; + x.DrawLine(Color.Red, 1, new PointF(0, baseline), new PointF(340, baseline)); + }), + properties: alignment.ToString().ToLowerInvariant()); + + // The visual output shows the reserved object space; the measured data + // also exposes one object-replacement glyph at the insertion point. + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, CodePoint.ObjectReplacementChar)); + } + + [Theory] + [InlineData(TextPlaceholderAlignment.Baseline)] + [InlineData(TextPlaceholderAlignment.AboveBaseline)] + [InlineData(TextPlaceholderAlignment.BelowBaseline)] + [InlineData(TextPlaceholderAlignment.Top)] + [InlineData(TextPlaceholderAlignment.Bottom)] + [InlineData(TextPlaceholderAlignment.Middle)] + public void TextPlaceholder_DrawsOversizedInlineReservedSpace(TextPlaceholderAlignment alignment) + { + const string text = "Alpha Omega\nAlpha Omega"; + Font font = CreateRenderingFont(34); + TextPlaceholder placeholder = new(56, 82, alignment, 23); + Vector2 origin = new(24, 98); + + TextOptions optionsBase = new(font) + { + Origin = origin, + }; + + TextOptions options = new(optionsBase) + { + TextRuns = + [ + new() + { + Start = 6, + End = 6, + Placeholder = placeholder + } + ] + }; + + IReadOnlyList glyphs = TextBuilder.GenerateGlyphs(text, options); + TextMetrics metrics = TextMeasurer.Measure(text, options); + + LineMetrics line = metrics.LineMetrics[0]; + LineMetrics lineBase = TextMeasurer.Measure(text, optionsBase).LineMetrics[0]; + + // Expected output for oversized objects: + // - Top keeps the object top aligned to the blue surrounding line box and grows downward. + // - Bottom keeps the object bottom aligned to the blue surrounding line box and grows upward. + // - Middle centers the object on the blue surrounding line box and grows equally both ways. + // - The green line box must expand far enough that the second line does not overlap the object. + ReadOnlySpan measuredGlyphs = metrics.GetGlyphMetrics().Span; + FontRectangle placeholderBounds = FontRectangle.Empty; + for (int i = 0; i < measuredGlyphs.Length; i++) + { + if (measuredGlyphs[i].CodePoint == CodePoint.ObjectReplacementChar) + { + placeholderBounds = measuredGlyphs[i].Bounds; + break; + } + } + + TextLayoutTestUtilities.TestImage( + 340, + 260, + image => image.Mutate(x => + { + RectangularPolygon lineBox = new( + 0, + line.Start.Y, + 340, + line.LineHeight); + + RectangularPolygon lineBoxBase = new( + 0, + lineBase.Start.Y, + 340, + lineBase.LineHeight); + + // Green is the expanded line box for the placeholder layout. + // It should grow enough that the second line does not overlap + // the oversized inline object. + x.Fill(Color.Green.WithAlpha(.15F), lineBox); + + // Blue is the normal surrounding text line box. Top, middle, + // and bottom align against this box before any oversized object + // growth is applied to the actual line. + x.Fill(Color.LightBlue.WithAlpha(.95F), lineBoxBase); + + x.Draw(Color.Gray, 1, lineBox); + x.Fill(Color.Black, glyphs); + + // The black outline is intentionally taller than the normal + // text line so this visual test shows both alignment and line + // growth in the same output. + RectangularPolygon box = new( + placeholderBounds.X, + placeholderBounds.Y, + placeholderBounds.Width, + placeholderBounds.Height); + + x.Draw(Color.Black, 1, box); + + // Red is the baseline for the surrounding text without the + // placeholder; these modes should not align to it directly. + float baseline = lineBase.Start.Y + lineBase.Baseline; + x.DrawLine(Color.Red, 1, new PointF(0, baseline), new PointF(340, baseline)); + }), + properties: alignment.ToString().ToLowerInvariant()); + + // The oversized visual still represents one atomic inline object. + Assert.Equal(1, CountGlyphs(metrics.GetGlyphMetrics().Span, CodePoint.ObjectReplacementChar)); + } + + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void LineMetrics_StartAndExtent_DrawsLineBoxes(LayoutMode layoutMode) + { + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); + + Font font = latin.CreateFont(30); + Font largeFont = latin.CreateFont(46); + const string text = "Tall שלום عرب\nSmall مرحبا שלום"; + + // Forced vertical layout intentionally does not enable generic horizontal features. + // Request cursive positioning explicitly so this visual test still covers + // feature-driven Arabic positioning in that mode. + Tag[] featureTags = layoutMode.IsVertical() + ? [KnownFeatureTags.CursivePositioning] + : []; + + TextOptions options = new(font) + { + FeatureTags = featureTags, + FallbackFontFamilies = [hebrew, arabic], + Origin = new(24, 28), + LayoutMode = layoutMode, + LineSpacing = 1.25F, + TextRuns = + [ + new() { Start = 0, End = 4, Font = largeFont }, + new() { Start = 20, End = 25, Font = largeFont } + ] + }; + + LineMetrics[] metrics = TextMeasurer.GetLineMetrics(text, options).ToArray(); + + void DrawLineBoxes(Image image) + { + for (int i = 0; i < metrics.Length; i++) + { + LineMetrics m = metrics[i]; + Color startColor = i == 0 + ? Color.Lime + : Color.Cyan; + + Color endColor = i == 0 + ? Color.Magenta + : Color.Yellow; + + PointF gradientStart = new(m.Start.X, m.Start.Y); + PointF gradientEnd = new(m.Start.X + m.Extent.X, m.Start.Y + m.Extent.Y); + + LinearGradientBrush fill = new( + gradientStart, + gradientEnd, + GradientRepetitionMode.None, + new ColorStop(0, startColor), + new ColorStop(1, endColor)); + + RectangularPolygon box = new(m.Start.X, m.Start.Y, m.Extent.X, m.Extent.Y); + + image.Mutate(x => + { + x.Fill(fill, box); + x.Draw(Color.Black, 2, box); + }); + } + } + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + beforeAction: DrawLineBoxes, + properties: layoutMode); + } + + [Theory] + [InlineData(LayoutMode.HorizontalTopBottom)] + [InlineData(LayoutMode.HorizontalBottomTop)] + [InlineData(LayoutMode.VerticalLeftRight)] + [InlineData(LayoutMode.VerticalRightLeft)] + [InlineData(LayoutMode.VerticalMixedLeftRight)] + [InlineData(LayoutMode.VerticalMixedRightLeft)] + public void GraphemeMetrics_GetSelectionBounds_DrawsGraphemeSelections(LayoutMode layoutMode) + { + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); + + Font font = latin.CreateFont(30); + Font largeFont = latin.CreateFont(46); + const string text = "Tall שלום عرب\nSmall مرحبا שלום"; + + TextOptions options = new(font) + { + FallbackFontFamilies = [hebrew, arabic], + Origin = new(24, 28), + LayoutMode = layoutMode, + LineSpacing = 1.25F, + TextRuns = + [ + new() { Start = 0, End = 4, Font = largeFont }, + new() { Start = 20, End = 25, Font = largeFont } + ] + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + void DrawSelections(Image image) + { + ReadOnlySpan graphemes = metrics.GraphemeMetrics; + for (int i = 0; i < graphemes.Length; i++) + { + GraphemeMetrics grapheme = graphemes[i]; + ReadOnlySpan selection = metrics.GetSelectionBounds(grapheme).Span; + if (selection.IsEmpty) + { + continue; + } + + FontRectangle bounds = selection[0]; + PointF gradientStart = new(bounds.Left, bounds.Top); + PointF gradientEnd = layoutMode.IsHorizontal() + ? new(bounds.Right, bounds.Top) + : new(bounds.Left, bounds.Bottom); + + // Vary the gradient by visual grapheme order so bidi reordering is visible. + Color startColor = (i & 1) == 0 + ? Color.Lime + : Color.Cyan; + + Color endColor = (i & 1) == 0 + ? Color.Magenta + : Color.Yellow; + + LinearGradientBrush fill = new( + gradientStart, + gradientEnd, + GradientRepetitionMode.None, + new ColorStop(0, startColor), + new ColorStop(1, endColor)); + + RectangularPolygon box = new(bounds.X, bounds.Y, bounds.Width, bounds.Height); + + image.Mutate(x => + { + x.Fill(fill, box); + x.Draw(Color.Black, 1, box); + }); + } + } + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + beforeAction: DrawSelections, + properties: layoutMode); + } + + [Fact] + public void GraphemeMetrics_GetSelectionBounds_DrawsSelectionWithBlankLine() + { + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); + + Font font = latin.CreateFont(30); + Font largeFont = latin.CreateFont(46); + const string text = "Tall عرب שלום\n\nSmall مرحبا שלום"; + + TextOptions options = new(font) + { + FallbackFontFamilies = [hebrew, arabic], + Origin = new(24, 28), + LayoutMode = LayoutMode.HorizontalTopBottom, + LineSpacing = 1.25F, + TextRuns = + [ + new() { Start = 0, End = 4, Font = largeFont }, + new() { Start = 15, End = 20, Font = largeFont } + ] + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + void DrawSelection(Image image) + { + CaretPosition anchor = metrics.GetCaret(CaretPlacement.Start); + CaretPosition focus = metrics.MoveCaret(anchor, CaretMovement.TextEnd); + ReadOnlySpan selection = metrics.GetSelectionBounds(anchor, focus).Span; + + // The first hard break ends a measuring text line and should not paint + // its own box. The second hard break owns the empty line between text + // lines, so full-text selection should include a visible blank-line box. + for (int i = 0; i < selection.Length; i++) + { + FontRectangle bounds = selection[i]; + RectangularPolygon box = new(bounds.X, bounds.Y, bounds.Width, bounds.Height); + + image.Mutate(x => x.Fill(Color.LightBlue, box)); + } + } + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + beforeAction: DrawSelection); + } + + [Fact] + public void GraphemeMetrics_GetSelectionBounds_DrawsBidiDragSelection() + { + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); + + Font font = latin.CreateFont(30); + Font largeFont = latin.CreateFont(46); + const string text = "Tall שלום عرب"; + + TextOptions options = new(font) + { + FallbackFontFamilies = [hebrew, arabic], + Origin = new(24, 28), + LayoutMode = LayoutMode.HorizontalTopBottom, + TextRuns = + [ + new() { Start = 0, End = 4, Font = largeFont } + ] + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + ReadOnlySpan graphemes = metrics.GraphemeMetrics; + GraphemeMetrics first = default; + GraphemeMetrics finalHebrew = default; + for (int i = 0; i < graphemes.Length; i++) + { + if (graphemes[i].GraphemeIndex == 0) + { + first = graphemes[i]; + } + + if (graphemes[i].GraphemeIndex == 8) + { + finalHebrew = graphemes[i]; + } + } + + // The text source is "Tall " then Hebrew then Arabic. In LTR paragraph + // layout the RTL run is painted with Arabic before Hebrew, so dragging + // left-to-right from "T" to the visual left side of the final Hebrew + // grapheme should select "Tall " and the Hebrew word while leaving the + // visually intervening Arabic word unselected. + Vector2 anchorPoint = new(first.Advance.Left, FontRectangle.Center(first.Advance).Y); + Vector2 focusPoint = new( + finalHebrew.Advance.Left + (finalHebrew.Advance.Width * 0.25F), + FontRectangle.Center(finalHebrew.Advance).Y); + + TextHit anchor = metrics.HitTest(anchorPoint); + TextHit focus = metrics.HitTest(focusPoint); + + void DrawSelection(Image image) { - WrappingLength = wrappingLength, - WordBreaking = WordBreaking.KeepAll - }; + ReadOnlySpan selection = metrics.GetSelectionBounds(anchor, focus).Span; + for (int i = 0; i < selection.Length; i++) + { + FontRectangle bounds = selection[i]; + RectangularPolygon box = new(bounds.X, bounds.Y, bounds.Width, bounds.Height); - Assert.Equal(2, TextMeasurer.CountLines(text, options)); + image.Mutate(x => x.Fill(Color.LightBlue, box)); + } + } + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + beforeAction: DrawSelection); } - [Fact] - public void StandardWordBreakingAllowsUrlBreakAfterNumericPathSegment() + [Theory] + [InlineData(TextDirection.LeftToRight, TextBidiMode.Normal)] + [InlineData(TextDirection.LeftToRight, TextBidiMode.Override)] + [InlineData(TextDirection.RightToLeft, TextBidiMode.Normal)] + [InlineData(TextDirection.RightToLeft, TextBidiMode.Override)] + public void TextBidiMode_DrawsMixedBidiLayout(TextDirection direction, TextBidiMode mode) { - const string text = "https://a/2024/05"; - const string expectedFirstLine = "https://a/2024/"; - Font font = CreateFont(text); - TextOptions noWrap = new(font); - float expectedWidth = TextMeasurer.MeasureAdvance(expectedFirstLine, noWrap).Width; + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); + + Font font = latin.CreateFont(40); + const string text = "abc שלום عرب def"; TextOptions options = new(font) { - WrappingLength = expectedWidth + 1.1F, - WordBreaking = WordBreaking.Standard + FallbackFontFamilies = [hebrew, arabic], + Origin = new(24, 52), + WrappingLength = 430, + TextDirection = direction, + TextBidiMode = mode, + LayoutMode = LayoutMode.HorizontalTopBottom }; - IReadOnlyList> lines = CollectLines(TextLayout.GenerateLayout(text.AsSpan(), options)); - FontRectangle size = TextMeasurer.MeasureAdvance(text, options); - - Assert.Equal(2, lines.Count); - Assert.Equal(expectedFirstLine, GetSourceTextForLine(text, lines[0])); - Assert.Equal("05", GetSourceTextForLine(text, lines[1])); - Assert.Equal(expectedWidth, size.Width, Comparer); + // Mixed-script text makes the distinction visible: Normal preserves each + // script's bidi class inside the paragraph direction, while Override + // forces every real text character through the paragraph direction. + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + properties: $"{(direction == TextDirection.RightToLeft ? "rtl" : "ltr")}-{mode.ToString().ToLowerInvariant()}"); } - [Fact] - public void StandardWordBreakingDoesNotTreatNumericFractionAsUrl() + [Theory] + [InlineData(TextDirection.LeftToRight)] + [InlineData(TextDirection.RightToLeft)] + public void CaretPosition_DrawsStartAndEndCarets(TextDirection direction) { - const string text = "1/2/3"; - Font font = CreateFont(text); - TextOptions noWrap = new(font); - float fullWidth = TextMeasurer.MeasureAdvance(text, noWrap).Width; - float wrappingWidth = TextMeasurer.MeasureAdvance("1/2/", noWrap).Width + .01F; + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); + + Font font = latin.CreateFont(30); + Font largeFont = latin.CreateFont(46); + const string text = "Tall שלום عرب\nSmall مرحبا שלום"; TextOptions options = new(font) { - WrappingLength = wrappingWidth, - WordBreaking = WordBreaking.Standard + FallbackFontFamilies = [hebrew, arabic], + Origin = new(24, 28), + TextDirection = direction, + LayoutMode = LayoutMode.HorizontalTopBottom, + LineSpacing = 1.25F, + TextRuns = + [ + new() { Start = 0, End = 4, Font = largeFont }, + new() { Start = 20, End = 25, Font = largeFont } + ] }; - FontRectangle size = TextMeasurer.MeasureAdvance(text, options); + TextMetrics metrics = TextMeasurer.Measure(text, options); - Assert.Equal(fullWidth, size.Width, Comparer); + void DrawCarets(Image image) + { + CaretPosition start = metrics.GetCaret(CaretPlacement.Start); + CaretPosition end = metrics.GetCaret(CaretPlacement.End); + + image.Mutate(x => + { + // Solid carets make absolute start/end placement easy to compare + // between LTR and RTL paragraph directions. + DrawCaret(x, start, Color.Lime, 3, dashed: false); + DrawCaret(x, end, Color.Magenta, 3, dashed: false); + }); + } + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + afterAction: DrawCarets, + properties: direction == TextDirection.RightToLeft ? "rtl" : "ltr"); } - [Fact] - public void StandardWordBreakingKeepsNonUrlSolidusRunTogether() + [Theory] + [InlineData(TextDirection.LeftToRight)] + [InlineData(TextDirection.RightToLeft)] + public void CaretPosition_DrawsMovedCarets(TextDirection direction) { - const string text = "bbbbb/ccccc"; - Font font = CreateFont(text); - TextOptions noWrap = new(font); - float fullWidth = TextMeasurer.MeasureAdvance(text, noWrap).Width; - float wrappingWidth = TextMeasurer.MeasureAdvance("bbbbb/", noWrap).Width + 1.1F; + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); + + Font font = latin.CreateFont(34); + const string text = "abc שלום عرب def\n123 אבג xyz"; TextOptions options = new(font) { - WrappingLength = wrappingWidth, - WordBreaking = WordBreaking.Standard + FallbackFontFamilies = [hebrew, arabic], + Origin = new(24, 34), + TextDirection = direction, + LayoutMode = LayoutMode.HorizontalTopBottom, + LineSpacing = 1.25F }; - FontRectangle size = TextMeasurer.MeasureAdvance(text, options); + TextMetrics metrics = TextMeasurer.Measure(text, options); - Assert.Equal(fullWidth, size.Width, Comparer); - } + void DrawCarets(Image image) + { + CaretPosition textStart = metrics.GetCaret(CaretPlacement.Start); + CaretPosition textEnd = metrics.GetCaret(CaretPlacement.End); + ReadOnlySpan graphemes = metrics.GraphemeMetrics; + int hebrewLamedStringIndex = text.IndexOf('ל'); + int arabicBehStringIndex = text.IndexOf('ب'); + int secondLineHebrewBetStringIndex = text.IndexOf('ב'); + GraphemeMetrics hebrewLamed = default; + GraphemeMetrics arabicBeh = default; + GraphemeMetrics secondLineHebrewBet = default; - [Theory] - [InlineData("ab", 477, 1081, false)] // no kerning rules defined for lowercase ab so widths should stay the same - [InlineData("ab", 477, 1081, true)] - [InlineData("AB", 465, 1033, false)] // width changes between kerning enabled or not - [InlineData("AB", 465, 654, true)] - public void MeasureTextWithKerning(string text, float height, float width, bool applyKerning) - { - Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); - FontRectangle size = TextMeasurer.MeasureBounds( - text, - new TextOptions(new Font(font, 1)) + for (int i = 0; i < graphemes.Length; i++) { - Dpi = font.FontMetrics.ScaleFactor, - KerningMode = applyKerning ? KerningMode.Standard : KerningMode.None, - }); + GraphemeMetrics grapheme = graphemes[i]; + if (grapheme.StringIndex == hebrewLamedStringIndex) + { + hebrewLamed = grapheme; + } + else if (grapheme.StringIndex == arabicBehStringIndex) + { + arabicBeh = grapheme; + } + else if (grapheme.StringIndex == secondLineHebrewBetStringIndex) + { + secondLineHebrewBet = grapheme; + } + } - Assert.Equal(height, size.Height, 4F); - Assert.Equal(width, size.Width, 4F); - } + // Previous/next movement is source-order navigation. In mixed bidi text the + // visual jump can cross runs, so the variable names include the anchor caret. + CaretPosition hebrewRunCaret = metrics.GetCaretPosition(metrics.HitTest(FontRectangle.Center(hebrewLamed.Advance))); + CaretPosition arabicRunCaret = metrics.GetCaretPosition(metrics.HitTest(FontRectangle.Center(arabicBeh.Advance))); + CaretPosition secondLineHebrewRunCaret = metrics.GetCaretPosition(metrics.HitTest(FontRectangle.Center(secondLineHebrewBet.Advance))); + CaretPosition nextFromHebrewRun = metrics.MoveCaret(hebrewRunCaret, CaretMovement.Next); + CaretPosition nextWordFromHebrewRun = metrics.MoveCaret(hebrewRunCaret, CaretMovement.NextWord); + CaretPosition previousFromArabicRun = metrics.MoveCaret(arabicRunCaret, CaretMovement.Previous); + CaretPosition previousWordFromArabicRun = metrics.MoveCaret(arabicRunCaret, CaretMovement.PreviousWord); + + // Line movement keeps the original horizontal preference, then line start/end + // resolve against the paragraph direction on the destination visual line. + CaretPosition lineStartFromHebrewRun = metrics.MoveCaret(secondLineHebrewRunCaret, CaretMovement.LineStart); + CaretPosition lineEndFromHebrewRun = metrics.MoveCaret(secondLineHebrewRunCaret, CaretMovement.LineEnd); + + image.Mutate(x => + { + // Blue starts from a hit on ל in שלום and moves to the caret after ו. + DrawCaret(x, nextFromHebrewRun, Color.Blue, 3, dashed: true); - [Theory] - [InlineData("a", 100, 100, 125, 396)] - public void LayoutWithLocation(string text, float x, float y, float expectedX, float expectedY) - { - Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); + // Cyan starts from a hit on ל in שלום and moves to the word boundary after ם. + DrawCaret(x, nextWordFromHebrewRun, Color.Cyan, 3, dashed: true); - GlyphRenderer glyphRenderer = new(); - TextRenderer renderer = new(glyphRenderer); - renderer.RenderText( - text, - new TextOptions(new Font(font, 1)) - { - Dpi = font.FontMetrics.ScaleFactor, - Origin = new Vector2(x, y) + // Red starts from a hit on ب in عرب and moves one source-order grapheme toward ر. + DrawCaret(x, previousFromArabicRun, Color.Red, 3, dashed: true); + + // Purple starts from a hit on ب in عرب and moves to the word boundary before ع. + DrawCaret(x, previousWordFromArabicRun, Color.Purple, 3, dashed: true); + + // Lime starts from a hit on ב in אבג and moves to the second-line start. + DrawCaret(x, lineStartFromHebrewRun, Color.Lime, 2, dashed: true); + + // Magenta starts from a hit on ב in אבג and moves to the second-line end. + DrawCaret(x, lineEndFromHebrewRun, Color.Magenta, 2, dashed: true); }); + } - Assert.Equal(expectedX, glyphRenderer.GlyphRects[0].Location.X, 2F); - Assert.Equal(expectedY, glyphRenderer.GlyphRects[0].Location.Y, 2F); + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + afterAction: DrawCarets, + properties: direction == TextDirection.RightToLeft ? "rtl" : "ltr"); } - // https://github.com/SixLabors/Fonts/issues/244 [Fact] - public void MeasureTextLeadingFraction() + public void LineLayoutEnumerator_DrawsManualFlowAroundCircle() { - Font font = TestFonts.GetFont(TestFonts.SimpleFontFile, 12); - TextOptions textOptions = new(font); - FontRectangle measurement = TextMeasurer.MeasureSize("/ This will fail", textOptions); + Font font = CreateRenderingFont(22); + const string text = + "Text can flow around arbitrary shapes when each line is measured independently. " + + "The caller chooses the available width for the next row, places the returned line, " + + "then asks for another line using the next open slot. This mirrors the Pretext-style " + + "manual layout demos without reshaping the paragraph for every row."; - Assert.NotEqual(FontRectangle.Empty, measurement); - } + TextOptions options = new(font) + { + Origin = Vector2.Zero, + WrappingLength = -1, + LineSpacing = 1.15F + }; - [Theory] - [InlineData("hello world", 1)] - [InlineData("hello world\nhello world", 2)] - [InlineData("hello world\nhello world\nhello world", 3)] - public void CountLines(string text, int usedLines) - { - Font font = CreateFont(text); - int count = TextMeasurer.CountLines(text, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor }); + TextBlock block = new(text, options); + LineLayoutEnumerator enumerator = block.EnumerateLineLayouts(); + + // The test owns all page geometry. TextBlock owns the expensive shaping + // and Unicode layout preparation, while the caller chooses where each + // successive line is allowed to fit. + const float pageLeft = 28; + const float pageTop = 28; + const float pageRight = 592; + const float pageBottom = 334; + const float circleX = 340; + const float circleY = 174; + const float circleRadius = 74; + const float circlePadding = 14; + const float minSlotWidth = 112; + float y = pageTop; + bool hasMoreText = true; + + TextLayoutTestUtilities.TestImage( + 620, + 360, + image => image.Mutate(x => + { + x.Fill(Color.White); + x.Draw(Color.DarkSlateGray, 1, new RectangularPolygon(pageLeft, pageTop, pageRight - pageLeft, pageBottom - pageTop)); + x.Fill(Color.SteelBlue.WithAlpha(.16F), new EllipsePolygon(circleX, circleY, circleRadius, circleRadius)); + x.Draw(Color.SteelBlue, 2, new EllipsePolygon(circleX, circleY, circleRadius, circleRadius)); + + // A horizontal band can be split by the obstacle into at most + // two usable slots. Keep these buffers outside the row loop so + // the test does not stackalloc on every line. + Span slotLefts = stackalloc float[2]; + Span slotRights = stackalloc float[2]; + + while (hasMoreText && y < pageBottom) + { + float bandTop = y; + float bandBottom = y + 30; + float blockedLeft = float.NaN; + float blockedRight = float.NaN; + + // The circle is converted into a blocked horizontal interval for + // the current line band. The remaining intervals become the + // widths passed to the line enumerator, so the text engine never + // needs to understand circles, columns, or obstacle geometry. + if (bandTop < circleY + circleRadius && bandBottom > circleY - circleRadius) + { + float closestY = Math.Clamp(circleY, bandTop, bandBottom); + float dy = Math.Abs(closestY - circleY); + float dx = MathF.Sqrt((circleRadius * circleRadius) - (dy * dy)); + blockedLeft = circleX - dx - circlePadding; + blockedRight = circleX + dx + circlePadding; + } + + int slotCount = 0; + if (float.IsNaN(blockedLeft)) + { + // Rows outside the circle receive one full-width slot. + slotLefts[slotCount] = pageLeft; + slotRights[slotCount++] = pageRight; + } + else + { + // Rows crossing the circle receive the left and right + // slots only when there is enough room for useful text. + if (blockedLeft - pageLeft >= minSlotWidth) + { + slotLefts[slotCount] = pageLeft; + slotRights[slotCount++] = blockedLeft; + } + + if (pageRight - blockedRight >= minSlotWidth) + { + slotLefts[slotCount] = blockedRight; + slotRights[slotCount++] = pageRight; + } + } - Assert.Equal(usedLines, count); + float rowHeight = 30; + for (int i = 0; i < slotCount && hasMoreText; i++) + { + float slotLeft = slotLefts[i]; + float slotRight = slotRights[i]; + float slotWidth = slotRight - slotLeft; + + // This is the API behavior under test: each MoveNext call + // supplies the width for exactly one produced line. The + // next call can use a completely different width. + hasMoreText = enumerator.MoveNext(slotWidth); + if (!hasMoreText) + { + break; + } + + LineLayout line = enumerator.Current; + ReadOnlySpan graphemes = line.GraphemeMetrics; + int stringStart = graphemes[0].StringIndex; + int stringEnd = stringStart; + + // Bidi reordering means visual order is not guaranteed to + // match source order. The test draws the original source + // slice for this line, so it derives the slice from the + // grapheme source indices rather than array position alone. + for (int j = 0; j < graphemes.Length; j++) + { + stringStart = Math.Min(stringStart, graphemes[j].StringIndex); + stringEnd = Math.Max(stringEnd, graphemes[j].StringIndex + 1); + } + + // The blue slot boxes are the caller-owned placement regions. + // Text is drawn from the line's source mapping at the slot origin, + // showing that one prepared block can feed arbitrary row widths + // without re-preparing the original paragraph. + x.Fill(Color.SteelBlue.WithAlpha(.28F), new RectangularPolygon(slotLeft, y, slotWidth, line.LineMetrics.LineHeight)); + x.DrawText( + new RichTextOptions(font) + { + Origin = new(slotLeft, y), + WrappingLength = -1, + LineSpacing = options.LineSpacing + }, + text[stringStart..stringEnd], + Color.Black); + + rowHeight = Math.Max(rowHeight, line.LineMetrics.LineHeight); + } + + y += rowHeight; + } + })); } [Fact] - public void CountLinesWithSpan() + public void WordMetrics_GetSelectionBounds_DrawsWordSelections() { - Font font = CreateFont("hello\n!"); + FontCollection fontCollection = new(); + FontFamily latin = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansRegular); + FontFamily hebrew = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoSansHebrewRegular); + FontFamily arabic = TestFonts.GetFontFamily(fontCollection, TestFonts.NotoNaskhArabicRegular); - Span text = - [ - 'h', - 'e', - 'l', - 'l', - 'o', - '\n', - '!' - ]; - int count = TextMeasurer.CountLines(text, new TextOptions(font) { Dpi = font.FontMetrics.ScaleFactor }); + Font font = latin.CreateFont(30); + Font largeFont = latin.CreateFont(46); + const string text = "can't stop שלום عرب"; - Assert.Equal(2, count); + TextOptions options = new(font) + { + FallbackFontFamilies = [hebrew, arabic], + Origin = new(24, 28), + LayoutMode = LayoutMode.HorizontalTopBottom, + TextRuns = + [ + new() { Start = 0, End = 5, Font = largeFont }, + new() { Start = 11, End = 15, Font = largeFont } + ] + }; + + TextMetrics metrics = TextMeasurer.Measure(text, options); + + void DrawSelections(Image image) + { + foreach (WordMetrics word in metrics.WordMetrics) + { + ReadOnlySpan selection = metrics.GetSelectionBounds(word).Span; + + // UAX #29 word segments include separators. Drawing every returned + // range with an outline makes the apostrophe in "can't" and the + // intervening spaces visible in the rendered output. + for (int i = 0; i < selection.Length; i++) + { + FontRectangle bounds = selection[i]; + RectangularPolygon box = new(bounds.X, bounds.Y, bounds.Width, bounds.Height); + + image.Mutate(x => + { + x.Fill(Color.LightBlue, box); + x.Draw(Color.Black, 1, box); + }); + } + } + } + + TextLayoutTestUtilities.TestLayout( + text, + options, + includeGeometry: false, + beforeAction: DrawSelections); } [Theory] @@ -640,7 +2059,7 @@ public void CountLinesWithSpan() [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 50, 4)] [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 100, 3)] [InlineData("This is a long and Honorificabilitudinitatibus califragilisticexpialidocious", 200, 3)] - public void CountLinesWrappingLength(string text, int wrappingLength, int usedLines) + public void CountLinesWrappingLength(string text, int wrappingLength, int usedLineMetrics) { Font font = CreateRenderingFont(); TextOptions options = new(font) @@ -648,10 +2067,10 @@ public void CountLinesWrappingLength(string text, int wrappingLength, int usedLi WrappingLength = wrappingLength }; - TextLayoutTestUtilities.TestLayout(text, options, properties: usedLines); + TextLayoutTestUtilities.TestLayout(text, options, properties: usedLineMetrics); int count = TextMeasurer.CountLines(text, options); - Assert.Equal(usedLines, count); + Assert.Equal(usedLineMetrics, count); } [Fact] @@ -767,34 +2186,28 @@ public void TextJustification_InterCharacter_Horizontal(TextDirection direction) TextJustification = TextJustification.InterCharacter }; - // Collect the first line so we can compare it to the target wrapping length. - IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); - IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); - TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan advances); + TextMetrics justifiedMetrics = TextMeasurer.Measure(text, options); - TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, options.TextJustification }); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { rtl = direction == TextDirection.RightToLeft }); - Assert.Equal(wrappingLength, advances.ToArray().Sum(x => x.Bounds.Width), 4F); + Assert.Equal(wrappingLength, justifiedMetrics.LineMetrics[0].Extent.X, 4F); - // Now compare character widths. options.TextJustification = TextJustification.None; - IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); - IReadOnlyList line = CollectFirstLine(glyphs); - TextMeasurer.TryGetCharacterAdvances(line, options.Dpi, out ReadOnlySpan characterBounds); + TextMetrics unJustifiedMetrics = TextMeasurer.Measure(text, options); - // All but the last justified character advance should be greater than the - // corresponding character advance. - for (int i = 0; i < characterBounds.Length; i++) + Assert.Equal(unJustifiedMetrics.GetGlyphMetrics().Length, justifiedMetrics.GetGlyphMetrics().Length); + + bool foundWidenedCharacter = false; + for (int i = 0; i < justifiedMetrics.GetGlyphMetrics().Length; i++) { - if (i == characterBounds.Length - 1) - { - Assert.Equal(advances[i].Bounds.Width, characterBounds[i].Bounds.Width); - } - else - { - Assert.True(advances[i].Bounds.Width > characterBounds[i].Bounds.Width); - } + float justifiedWidth = justifiedMetrics.GetGlyphMetrics().Span[i].Advance.Width; + float unJustifiedWidth = unJustifiedMetrics.GetGlyphMetrics().Span[i].Advance.Width; + + Assert.True(justifiedWidth >= unJustifiedWidth); + foundWidenedCharacter |= justifiedWidth > unJustifiedWidth; } + + Assert.True(foundWidenedCharacter); } [Theory] @@ -802,7 +2215,7 @@ public void TextJustification_InterCharacter_Horizontal(TextDirection direction) [InlineData(TextDirection.LeftToRight, TextJustification.InterWord)] [InlineData(TextDirection.RightToLeft, TextJustification.InterCharacter)] [InlineData(TextDirection.RightToLeft, TextJustification.InterWord)] - public void TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphFinalLines(TextDirection direction, TextJustification justification) + public void TextJustification_MultiParagraph_Horizontal_SkipsFinalLines(TextDirection direction, TextJustification justification) { const string paragraph = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; string text = $"{paragraph}\n{paragraph}"; @@ -816,64 +2229,36 @@ public void TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphF TextJustification = justification }; - TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, justification }); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { rtl = direction == TextDirection.RightToLeft, mode = justification }); - // Compare justified and non-justified layouts line-by-line. - // This lets us prove that some wrapped lines are still stretched, while - // paragraph-final lines remain unchanged even when they are not the last - // line of the overall text box. - IReadOnlyList> justifiedLines = CollectLines(TextLayout.GenerateLayout(text.AsSpan(), options)); + ReadOnlySpan justifiedLineMetrics = TextMeasurer.GetLineMetrics(text, options).Span; options.TextJustification = TextJustification.None; - IReadOnlyList> unJustifiedLines = CollectLines(TextLayout.GenerateLayout(text.AsSpan(), options)); + ReadOnlySpan unJustifiedLineMetrics = TextMeasurer.GetLineMetrics(text, options).Span; - Assert.Equal(unJustifiedLines.Count, justifiedLines.Count); + Assert.Equal(unJustifiedLineMetrics.Length, justifiedLineMetrics.Length); bool foundUnchangedNonLastLine = false; bool foundJustifiedNonParagraphLine = false; - for (int i = 0; i < justifiedLines.Count; i++) + for (int i = 0; i < justifiedLineMetrics.Length; i++) { - TextMeasurer.TryGetCharacterAdvances(justifiedLines[i], options.Dpi, out ReadOnlySpan justifiedCharacterBounds); - TextMeasurer.TryGetCharacterAdvances(unJustifiedLines[i], options.Dpi, out ReadOnlySpan unJustifiedCharacterBounds); - - GlyphBounds[] justified = justifiedCharacterBounds.ToArray(); - GlyphBounds[] unJustified = unJustifiedCharacterBounds.ToArray(); - - bool isLastLine = i == justifiedLines.Count - 1; - bool linesMatch = justified.Length == unJustified.Length; - - // A paragraph-final line should be byte-for-byte equivalent in terms of - // measured per-character advances, so we first test whether every glyph - // advance is unchanged relative to the non-justified layout. - if (linesMatch) - { - for (int j = 0; j < justified.Length; j++) - { - if (justified[j].Bounds.Width != unJustified[j].Bounds.Width) - { - linesMatch = false; - break; - } - } - } + bool isLastLine = i == justifiedLineMetrics.Length - 1; + bool linesMatch = MathF.Abs(justifiedLineMetrics[i].Extent.X - unJustifiedLineMetrics[i].Extent.X) <= .01F; if (isLastLine) { // The trailing line in the text box must never be justified. - Assert.True(linesMatch); + Assert.Equal(unJustifiedLineMetrics[i].Extent.X, justifiedLineMetrics[i].Extent.X, 4F); } else { - float justifiedWidth = justified.Sum(x => x.Bounds.Width); - float unJustifiedWidth = unJustified.Sum(x => x.Bounds.Width); - // At least one earlier line should stay unchanged, proving that a // paragraph-final line created by the explicit newline was not justified. foundUnchangedNonLastLine |= linesMatch; // At least one other earlier line should still widen, proving that we // did not disable justification for all wrapped lines. - foundJustifiedNonParagraphLine |= justifiedWidth > unJustifiedWidth; + foundJustifiedNonParagraphLine |= justifiedLineMetrics[i].Extent.X > unJustifiedLineMetrics[i].Extent.X; } } @@ -888,7 +2273,7 @@ public void TextJustification_MultiParagraph_Horizontal_DoesNotJustifyParagraphF [InlineData(TextDirection.LeftToRight, TextJustification.InterWord)] [InlineData(TextDirection.RightToLeft, TextJustification.InterCharacter)] [InlineData(TextDirection.RightToLeft, TextJustification.InterWord)] - public void TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFinalLines(TextDirection direction, TextJustification justification) + public void TextJustification_MultiParagraph_Vertical_SkipsFinalLines(TextDirection direction, TextJustification justification) { const string paragraph = "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc ornare maximus vehicula. Duis nisi velit, dictum id mauris vitae, lobortis pretium quam. Quisque sed nisi pulvinar, consequat justo id, feugiat leo. Cras eu elementum dui."; string text = $"{paragraph}\n{paragraph}"; @@ -903,60 +2288,34 @@ public void TextJustification_MultiParagraph_Vertical_DoesNotJustifyParagraphFin TextJustification = justification }; - TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, justification }); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { rtl = direction == TextDirection.RightToLeft, mode = justification }); - // Same coverage as the horizontal test, but in vertical flow where the - // effective line extent is measured along height rather than width. - IReadOnlyList> justifiedLines = CollectLines(TextLayout.GenerateLayout(text.AsSpan(), options)); + ReadOnlySpan justifiedLineMetrics = TextMeasurer.GetLineMetrics(text, options).Span; options.TextJustification = TextJustification.None; - IReadOnlyList> unJustifiedLines = CollectLines(TextLayout.GenerateLayout(text.AsSpan(), options)); + ReadOnlySpan unJustifiedLineMetrics = TextMeasurer.GetLineMetrics(text, options).Span; - Assert.Equal(unJustifiedLines.Count, justifiedLines.Count); + Assert.Equal(unJustifiedLineMetrics.Length, justifiedLineMetrics.Length); bool foundUnchangedNonLastLine = false; bool foundJustifiedNonParagraphLine = false; - for (int i = 0; i < justifiedLines.Count; i++) + for (int i = 0; i < justifiedLineMetrics.Length; i++) { - TextMeasurer.TryGetCharacterAdvances(justifiedLines[i], options.Dpi, out ReadOnlySpan justifiedCharacterBounds); - TextMeasurer.TryGetCharacterAdvances(unJustifiedLines[i], options.Dpi, out ReadOnlySpan unJustifiedCharacterBounds); - - GlyphBounds[] justified = justifiedCharacterBounds.ToArray(); - GlyphBounds[] unJustified = unJustifiedCharacterBounds.ToArray(); - - bool isLastLine = i == justifiedLines.Count - 1; - bool linesMatch = justified.Length == unJustified.Length; - - // Paragraph-final lines should preserve every per-glyph advance from the - // non-justified layout, so unchanged per-character heights are the signal - // that a line was intentionally skipped by the justification pass. - if (linesMatch) - { - for (int j = 0; j < justified.Length; j++) - { - if (justified[j].Bounds.Height != unJustified[j].Bounds.Height) - { - linesMatch = false; - break; - } - } - } + bool isLastLine = i == justifiedLineMetrics.Length - 1; + bool linesMatch = MathF.Abs(justifiedLineMetrics[i].Extent.Y - unJustifiedLineMetrics[i].Extent.Y) <= .01F; if (isLastLine) { // The trailing line in the text box must remain ragged in vertical layout too. - Assert.True(linesMatch); + Assert.Equal(unJustifiedLineMetrics[i].Extent.Y, justifiedLineMetrics[i].Extent.Y, 4F); } else { - float justifiedHeight = justified.Sum(x => x.Bounds.Height); - float unJustifiedHeight = unJustified.Sum(x => x.Bounds.Height); - // This captures a non-last line that still behaves like a paragraph end. foundUnchangedNonLastLine |= linesMatch; // This captures a wrapped line that continues to justify normally. - foundJustifiedNonParagraphLine |= justifiedHeight > unJustifiedHeight; + foundJustifiedNonParagraphLine |= justifiedLineMetrics[i].Extent.Y > unJustifiedLineMetrics[i].Extent.Y; } } @@ -981,34 +2340,35 @@ public void TextJustification_InterWord_Horizontal(TextDirection direction) TextJustification = TextJustification.InterWord }; - // Collect the first line so we can compare it to the target wrapping length. - IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); - IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); - TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan justifiedCharacterBounds); + TextMetrics justifiedMetrics = TextMeasurer.Measure(text, options); - TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, options.TextJustification }); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { rtl = direction == TextDirection.RightToLeft }); - Assert.Equal(wrappingLength, justifiedCharacterBounds.ToArray().Sum(x => x.Bounds.Width), 4F); + Assert.Equal(wrappingLength, justifiedMetrics.LineMetrics[0].Extent.X, 4F); - // Now compare character widths. options.TextJustification = TextJustification.None; - IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); - IReadOnlyList line = CollectFirstLine(glyphs); - TextMeasurer.TryGetCharacterAdvances(line, options.Dpi, out ReadOnlySpan characterBounds); + TextMetrics unJustifiedMetrics = TextMeasurer.Measure(text, options); - // All but the last justified whitespace character advance should be greater than the - // corresponding character advance. - for (int i = 0; i < characterBounds.Length; i++) + Assert.Equal(unJustifiedMetrics.GetGlyphMetrics().Length, justifiedMetrics.GetGlyphMetrics().Length); + + bool foundWidenedWhitespace = false; + for (int i = 0; i < justifiedMetrics.GetGlyphMetrics().Length; i++) { - if (CodePoint.IsWhiteSpace(characterBounds[i].Codepoint) && i != characterBounds.Length - 1) + float justifiedWidth = justifiedMetrics.GetGlyphMetrics().Span[i].Advance.Width; + float unJustifiedWidth = unJustifiedMetrics.GetGlyphMetrics().Span[i].Advance.Width; + + if (CodePoint.IsWhiteSpace(unJustifiedMetrics.GetGlyphMetrics().Span[i].CodePoint)) { - Assert.True(justifiedCharacterBounds[i].Bounds.Width > characterBounds[i].Bounds.Width); + Assert.True(justifiedWidth >= unJustifiedWidth); + foundWidenedWhitespace |= justifiedWidth > unJustifiedWidth; } else { - Assert.Equal(justifiedCharacterBounds[i].Bounds.Width, characterBounds[i].Bounds.Width); + Assert.Equal(unJustifiedWidth, justifiedWidth); } } + + Assert.True(foundWidenedWhitespace); } [Theory] @@ -1028,34 +2388,28 @@ public void TextJustification_InterCharacter_Vertical(TextDirection direction) TextJustification = TextJustification.InterCharacter }; - // Collect the first line so we can compare it to the target wrapping length. - IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); - IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); - TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan justifiedCharacterBounds); + TextMetrics justifiedMetrics = TextMeasurer.Measure(text, options); - TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, options.TextJustification }); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { rtl = direction == TextDirection.RightToLeft }); - Assert.Equal(wrappingLength, justifiedCharacterBounds.ToArray().Sum(x => x.Bounds.Height), 4F); + Assert.Equal(wrappingLength, justifiedMetrics.LineMetrics[0].Extent.Y, 4F); - // Now compare character widths. options.TextJustification = TextJustification.None; - IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); - IReadOnlyList line = CollectFirstLine(glyphs); - TextMeasurer.TryGetCharacterAdvances(line, options.Dpi, out ReadOnlySpan characterBounds); + TextMetrics unJustifiedMetrics = TextMeasurer.Measure(text, options); + + Assert.Equal(unJustifiedMetrics.GetGlyphMetrics().Length, justifiedMetrics.GetGlyphMetrics().Length); - // All but the last justified character advance should be greater than the - // corresponding character advance. - for (int i = 0; i < characterBounds.Length; i++) + bool foundWidenedCharacter = false; + for (int i = 0; i < justifiedMetrics.GetGlyphMetrics().Length; i++) { - if (i == characterBounds.Length - 1) - { - Assert.Equal(justifiedCharacterBounds[i].Bounds.Height, characterBounds[i].Bounds.Height); - } - else - { - Assert.True(justifiedCharacterBounds[i].Bounds.Height > characterBounds[i].Bounds.Height); - } + float justifiedHeight = justifiedMetrics.GetGlyphMetrics().Span[i].Advance.Height; + float unJustifiedHeight = unJustifiedMetrics.GetGlyphMetrics().Span[i].Advance.Height; + + Assert.True(justifiedHeight >= unJustifiedHeight); + foundWidenedCharacter |= justifiedHeight > unJustifiedHeight; } + + Assert.True(foundWidenedCharacter); } [Theory] @@ -1075,34 +2429,35 @@ public void TextJustification_InterWord_Vertical(TextDirection direction) TextJustification = TextJustification.InterWord }; - // Collect the first line so we can compare it to the target wrapping length. - IReadOnlyList justifiedGlyphs = TextLayout.GenerateLayout(text.AsSpan(), options); - IReadOnlyList justifiedLine = CollectFirstLine(justifiedGlyphs); - TextMeasurer.TryGetCharacterAdvances(justifiedLine, options.Dpi, out ReadOnlySpan justifiedCharacterBounds); + TextMetrics justifiedMetrics = TextMeasurer.Measure(text, options); - TextLayoutTestUtilities.TestLayout(text, options, properties: new { direction, options.TextJustification }); + TextLayoutTestUtilities.TestLayout(text, options, properties: new { rtl = direction == TextDirection.RightToLeft }); - Assert.Equal(wrappingLength, justifiedCharacterBounds.ToArray().Sum(x => x.Bounds.Height), 4F); + Assert.Equal(wrappingLength, justifiedMetrics.LineMetrics[0].Extent.Y, 4F); - // Now compare character widths. options.TextJustification = TextJustification.None; - IReadOnlyList glyphs = TextLayout.GenerateLayout(text.AsSpan(), options); - IReadOnlyList line = CollectFirstLine(glyphs); - TextMeasurer.TryGetCharacterAdvances(line, options.Dpi, out ReadOnlySpan characterBounds); + TextMetrics unJustifiedMetrics = TextMeasurer.Measure(text, options); + + Assert.Equal(unJustifiedMetrics.GetGlyphMetrics().Length, justifiedMetrics.GetGlyphMetrics().Length); - // All but the last justified whitespace character advance should be greater than the - // corresponding character advance. - for (int i = 0; i < characterBounds.Length; i++) + bool foundWidenedWhitespace = false; + for (int i = 0; i < justifiedMetrics.GetGlyphMetrics().Length; i++) { - if (CodePoint.IsWhiteSpace(characterBounds[i].Codepoint) && i != characterBounds.Length - 1) + float justifiedHeight = justifiedMetrics.GetGlyphMetrics().Span[i].Advance.Height; + float unJustifiedHeight = unJustifiedMetrics.GetGlyphMetrics().Span[i].Advance.Height; + + if (CodePoint.IsWhiteSpace(unJustifiedMetrics.GetGlyphMetrics().Span[i].CodePoint)) { - Assert.True(justifiedCharacterBounds[i].Bounds.Height > characterBounds[i].Bounds.Height); + Assert.True(justifiedHeight >= unJustifiedHeight); + foundWidenedWhitespace |= justifiedHeight > unJustifiedHeight; } else { - Assert.Equal(justifiedCharacterBounds[i].Bounds.Height, characterBounds[i].Bounds.Height); + Assert.Equal(unJustifiedHeight, justifiedHeight); } } + + Assert.True(foundWidenedWhitespace); } public static TheoryData OpenSans_Data { get; } @@ -1214,8 +2569,9 @@ public void TrueTypeHinting_CanHintSmallOpenSans(char c, FontRectangle expected) HintingMode = HintingMode.Standard }; - FontRectangle actual = TextMeasurer.MeasureSize(c.ToString(), options); - Assert.Equal(expected, actual, Comparer); + FontRectangle actual = TextMeasurer.MeasureBounds(c.ToString(), options); + Assert.Equal(expected.Width, actual.Width, Comparer); + Assert.Equal(expected.Height, actual.Height, Comparer); options = new(OpenSansWoff) { @@ -1223,8 +2579,9 @@ public void TrueTypeHinting_CanHintSmallOpenSans(char c, FontRectangle expected) HintingMode = HintingMode.Standard }; - actual = TextMeasurer.MeasureSize(c.ToString(), options); - Assert.Equal(expected, actual, Comparer); + actual = TextMeasurer.MeasureBounds(c.ToString(), options); + Assert.Equal(expected.Width, actual.Width, Comparer); + Assert.Equal(expected.Height, actual.Height, Comparer); } public static TheoryData FontTrackingHorizontalData { get; } @@ -1248,11 +2605,11 @@ public void FontTracking_SpaceCharacters_WithHorizontalLayout(string text, float Tracking = tracking, }; - FontRectangle actual = TextMeasurer.MeasureSize(text, options); + FontRectangle actual = TextMeasurer.MeasureBounds(text, options); Assert.Equal(width, actual.Width, Comparer); - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds)); - Assert.Equal(characterPosition, bounds.ToArray().Select(x => x.Bounds.X), Comparer); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; + Assert.Equal(characterPosition, glyphs.ToArray().Select(x => x.Bounds.X), Comparer); } public static TheoryData FontTrackingVerticalData { get; } @@ -1277,11 +2634,11 @@ public void FontTracking_SpaceCharacters_WithVerticalLayout(string text, float t LayoutMode = LayoutMode.VerticalLeftRight, }; - FontRectangle actual = TextMeasurer.MeasureSize(text, options); + FontRectangle actual = TextMeasurer.MeasureBounds(text, options); Assert.Equal(width, actual.Height, Comparer); - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds)); - Assert.Equal(characterPosition, bounds.ToArray().Select(x => x.Bounds.Y), Comparer); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; + Assert.Equal(characterPosition, glyphs.ToArray().Select(x => x.Bounds.Y), Comparer); } [Theory] @@ -1295,7 +2652,7 @@ public void FontTracking_DoNotAddSpacingAfterCharacterThatDidNotAdvance(string t Tracking = tracking, }; - FontRectangle actual = TextMeasurer.MeasureSize(text, options); + FontRectangle actual = TextMeasurer.MeasureBounds(text, options); Assert.Equal(width, actual.Width, Comparer); } @@ -1312,7 +2669,7 @@ public void FontTracking_CorrectlyAddSpacingForComposedCharacter(string text, fl Tracking = tracking, }; - FontRectangle actual = TextMeasurer.MeasureSize(text, options); + FontRectangle actual = TextMeasurer.MeasureBounds(text, options); Assert.Equal(width, actual.Width, Comparer); } @@ -1391,7 +2748,7 @@ public void CanMeasureTextAdvance() } [Fact] - public void CanMeasureCharacterLayouts() + public void CanMeasureGlyphLayouts() { FontFamily family = TestFonts.GetFontFamily(TestFonts.OpenSansFile); family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics); @@ -1403,31 +2760,16 @@ public void CanMeasureCharacterLayouts() const string text = "Hello World!"; - Assert.True(TextMeasurer.TryMeasureCharacterAdvances(text, options, out ReadOnlySpan advances)); - Assert.True(TextMeasurer.TryMeasureCharacterSizes(text, options, out ReadOnlySpan sizes)); - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds)); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; - Assert.Equal(advances.Length, sizes.Length); - Assert.Equal(advances.Length, bounds.Length); - - for (int i = 0; i < advances.Length; i++) + for (int i = 0; i < glyphs.Length; i++) { - GlyphBounds advance = advances[i]; - GlyphBounds size = sizes[i]; - GlyphBounds bound = bounds[i]; - - Assert.Equal(advance.Codepoint, size.Codepoint); - Assert.Equal(advance.Codepoint, bound.Codepoint); - - // Since this is a single line starting at 0,0 the following should be predictable. - Assert.Equal(advance.Bounds.X, size.Bounds.X); - Assert.Equal(size.Bounds.Width, bound.Bounds.Width); - Assert.Equal(size.Bounds.Height, bound.Bounds.Height); + Assert.Equal(FontRectangle.Union(glyphs[i].Advance, glyphs[i].Bounds), glyphs[i].RenderableBounds, Comparer); } } [Fact] - public void CanMeasureMultilineCharacterLayouts() + public void CanMeasureMultilineGlyphLayouts() { FontFamily family = TestFonts.GetFontFamily(TestFonts.OpenSansFile); family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics); @@ -1439,34 +2781,38 @@ public void CanMeasureMultilineCharacterLayouts() const string text = "A\nA\nA\nA"; - Assert.True(TextMeasurer.TryMeasureCharacterAdvances(text, options, out ReadOnlySpan advances)); - Assert.True(TextMeasurer.TryMeasureCharacterSizes(text, options, out ReadOnlySpan sizes)); - - Assert.Equal(advances.Length, sizes.Length); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; - GlyphBounds? lastAdvance = null; - GlyphBounds? lastSize = null; - for (int i = 0; i < advances.Length; i++) + GlyphMetrics? lastAdvance = null; + int lineBreakCount = 0; + for (int i = 0; i < glyphs.Length; i++) { - GlyphBounds advance = advances[i]; - GlyphBounds size = sizes[i]; + GlyphMetrics advance = glyphs[i]; - Assert.Equal(advance.Codepoint, size.Codepoint); + if (CodePoint.IsNewLine(advance.CodePoint)) + { + Assert.True(advance.Advance.Width > 0 || advance.Advance.Height > 0); + lineBreakCount++; + continue; + } if (lastAdvance.HasValue) { - Assert.Equal(lastAdvance.Value.Bounds, advance.Bounds); - Assert.Equal(lastSize.Value.Bounds.Width, size.Bounds.Width, 2F); - Assert.Equal(lastSize.Value.Bounds.Height, size.Bounds.Height, 2F); + Assert.Equal(lastAdvance.Value.Advance.Width, advance.Advance.Width, Comparer); + Assert.Equal(lastAdvance.Value.Advance.Height, advance.Advance.Height, Comparer); } lastAdvance = advance; - lastSize = size; } + + // Hard breaks that terminate non-empty lines are trimmed from the visual + // glyph stream. The four visible glyphs are still laid out on four lines. + Assert.Equal(0, lineBreakCount); + Assert.Equal(4, glyphs.Length); } [Fact] - public void DoesMeasureCharacterLayoutIncludeStringIndex() + public void DoesMeasureGlyphLayoutIncludeStringIndex() { FontFamily family = TestFonts.GetFontFamily(TestFonts.OpenSansFile); family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics); @@ -1478,28 +2824,15 @@ public void DoesMeasureCharacterLayoutIncludeStringIndex() const string text = "The quick👩🏽‍🚒 brown fox jumps over \r\n the lazy dog"; - Assert.True(TextMeasurer.TryMeasureCharacterAdvances(text, options, out ReadOnlySpan advances)); - Assert.True(TextMeasurer.TryMeasureCharacterSizes(text, options, out ReadOnlySpan sizes)); - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds)); - - Assert.Equal(advances.Length, sizes.Length); - Assert.Equal(advances.Length, bounds.Length); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; int stringIndex = -1; - for (int i = 0; i < advances.Length; i++) + for (int i = 0; i < glyphs.Length; i++) { - GlyphBounds advance = advances[i]; - GlyphBounds size = sizes[i]; - GlyphBounds bound = bounds[i]; + GlyphMetrics bound = glyphs[i]; - Assert.Equal(bound.StringIndex, advance.StringIndex); - Assert.Equal(bound.StringIndex, size.StringIndex); - - Assert.Equal(bound.GraphemeIndex, advance.GraphemeIndex); - Assert.Equal(bound.GraphemeIndex, size.GraphemeIndex); - - if (bound.Codepoint == new CodePoint("k"[0])) + if (bound.CodePoint == new CodePoint("k"[0])) { stringIndex = text.IndexOf('k'); Assert.Equal(stringIndex, bound.StringIndex); @@ -1507,7 +2840,7 @@ public void DoesMeasureCharacterLayoutIncludeStringIndex() } // after emoji - if (bound.Codepoint == new CodePoint("b"[0])) + if (bound.CodePoint == new CodePoint("b"[0])) { stringIndex = text.IndexOf('b'); Assert.NotEqual(bound.StringIndex, bound.GraphemeIndex); @@ -1523,17 +2856,17 @@ public void DoesMeasureCharacterLayoutIncludeStringIndex() graphemeCount += 1; } - GlyphBounds firstBound = bounds[0]; + GlyphMetrics firstBound = glyphs[0]; Assert.Equal(0, firstBound.StringIndex); Assert.Equal(0, firstBound.GraphemeIndex); - GlyphBounds lastBound = bounds[^1]; + GlyphMetrics lastBound = glyphs[^1]; Assert.Equal(text.Length - 1, lastBound.StringIndex); Assert.Equal(graphemeCount - 1, lastBound.GraphemeIndex); } [Fact] - public void DoesMeasureCharacterBoundsExtendForAdvanceMultipliers() + public void DoesGetGlyphMetricsExtendForAdvanceMultipliers() { FontFamily family = TestFonts.GetFontFamily(TestFonts.OpenSansFile); family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics); @@ -1545,25 +2878,22 @@ public void DoesMeasureCharacterBoundsExtendForAdvanceMultipliers() const string text = "H\tH"; - List glyphs = CaptureGlyphBoundBuilder.GenerateGlyphsBoxes(text, options); - TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds); + List glyphs = CaptureGlyphRectangleBuilder.GenerateGlyphRectangles(text, options); + ReadOnlySpan glyphMetrics = TextMeasurer.GetGlyphMetrics(text, options).Span; - IReadOnlyList glyphLayouts = TextLayout.GenerateLayout(text, options); - - Assert.Equal(glyphs.Count, bounds.Length); - Assert.Equal(glyphs.Count, glyphs.Count); + Assert.Equal(glyphs.Count, glyphMetrics.Length); for (int glyphIndex = 0; glyphIndex < glyphs.Count; glyphIndex++) { FontRectangle renderGlyph = glyphs[glyphIndex]; - FontRectangle measureGlyph = bounds[glyphIndex].Bounds; - GlyphLayout glyphLayout = glyphLayouts[glyphIndex]; + FontRectangle measureGlyph = glyphMetrics[glyphIndex].Bounds; + GlyphMetrics metric = glyphMetrics[glyphIndex]; - if (glyphLayout.IsWhiteSpace()) + if (CodePoint.IsWhiteSpace(metric.CodePoint)) { Assert.Equal(renderGlyph.X, measureGlyph.X); Assert.Equal(renderGlyph.Y, measureGlyph.Y); - Assert.Equal(glyphLayout.AdvanceX * options.Dpi, measureGlyph.Width); + Assert.Equal(metric.Advance.Width, measureGlyph.Width); Assert.Equal(renderGlyph.Height, measureGlyph.Height); } else @@ -1574,7 +2904,7 @@ public void DoesMeasureCharacterBoundsExtendForAdvanceMultipliers() } [Fact] - public void IsMeasureCharacterBoundsSameAsRenderBounds() + public void IsGetGlyphMetricsSameAsRenderBounds() { FontFamily family = TestFonts.GetFontFamily(TestFonts.OpenSansFile); family.TryGetMetrics(FontStyle.Regular, out FontMetrics metrics); @@ -1585,15 +2915,15 @@ public void IsMeasureCharacterBoundsSameAsRenderBounds() const string text = "Hello WorLLd"; - List glyphs = CaptureGlyphBoundBuilder.GenerateGlyphsBoxes(text, options); - TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds); + List glyphs = CaptureGlyphRectangleBuilder.GenerateGlyphRectangles(text, options); + ReadOnlySpan glyphMetrics = TextMeasurer.GetGlyphMetrics(text, options).Span; - Assert.Equal(glyphs.Count, bounds.Length); + Assert.Equal(glyphs.Count, glyphMetrics.Length); for (int glyphIndex = 0; glyphIndex < glyphs.Count; glyphIndex++) { FontRectangle renderGlyph = glyphs[glyphIndex]; - FontRectangle measureGlyph = bounds[glyphIndex].Bounds; + FontRectangle measureGlyph = glyphMetrics[glyphIndex].Bounds; Assert.Equal(renderGlyph.X, measureGlyph.X); Assert.Equal(renderGlyph.Y, measureGlyph.Y); @@ -1622,25 +2952,25 @@ public void BreakWordEnsuresSingleCharacterPerLine() Assert.Equal(text.Length - 1, lineCount); } - private class CaptureGlyphBoundBuilder : IGlyphRenderer + private class CaptureGlyphRectangleBuilder : IGlyphRenderer { - public static List GenerateGlyphsBoxes(string text, TextOptions options) + public static List GenerateGlyphRectangles(string text, TextOptions options) { - CaptureGlyphBoundBuilder glyphBuilder = new(); + CaptureGlyphRectangleBuilder glyphBuilder = new(); TextRenderer renderer = new(glyphBuilder); renderer.RenderText(text, options); - return glyphBuilder.GlyphBounds; + return glyphBuilder.GlyphRectangles; } - public readonly List GlyphBounds = []; + public readonly List GlyphRectangles = []; - public CaptureGlyphBoundBuilder() + public CaptureGlyphRectangleBuilder() { } bool IGlyphRenderer.BeginGlyph(in FontRectangle bounds, in GlyphRendererParameters parameters) { - this.GlyphBounds.Add(bounds); + this.GlyphRectangles.Add(bounds); return true; } @@ -1812,60 +3142,121 @@ public void TrueTypeHinting_CanHintSmallSegoeUi(char c, FontRectangle expected) HintingMode = HintingMode.Standard }; - FontRectangle actual = TextMeasurer.MeasureSize(c.ToString(), options); + FontRectangle actual = TextMeasurer.MeasureBounds(c.ToString(), options); - Assert.Equal(expected, actual, Comparer); + Assert.Equal(expected.Width, actual.Width, Comparer); + Assert.Equal(expected.Height, actual.Height, Comparer); } private static readonly Font SegoeUi = SystemFonts.CreateFont("Segoe Ui", 10); #endif - private static List CollectFirstLine(IReadOnlyList glyphs) + private static void DrawCaret( + IImageProcessingContext context, + CaretPosition caret, + Color color, + float thickness, + bool dashed) { - List line = - [ - glyphs[0] - ]; + DrawCaretLine(context, caret.Start, caret.End, color, thickness, dashed); - for (int i = 1; i < glyphs.Count; i++) + if (caret.HasSecondary) { - if (glyphs[i].IsStartOfLine) - { - break; - } + DrawCaretLine(context, caret.SecondaryStart, caret.SecondaryEnd, color, thickness, dashed); + } + } - line.Add(glyphs[i]); + private static void DrawCaretLine( + IImageProcessingContext context, + Vector2 start, + Vector2 end, + Color color, + float thickness, + bool dashed) + { + if (!dashed) + { + context.DrawLine(color, thickness, new PointF(start.X, start.Y), new PointF(end.X, end.Y)); + return; } - return line; + Vector2 delta = end - start; + Vector2 step = Vector2.Normalize(delta); + float length = delta.Length(); + + // Build movement carets from short line segments instead of using a pen + // option so the dashed output is independent of ImageSharp.Drawing internals. + const float dash = 5F; + const float gap = 4F; + + for (float distance = 0; distance < length; distance += dash + gap) + { + Vector2 dashStart = start + (step * distance); + Vector2 dashEnd = start + (step * MathF.Min(distance + dash, length)); + + context.DrawLine(color, thickness, new PointF(dashStart.X, dashStart.Y), new PointF(dashEnd.X, dashEnd.Y)); + } } - private static IReadOnlyList> CollectLines(IReadOnlyList glyphs) + private static string GetSourceTextForLine(string text, TextMetrics metrics, int lineIndex) { - List> lines = []; - List? current = null; + int glyphIndex = 0; + for (int i = 0; i < lineIndex; i++) + { + glyphIndex = AdvanceGlyphIndex(metrics.GetGlyphMetrics().Span, glyphIndex, metrics.LineMetrics[i].GraphemeCount); + } + + int startGlyphIndex = glyphIndex; + int endGlyphIndex = AdvanceGlyphIndex(metrics.GetGlyphMetrics().Span, glyphIndex, metrics.LineMetrics[lineIndex].GraphemeCount); - // Re-slice the flat glyph list into visual lines using IsStartOfLine so the - // tests can compare whole lines instead of reasoning about glyph indices. - for (int i = 0; i < glyphs.Count; i++) + int start = int.MaxValue; + int end = 0; + for (int i = startGlyphIndex; i < endGlyphIndex; i++) { - if (glyphs[i].IsStartOfLine || current is null) + GlyphMetrics glyph = metrics.GetGlyphMetrics().Span[i]; + start = Math.Min(start, glyph.StringIndex); + end = Math.Max(end, glyph.StringIndex + glyph.CodePoint.Utf16SequenceLength); + } + + return text[start..end]; + } + + private static int AdvanceGlyphIndex(ReadOnlySpan glyphs, int glyphIndex, int graphemeCount) + { + int consumed = 0; + int lastGraphemeIndex = -1; + while (glyphIndex < glyphs.Length && consumed < graphemeCount) + { + int graphemeIndex = glyphs[glyphIndex].GraphemeIndex; + if (graphemeIndex != lastGraphemeIndex) { - current = []; - lines.Add(current); + consumed++; + lastGraphemeIndex = graphemeIndex; } - current.Add(glyphs[i]); + glyphIndex++; + } + + while (glyphIndex < glyphs.Length && glyphs[glyphIndex].GraphemeIndex == lastGraphemeIndex) + { + glyphIndex++; } - return lines; + return glyphIndex; } - private static string GetSourceTextForLine(string text, IReadOnlyList line) + private static int CountGlyphs(ReadOnlySpan glyphs, CodePoint codePoint) { - int start = line.Min(x => x.StringIndex); - int end = line.Max(x => x.StringIndex + x.CodePoint.Utf16SequenceLength); - return text[start..end]; + int count = 0; + for (int i = 0; i < glyphs.Length; i++) + { + if (glyphs[i].CodePoint == codePoint) + { + count++; + } + } + + return count; } #if OS_WINDOWS @@ -1873,7 +3264,7 @@ private static string GetSourceTextForLine(string text, IReadOnlyList metrics = TextMeasurer.GetLineMetrics(text, Options()).Span; + Assert.Equal(expectedLineMetrics, metrics.Length); + Assert.Equal(expectedLineMetrics, TextMeasurer.CountLines(text, Options())); } [Fact] public void GetLineMetrics_PerLineExtent_MatchesLineAdvance() { const string text = "Hello\nWorld world"; - LineMetrics[] metrics = TextMeasurer.GetLineMetrics(text, Options()); + ReadOnlySpan metrics = TextMeasurer.GetLineMetrics(text, Options()).Span; Assert.Equal(2, metrics.Length); FontRectangle line1Advance = TextMeasurer.MeasureAdvance("Hello", Options()); FontRectangle line2Advance = TextMeasurer.MeasureAdvance("World world", Options()); - Assert.Equal(line1Advance.Width, metrics[0].Extent, Comparer); - Assert.Equal(line2Advance.Width, metrics[1].Extent, Comparer); + Assert.Equal(line1Advance.Width, metrics[0].Extent.X, Comparer); + Assert.Equal(line2Advance.Width, metrics[1].Extent.X, Comparer); } [Fact] public void GetLineMetrics_AscenderBaselineDescender_AreOrdered() { - LineMetrics[] metrics = TextMeasurer.GetLineMetrics("Hello", Options()); - Assert.Single(metrics); + ReadOnlySpan metrics = TextMeasurer.GetLineMetrics("Hello", Options()).Span; + Assert.Equal(1, metrics.Length); LineMetrics m = metrics[0]; Assert.True(m.Ascender < m.Baseline, $"Ascender ({m.Ascender}) should be < Baseline ({m.Baseline})"); Assert.True(m.Baseline < m.Descender, $"Baseline ({m.Baseline}) should be < Descender ({m.Descender})"); @@ -185,113 +162,130 @@ public void GetLineMetrics_AscenderBaselineDescender_AreOrdered() [Fact] public void GetLineMetrics_EmptyText_ReturnsEmpty() - => Assert.Empty(TextMeasurer.GetLineMetrics(string.Empty, Options())); + => Assert.True(TextMeasurer.GetLineMetrics(string.Empty, Options()).IsEmpty); - // ====================================================================== - // Per-character metadata (codepoints, indices) — character content pinned - // ====================================================================== [Fact] - public void TryMeasureCharacterBounds_Hi_Pinned() + public void GetGlyphMetrics_Hi_Pinned() { const string text = "Hi!"; - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, Options(), out ReadOnlySpan bounds)); + ReadOnlySpan metrics = TextMeasurer.GetGlyphMetrics(text, Options()).Span; - GlyphBounds[] expected = + GlyphMetrics[] expected = [ - new(new CodePoint('H'), new FontRectangle(1.1719f, 2.0889f, 6.4922f, 8.5664f), 0, 0), - new(new CodePoint('i'), new FontRectangle(9.7852f, 1.8311f, 1.1719f, 8.8242f), 1, 1), - new(new CodePoint('!'), new FontRectangle(12.7559f, 2.0889f, 1.3945f, 8.7305f), 2, 2), + new( + new CodePoint('H'), + new FontRectangle(0, 0, 8.8477f, 12f), + new FontRectangle(1.1719f, 2.0889f, 6.4922f, 8.5664f), + new FontRectangle(0f, 0f, 8.8477f, 12f), + 0, + 0), + new( + new CodePoint('i'), + new FontRectangle(8.8477f, 0, 3.0293f, 12f), + new FontRectangle(9.7852f, 1.8311f, 1.1719f, 8.8242f), + new FontRectangle(8.8477f, 0f, 3.0293f, 12f), + 1, + 1), + new( + new CodePoint('!'), + new FontRectangle(11.877f, 0, 3.1699f, 12f), + new FontRectangle(12.7559f, 2.0889f, 1.3945f, 8.7305f), + new FontRectangle(11.877f, 0f, 3.1699f, 12f), + 2, + 2), ]; - AssertGlyphBoundsEqual(expected, bounds); + + AssertGlyphMetricsEqual(expected, metrics); } [Fact] - public void TryMeasureCharacterAdvances_Hi_Pinned() + public void GetGraphemeMetrics_Hi_Pinned() { const string text = "Hi!"; - Assert.True(TextMeasurer.TryMeasureCharacterAdvances(text, Options(), out ReadOnlySpan advances)); + ReadOnlySpan graphemes = TextMeasurer.GetGraphemeMetrics(text, Options()).Span; - GlyphBounds[] expected = + GraphemeMetrics[] expected = [ - new(new CodePoint('H'), new FontRectangle(0, 0, 8.8477f, 12f), 0, 0), - new(new CodePoint('i'), new FontRectangle(0, 0, 3.0293f, 12f), 1, 1), - new(new CodePoint('!'), new FontRectangle(0, 0, 3.1699f, 12f), 2, 2), + new( + new FontRectangle(0, 0, 8.8477f, 12f), + new FontRectangle(1.1719f, 2.0889f, 6.4922f, 8.5664f), + new FontRectangle(0, 0, 8.8477f, 12f), + 0, + 0, + 0, + false), + new( + new FontRectangle(8.8477f, 0, 3.0293f, 12f), + new FontRectangle(9.7852f, 1.8311f, 1.1719f, 8.8242f), + new FontRectangle(8.8477f, 0, 3.0293f, 12f), + 1, + 1, + 0, + false), + new( + new FontRectangle(11.877f, 0, 3.1699f, 12f), + new FontRectangle(12.7559f, 2.0889f, 1.3945f, 8.7305f), + new FontRectangle(11.877f, 0, 3.1699f, 12f), + 2, + 2, + 0, + false), ]; - AssertGlyphBoundsEqual(expected, advances); + AssertGraphemeMetricsEqual(expected, graphemes); } [Fact] - public void TryMeasureCharacterSizes_Hi_Pinned() + public void GetGlyphMetrics_Codepoints_PreserveSourceOrder() { const string text = "Hi!"; - Assert.True(TextMeasurer.TryMeasureCharacterSizes(text, Options(), out ReadOnlySpan sizes)); - - GlyphBounds[] expected = - [ - new(new CodePoint('H'), new FontRectangle(0, 0, 6.4922f, 8.5664f), 0, 0), - new(new CodePoint('i'), new FontRectangle(0, 0, 1.1719f, 8.8242f), 1, 1), - new(new CodePoint('!'), new FontRectangle(0, 0, 1.3945f, 8.7305f), 2, 2), - ]; - AssertGlyphBoundsEqual(expected, sizes); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, Options()).Span; + + Assert.Equal(3, glyphs.Length); + Assert.Equal(new CodePoint('H'), glyphs[0].CodePoint); + Assert.Equal(new CodePoint('i'), glyphs[1].CodePoint); + Assert.Equal(new CodePoint('!'), glyphs[2].CodePoint); + Assert.Equal(0, glyphs[0].StringIndex); + Assert.Equal(1, glyphs[1].StringIndex); + Assert.Equal(2, glyphs[2].StringIndex); } [Fact] - public void TryMeasureCharacterRenderableBounds_Hi_Pinned() + public void GetGlyphMetrics_Newline_IsTrimmedAndAdvancesIndices() { - const string text = "Hi!"; - Assert.True(TextMeasurer.TryMeasureCharacterRenderableBounds(text, Options(), out ReadOnlySpan renderable)); + const string text = "A\nB"; + TextOptions options = Options(); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; - GlyphBounds[] expected = - [ - new(new CodePoint('H'), new FontRectangle(0f, 0f, 8.8477f, 12f), 0, 0), - new(new CodePoint('i'), new FontRectangle(8.8477f, 0f, 3.0293f, 12f), 1, 1), - new(new CodePoint('!'), new FontRectangle(11.877f, 0f, 3.1699f, 12f), 2, 2), - ]; - AssertGlyphBoundsEqual(expected, renderable); - } + Assert.Equal(2, glyphs.Length); + Assert.Equal(new CodePoint('A'), glyphs[0].CodePoint); + Assert.Equal(0, glyphs[0].StringIndex); - [Fact] - public void TryMeasureCharacterBounds_Codepoints_PreserveSourceOrder() - { - const string text = "Hi!"; - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, Options(), out ReadOnlySpan bounds)); - - Assert.Equal(3, bounds.Length); - Assert.Equal(new CodePoint('H'), bounds[0].Codepoint); - Assert.Equal(new CodePoint('i'), bounds[1].Codepoint); - Assert.Equal(new CodePoint('!'), bounds[2].Codepoint); - Assert.Equal(0, bounds[0].StringIndex); - Assert.Equal(1, bounds[1].StringIndex); - Assert.Equal(2, bounds[2].StringIndex); - } + Assert.Equal(new CodePoint('B'), glyphs[1].CodePoint); + Assert.Equal(2, glyphs[1].StringIndex); - [Fact] - public void TryMeasureCharacterBounds_Newline_IsSkippedButAdvancesIndices() - { - const string text = "A\nB"; - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, Options(), out ReadOnlySpan bounds)); + TextMetrics metrics = TextMeasurer.Measure(text, options); - Assert.Equal(2, bounds.Length); - Assert.Equal(new CodePoint('A'), bounds[0].Codepoint); - Assert.Equal(0, bounds[0].StringIndex); - Assert.Equal(new CodePoint('B'), bounds[1].Codepoint); - Assert.Equal(2, bounds[1].StringIndex); + // The newline ends a non-empty line and is trimmed from visual metrics. + // The following glyph keeps its source string index, so callers still see + // the original source gap while measurements only reflect visible content. + FontRectangle expectedBounds = FontRectangle.Union(glyphs[0].Bounds, glyphs[1].Bounds); + Assert.Equal(expectedBounds, metrics.Bounds, Comparer); } [Fact] - public void TryMeasureCharacterAdvances_EmptyText_ReturnsFalse() + public void GetGlyphMetrics_EmptyText_ReturnsEmpty() { - Assert.False(TextMeasurer.TryMeasureCharacterAdvances(string.Empty, Options(), out ReadOnlySpan advances)); - Assert.Equal(0, advances.Length); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(string.Empty, Options()).Span; + Assert.Equal(0, glyphs.Length); } [Fact] - public void TryMeasureCharacterBounds_ShiftsWithOrigin() + public void GetGlyphMetrics_ShiftsWithOrigin() { const string text = "Hi!"; - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, Options(), out ReadOnlySpan atZero)); - GlyphBounds[] zeroCopy = atZero.ToArray(); - Assert.True(TextMeasurer.TryMeasureCharacterBounds(text, Options(100, 50), out ReadOnlySpan atOffset)); + ReadOnlySpan atZero = TextMeasurer.GetGlyphMetrics(text, Options()).Span; + GlyphMetrics[] zeroCopy = atZero.ToArray(); + ReadOnlySpan atOffset = TextMeasurer.GetGlyphMetrics(text, Options(100, 50)).Span; Assert.Equal(zeroCopy.Length, atOffset.Length); for (int i = 0; i < zeroCopy.Length; i++) @@ -303,41 +297,6 @@ public void TryMeasureCharacterBounds_ShiftsWithOrigin() } } - [Fact] - public void TryMeasureCharacterSizes_IsOriginIndependent() - { - const string text = "Hi!"; - Assert.True(TextMeasurer.TryMeasureCharacterSizes(text, Options(), out ReadOnlySpan atZero)); - GlyphBounds[] zeroCopy = atZero.ToArray(); - Assert.True(TextMeasurer.TryMeasureCharacterSizes(text, Options(100, 50), out ReadOnlySpan atOffset)); - - Assert.Equal(zeroCopy.Length, atOffset.Length); - for (int i = 0; i < zeroCopy.Length; i++) - { - Assert.Equal(zeroCopy[i].Bounds, atOffset[i].Bounds, Comparer); - } - } - - // ====================================================================== - // Invariants — hold regardless of font or text - // ====================================================================== - [Theory] - [InlineData("Hello", 0f, 0f)] - [InlineData("Hello, World!", 100, 50)] - [InlineData("Hello\nWorld", 10, 20)] - [InlineData("A\nB\nC", -20, 30)] - public void Invariant_SizeEqualsBoundsWithZeroedOrigin(string text, float ox, float oy) - { - TextOptions options = Options(ox, oy); - FontRectangle bounds = TextMeasurer.MeasureBounds(text, options); - FontRectangle size = TextMeasurer.MeasureSize(text, options); - - Assert.Equal(0, size.X, Comparer); - Assert.Equal(0, size.Y, Comparer); - Assert.Equal(bounds.Width, size.Width, Comparer); - Assert.Equal(bounds.Height, size.Height, Comparer); - } - [Theory] [InlineData("Hello", 0f, 0f)] [InlineData("Hello, World!", 100, 50)] @@ -361,43 +320,28 @@ public void Invariant_RenderableBoundsEqualsUnionOfAdvanceAndBounds(string text, [InlineData("Hi!")] [InlineData("A\nB\nC")] [InlineData("Hello world")] - public void Invariant_AllPerCharArraysHaveSameLength(string text) + public void Invariant_GlyphMetricsRenderableBoundsEqualsUnion(string text) { TextOptions options = Options(); - TextMeasurer.TryMeasureCharacterAdvances(text, options, out ReadOnlySpan advances); - TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds); - TextMeasurer.TryMeasureCharacterSizes(text, options, out ReadOnlySpan sizes); - TextMeasurer.TryMeasureCharacterRenderableBounds(text, options, out ReadOnlySpan renderable); - - Assert.Equal(advances.Length, bounds.Length); - Assert.Equal(advances.Length, sizes.Length); - Assert.Equal(advances.Length, renderable.Length); + ReadOnlySpan glyphs = TextMeasurer.GetGlyphMetrics(text, options).Span; + + for (int i = 0; i < glyphs.Length; i++) + { + Assert.Equal(FontRectangle.Union(glyphs[i].Advance, glyphs[i].Bounds), glyphs[i].RenderableBounds, Comparer); + } } [Theory] [InlineData("Hello")] [InlineData("Hi!")] [InlineData("A\nB\nC")] - public void Invariant_PerCharMetadataMatchAcrossArrays(string text) + public void Invariant_StringAndSpanGlyphMetricsOverloads_Match(string text) { TextOptions options = Options(); - TextMeasurer.TryMeasureCharacterAdvances(text, options, out ReadOnlySpan advances); - TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan bounds); - TextMeasurer.TryMeasureCharacterSizes(text, options, out ReadOnlySpan sizes); - TextMeasurer.TryMeasureCharacterRenderableBounds(text, options, out ReadOnlySpan renderable); + ReadOnlySpan fromString = TextMeasurer.GetGlyphMetrics(text, options).Span; + ReadOnlySpan fromSpan = TextMeasurer.GetGlyphMetrics(text.AsSpan(), options).Span; - for (int i = 0; i < advances.Length; i++) - { - Assert.Equal(advances[i].Codepoint, bounds[i].Codepoint); - Assert.Equal(advances[i].Codepoint, sizes[i].Codepoint); - Assert.Equal(advances[i].Codepoint, renderable[i].Codepoint); - Assert.Equal(advances[i].GraphemeIndex, bounds[i].GraphemeIndex); - Assert.Equal(advances[i].StringIndex, bounds[i].StringIndex); - Assert.Equal(advances[i].GraphemeIndex, sizes[i].GraphemeIndex); - Assert.Equal(advances[i].StringIndex, sizes[i].StringIndex); - Assert.Equal(advances[i].GraphemeIndex, renderable[i].GraphemeIndex); - Assert.Equal(advances[i].StringIndex, renderable[i].StringIndex); - } + AssertGlyphMetricsEqual(fromString, fromSpan); } [Theory] @@ -416,10 +360,6 @@ public void Invariant_StringAndSpanOverloads_Match(string text) TextMeasurer.MeasureBounds(text, options), TextMeasurer.MeasureBounds(text.AsSpan(), options), Comparer); - Assert.Equal( - TextMeasurer.MeasureSize(text, options), - TextMeasurer.MeasureSize(text.AsSpan(), options), - Comparer); Assert.Equal( TextMeasurer.MeasureRenderableBounds(text, options), TextMeasurer.MeasureRenderableBounds(text.AsSpan(), options), @@ -437,11 +377,8 @@ public void Invariant_EmptyText_AllMethodsReturnEmpty() Assert.Equal(FontRectangle.Empty, TextMeasurer.MeasureAdvance(string.Empty, options), Comparer); Assert.Equal(FontRectangle.Empty, TextMeasurer.MeasureRenderableBounds(string.Empty, options), Comparer); Assert.Equal(0, TextMeasurer.CountLines(string.Empty, options)); - Assert.Empty(TextMeasurer.GetLineMetrics(string.Empty, options)); - Assert.False(TextMeasurer.TryMeasureCharacterAdvances(string.Empty, options, out _)); - Assert.False(TextMeasurer.TryMeasureCharacterBounds(string.Empty, options, out _)); - Assert.False(TextMeasurer.TryMeasureCharacterSizes(string.Empty, options, out _)); - Assert.False(TextMeasurer.TryMeasureCharacterRenderableBounds(string.Empty, options, out _)); + Assert.True(TextMeasurer.GetLineMetrics(string.Empty, options).IsEmpty); + Assert.True(TextMeasurer.GetGlyphMetrics(string.Empty, options).IsEmpty); } [Theory] @@ -466,17 +403,36 @@ public void Invariant_RenderableBoundsEqualsUnion_AcrossLayoutModes(LayoutMode l Assert.Equal(expected, renderable, Comparer); } - private static void AssertGlyphBoundsEqual(GlyphBounds[] expected, ReadOnlySpan actual) + private static void AssertGlyphMetricsEqual(ReadOnlySpan expected, ReadOnlySpan actual) { Assert.Equal(expected.Length, actual.Length); for (int i = 0; i < expected.Length; i++) { - GlyphBounds e = expected[i]; - GlyphBounds a = actual[i]; - Assert.Equal(e.Codepoint, a.Codepoint); + GlyphMetrics e = expected[i]; + GlyphMetrics a = actual[i]; + Assert.Equal(e.CodePoint, a.CodePoint); Assert.Equal(e.GraphemeIndex, a.GraphemeIndex); Assert.Equal(e.StringIndex, a.StringIndex); + Assert.Equal(e.Advance, a.Advance, Comparer); Assert.Equal(e.Bounds, a.Bounds, Comparer); + Assert.Equal(e.RenderableBounds, a.RenderableBounds, Comparer); + } + } + + private static void AssertGraphemeMetricsEqual(GraphemeMetrics[] expected, ReadOnlySpan actual) + { + Assert.Equal(expected.Length, actual.Length); + for (int i = 0; i < expected.Length; i++) + { + GraphemeMetrics e = expected[i]; + GraphemeMetrics a = actual[i]; + Assert.Equal(e.Advance, a.Advance, Comparer); + Assert.Equal(e.Bounds, a.Bounds, Comparer); + Assert.Equal(e.RenderableBounds, a.RenderableBounds, Comparer); + Assert.Equal(e.GraphemeIndex, a.GraphemeIndex); + Assert.Equal(e.StringIndex, a.StringIndex); + Assert.Equal(e.BidiLevel, a.BidiLevel); + Assert.Equal(e.IsLineBreak, a.IsLineBreak); } } } diff --git a/tests/SixLabors.Fonts.Tests/TextMetricsTests.cs b/tests/SixLabors.Fonts.Tests/TextMetricsTests.cs index 3c9eb554..335d3610 100644 --- a/tests/SixLabors.Fonts.Tests/TextMetricsTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextMetricsTests.cs @@ -9,23 +9,6 @@ public class TextMetricsTests { private static readonly ApproximateFloatComparer Comparer = new(0.001F); - [Fact] - public void Empty_HasZeroedRectanglesAndEmptyCollections() - { - TextMetrics metrics = TextMetrics.Empty; - - Assert.Equal(FontRectangle.Empty, metrics.Advance, Comparer); - Assert.Equal(FontRectangle.Empty, metrics.Bounds, Comparer); - Assert.Equal(FontRectangle.Empty, metrics.Size, Comparer); - Assert.Equal(FontRectangle.Empty, metrics.RenderableBounds, Comparer); - Assert.Equal(0, metrics.LineCount); - Assert.Empty(metrics.CharacterAdvances); - Assert.Empty(metrics.CharacterSizes); - Assert.Empty(metrics.CharacterBounds); - Assert.Empty(metrics.CharacterRenderableBounds); - Assert.Empty(metrics.Lines); - } - [Fact] public void Measure_EmptyString_ReturnsEmptyMetrics() { @@ -36,14 +19,11 @@ public void Measure_EmptyString_ReturnsEmptyMetrics() Assert.Equal(FontRectangle.Empty, metrics.Advance, Comparer); Assert.Equal(FontRectangle.Empty, metrics.Bounds, Comparer); - Assert.Equal(FontRectangle.Empty, metrics.Size, Comparer); Assert.Equal(FontRectangle.Empty, metrics.RenderableBounds, Comparer); Assert.Equal(0, metrics.LineCount); - Assert.Empty(metrics.CharacterAdvances); - Assert.Empty(metrics.CharacterSizes); - Assert.Empty(metrics.CharacterBounds); - Assert.Empty(metrics.CharacterRenderableBounds); - Assert.Empty(metrics.Lines); + Assert.True(metrics.GetGlyphMetrics().IsEmpty); + Assert.True(metrics.GraphemeMetrics.IsEmpty); + Assert.True(metrics.LineMetrics.IsEmpty); } [Fact] @@ -58,11 +38,10 @@ public void Measure_StringAndSpanOverloads_Match() Assert.Equal(fromString.Advance, fromSpan.Advance, Comparer); Assert.Equal(fromString.Bounds, fromSpan.Bounds, Comparer); - Assert.Equal(fromString.Size, fromSpan.Size, Comparer); Assert.Equal(fromString.RenderableBounds, fromSpan.RenderableBounds, Comparer); Assert.Equal(fromString.LineCount, fromSpan.LineCount); - Assert.Equal(fromString.CharacterAdvances.Count, fromSpan.CharacterAdvances.Count); - Assert.Equal(fromString.Lines.Count, fromSpan.Lines.Count); + Assert.Equal(fromString.GetGlyphMetrics().Length, fromSpan.GetGlyphMetrics().Length); + Assert.Equal(fromString.LineMetrics.Length, fromSpan.LineMetrics.Length); } [Theory] @@ -81,7 +60,6 @@ public void Measure_MatchesGranularRectangles(string text) Assert.Equal(TextMeasurer.MeasureAdvance(text, options), metrics.Advance, Comparer); Assert.Equal(TextMeasurer.MeasureBounds(text, options), metrics.Bounds, Comparer); - Assert.Equal(TextMeasurer.MeasureSize(text, options), metrics.Size, Comparer); Assert.Equal(TextMeasurer.MeasureRenderableBounds(text, options), metrics.RenderableBounds, Comparer); Assert.Equal(TextMeasurer.CountLines(text, options), metrics.LineCount); } @@ -91,112 +69,61 @@ public void Measure_MatchesGranularRectangles(string text) [InlineData("hello")] [InlineData("hello world")] [InlineData("a b\nc")] - public void Measure_CharacterAdvances_MatchGranularOverload(string text) - { - Font font = TextLayoutTests.CreateFont(text); - TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; - - bool hasAdvances = TextMeasurer.TryMeasureCharacterAdvances(text, options, out ReadOnlySpan expected); - TextMetrics metrics = TextMeasurer.Measure(text, options); - - Assert.Equal(hasAdvances, metrics.CharacterAdvances.Any(g => g.Bounds.Width > 0 || g.Bounds.Height > 0)); - AssertGlyphBoundsEqual(expected, metrics.CharacterAdvances); - } - - [Theory] - [InlineData("h")] - [InlineData("hello")] - [InlineData("hello world")] - [InlineData("a b\nc")] - public void Measure_CharacterBounds_MatchGranularOverload(string text) - { - Font font = TextLayoutTests.CreateFont(text); - TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; - - TextMeasurer.TryMeasureCharacterBounds(text, options, out ReadOnlySpan expected); - TextMetrics metrics = TextMeasurer.Measure(text, options); - - AssertGlyphBoundsEqual(expected, metrics.CharacterBounds); - } - - [Theory] - [InlineData("h")] - [InlineData("hello")] - [InlineData("hello world")] - [InlineData("a b\nc")] - public void Measure_CharacterSizes_MatchGranularOverload(string text) + public void Measure_GetGlyphMetrics_MatchGranularOverload(string text) { Font font = TextLayoutTests.CreateFont(text); TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; - TextMeasurer.TryMeasureCharacterSizes(text, options, out ReadOnlySpan expected); + ReadOnlySpan expected = TextMeasurer.GetGlyphMetrics(text, options).Span; TextMetrics metrics = TextMeasurer.Measure(text, options); - AssertGlyphBoundsEqual(expected, metrics.CharacterSizes); - } - - [Theory] - [InlineData("h")] - [InlineData("hello")] - [InlineData("hello world")] - [InlineData("a b\nc")] - public void Measure_CharacterRenderableBounds_MatchGranularOverload(string text) - { - Font font = TextLayoutTests.CreateFont(text); - TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; - - TextMeasurer.TryMeasureCharacterRenderableBounds(text, options, out ReadOnlySpan expected); - TextMetrics metrics = TextMeasurer.Measure(text, options); - - AssertGlyphBoundsEqual(expected, metrics.CharacterRenderableBounds); + AssertGlyphMetricsEqual(expected, metrics.GetGlyphMetrics().Span); } [Theory] [InlineData("hello")] [InlineData("hello\nworld")] [InlineData("hello world\nhello world\nhello")] - public void Measure_Lines_MatchGetLineMetrics(string text) + public void Measure_LineMetrics_MatchGetLineMetrics(string text) { Font font = TextLayoutTests.CreateFont(text); TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; - LineMetrics[] expected = TextMeasurer.GetLineMetrics(text, options); + ReadOnlySpan expected = TextMeasurer.GetLineMetrics(text, options).Span; TextMetrics metrics = TextMeasurer.Measure(text, options); - Assert.Equal(expected.Length, metrics.Lines.Count); + Assert.Equal(expected.Length, metrics.LineMetrics.Length); for (int i = 0; i < expected.Length; i++) { LineMetrics e = expected[i]; - LineMetrics a = metrics.Lines[i]; + LineMetrics a = metrics.LineMetrics[i]; Assert.Equal(e.Ascender, a.Ascender, Comparer); Assert.Equal(e.Baseline, a.Baseline, Comparer); Assert.Equal(e.Descender, a.Descender, Comparer); Assert.Equal(e.LineHeight, a.LineHeight, Comparer); Assert.Equal(e.Start, a.Start, Comparer); Assert.Equal(e.Extent, a.Extent, Comparer); + Assert.Equal(e.StringIndex, a.StringIndex); + Assert.Equal(e.GraphemeIndex, a.GraphemeIndex); + Assert.Equal(e.GraphemeCount, a.GraphemeCount); } } [Fact] - public void Measure_PerCharacterArrays_HaveMatchingLengthAndCodepoints() + public void Measure_GlyphMetrics_ExposeAllRectangles() { const string text = "hello world\nhello"; Font font = TextLayoutTests.CreateFont(text); TextOptions options = new(font) { Dpi = font.FontMetrics.ScaleFactor }; TextMetrics metrics = TextMeasurer.Measure(text, options); + ReadOnlySpan glyphs = metrics.GetGlyphMetrics().Span; - int count = metrics.CharacterAdvances.Count; - Assert.Equal(count, metrics.CharacterSizes.Count); - Assert.Equal(count, metrics.CharacterBounds.Count); - Assert.Equal(count, metrics.CharacterRenderableBounds.Count); + Assert.False(glyphs.IsEmpty); - for (int i = 0; i < count; i++) + for (int i = 0; i < glyphs.Length; i++) { - CodePoint cp = metrics.CharacterAdvances[i].Codepoint; - Assert.Equal(cp, metrics.CharacterSizes[i].Codepoint); - Assert.Equal(cp, metrics.CharacterBounds[i].Codepoint); - Assert.Equal(cp, metrics.CharacterRenderableBounds[i].Codepoint); + Assert.Equal(FontRectangle.Union(glyphs[i].Advance, glyphs[i].Bounds), glyphs[i].RenderableBounds, Comparer); } } @@ -221,17 +148,19 @@ public void Measure_MatchesGranularRectangles_AcrossLayoutModes(LayoutMode layou Assert.Equal(TextMeasurer.MeasureRenderableBounds(text, options), metrics.RenderableBounds, Comparer); } - private static void AssertGlyphBoundsEqual(ReadOnlySpan expected, IReadOnlyList actual) + private static void AssertGlyphMetricsEqual(ReadOnlySpan expected, ReadOnlySpan actual) { - Assert.Equal(expected.Length, actual.Count); + Assert.Equal(expected.Length, actual.Length); for (int i = 0; i < expected.Length; i++) { - GlyphBounds e = expected[i]; - GlyphBounds a = actual[i]; - Assert.Equal(e.Codepoint, a.Codepoint); + GlyphMetrics e = expected[i]; + GlyphMetrics a = actual[i]; + Assert.Equal(e.CodePoint, a.CodePoint); Assert.Equal(e.GraphemeIndex, a.GraphemeIndex); Assert.Equal(e.StringIndex, a.StringIndex); + Assert.Equal(e.Advance, a.Advance, Comparer); Assert.Equal(e.Bounds, a.Bounds, Comparer); + Assert.Equal(e.RenderableBounds, a.RenderableBounds, Comparer); } } } diff --git a/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs b/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs index bae45ecd..ff4fbd87 100644 --- a/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs +++ b/tests/SixLabors.Fonts.Tests/TextOptionsTests.cs @@ -4,6 +4,7 @@ using System.Numerics; using SixLabors.Fonts.Rendering; using SixLabors.Fonts.Tables.AdvancedTypographic; +using SixLabors.Fonts.Unicode; namespace SixLabors.Fonts.Tests; @@ -312,6 +313,11 @@ public void NonDefaultClone() VerticalAlignment = VerticalAlignment.Bottom, DecorationPositioningMode = DecorationPositioningMode.GlyphFont, WrappingLength = 42F, + MaxLines = 2, + TextEllipsis = TextEllipsis.Custom, + CustomEllipsis = new CodePoint('*'), + TextHyphenation = TextHyphenation.Custom, + CustomHyphen = new CodePoint('-'), Tracking = 66F, FeatureTags = new List { KnownFeatureTags.OldstyleFigures } }; @@ -325,6 +331,11 @@ public void NonDefaultClone() Assert.Equal(expected.TabWidth, actual.TabWidth); Assert.Equal(expected.VerticalAlignment, actual.VerticalAlignment); Assert.Equal(expected.WrappingLength, actual.WrappingLength); + Assert.Equal(expected.MaxLines, actual.MaxLines); + Assert.Equal(expected.TextEllipsis, actual.TextEllipsis); + Assert.Equal(expected.CustomEllipsis, actual.CustomEllipsis); + Assert.Equal(expected.TextHyphenation, actual.TextHyphenation); + Assert.Equal(expected.CustomHyphen, actual.CustomHyphen); Assert.Equal(expected.DecorationPositioningMode, actual.DecorationPositioningMode); Assert.Equal(expected.FeatureTags, actual.FeatureTags); Assert.Equal(expected.Tracking, actual.Tracking); @@ -345,6 +356,11 @@ public void CloneIsDeep() TextJustification = TextJustification.InterCharacter, DecorationPositioningMode = DecorationPositioningMode.GlyphFont, WrappingLength = 42F, + MaxLines = 2, + TextEllipsis = TextEllipsis.Custom, + CustomEllipsis = new CodePoint('*'), + TextHyphenation = TextHyphenation.Custom, + CustomHyphen = new CodePoint('-'), Tracking = 66F, }; @@ -355,6 +371,11 @@ public void CloneIsDeep() Assert.NotEqual(expected.TabWidth, actual.TabWidth); Assert.NotEqual(expected.VerticalAlignment, actual.VerticalAlignment); Assert.NotEqual(expected.WrappingLength, actual.WrappingLength); + Assert.NotEqual(expected.MaxLines, actual.MaxLines); + Assert.NotEqual(expected.TextEllipsis, actual.TextEllipsis); + Assert.NotEqual(expected.CustomEllipsis, actual.CustomEllipsis); + Assert.NotEqual(expected.TextHyphenation, actual.TextHyphenation); + Assert.NotEqual(expected.CustomHyphen, actual.CustomHyphen); Assert.NotEqual(expected.DecorationPositioningMode, actual.DecorationPositioningMode); Assert.NotEqual(expected.TextJustification, actual.TextJustification); Assert.NotEqual(expected.Tracking, actual.Tracking); @@ -365,10 +386,15 @@ private static void VerifyPropertyDefault(TextOptions options) Assert.Equal(-1, options.TabWidth); Assert.Equal(KerningMode.Standard, options.KerningMode); Assert.Equal(-1, options.WrappingLength); + Assert.Equal(-1, options.MaxLines); Assert.Equal(HorizontalAlignment.Left, options.HorizontalAlignment); Assert.Equal(VerticalAlignment.Top, options.VerticalAlignment); Assert.Equal(TextAlignment.Start, options.TextAlignment); Assert.Equal(TextJustification.None, options.TextJustification); + Assert.Equal(TextEllipsis.None, options.TextEllipsis); + Assert.Null(options.CustomEllipsis); + Assert.Equal(TextHyphenation.None, options.TextHyphenation); + Assert.Null(options.CustomHyphen); Assert.Equal(TextDirection.Auto, options.TextDirection); Assert.Equal(LayoutMode.HorizontalTopBottom, options.LayoutMode); Assert.Equal(DecorationPositioningMode.PrimaryFont, options.DecorationPositioningMode); diff --git a/tests/SixLabors.Fonts.Tests/TrueTypeCollectionTests.cs b/tests/SixLabors.Fonts.Tests/TrueTypeCollectionTests.cs index 17a86838..347d636f 100644 --- a/tests/SixLabors.Fonts.Tests/TrueTypeCollectionTests.cs +++ b/tests/SixLabors.Fonts.Tests/TrueTypeCollectionTests.cs @@ -11,22 +11,24 @@ public class TrueTypeCollectionTests public void AddViaPathReturnsDescription() { FontCollection suit = new(); - IEnumerable collectionFromPath = suit.AddCollection(TestFonts.SimpleTrueTypeCollection, out IEnumerable descriptions); + ReadOnlyMemory collectionFromPath = suit.AddCollection(TestFonts.SimpleTrueTypeCollection, out ReadOnlyMemory descriptions); + FontFamily[] families = collectionFromPath.ToArray(); + FontDescription[] descriptionArray = descriptions.ToArray(); - Assert.Equal(2, descriptions.Count()); - FontFamily openSans = Assert.Single(collectionFromPath, x => x.Name == "Open Sans"); - FontFamily abFont = Assert.Single(collectionFromPath, x => x.Name == "SixLaborsSampleAB"); + Assert.Equal(2, descriptions.Length); + FontFamily openSans = Assert.Single(families, x => x.Name == "Open Sans"); + FontFamily abFont = Assert.Single(families, x => x.Name == "SixLaborsSampleAB"); - Assert.Equal(2, descriptions.Count()); - FontDescription openSansDescription = Assert.Single(descriptions, x => x.FontNameInvariantCulture == "Open Sans"); - FontDescription abFontDescription = Assert.Single(descriptions, x => x.FontNameInvariantCulture == "SixLaborsSampleAB regular"); + Assert.Equal(2, descriptions.Length); + FontDescription openSansDescription = Assert.Single(descriptionArray, x => x.FontNameInvariantCulture == "Open Sans"); + FontDescription abFontDescription = Assert.Single(descriptionArray, x => x.FontNameInvariantCulture == "SixLaborsSampleAB regular"); } [Fact] public void AddViaPathAddFontFileInstances() { FontCollection sut = new(); - IEnumerable collectionFromPath = sut.AddCollection(TestFonts.SimpleTrueTypeCollection, out IEnumerable descriptions); + _ = sut.AddCollection(TestFonts.SimpleTrueTypeCollection, out _); IEnumerable allInstances = sut.Families.SelectMany(x => ((IReadOnlyFontMetricsCollection)sut).GetAllMetrics(x.Name, CultureInfo.InvariantCulture)); @@ -40,14 +42,16 @@ public void AddViaPathAddFontFileInstances() public void AddViaStreamReturnsDescription() { FontCollection suit = new(); - IEnumerable collectionFromPath = suit.AddCollection(TestFonts.SSimpleTrueTypeCollectionData(), out IEnumerable descriptions); + ReadOnlyMemory collectionFromPath = suit.AddCollection(TestFonts.SSimpleTrueTypeCollectionData(), out ReadOnlyMemory descriptions); + FontFamily[] families = collectionFromPath.ToArray(); + FontDescription[] descriptionArray = descriptions.ToArray(); - Assert.Equal(2, collectionFromPath.Count()); - FontFamily openSans = Assert.Single(collectionFromPath, x => x.Name == "Open Sans"); - FontFamily abFont = Assert.Single(collectionFromPath, x => x.Name == "SixLaborsSampleAB"); + Assert.Equal(2, collectionFromPath.Length); + FontFamily openSans = Assert.Single(families, x => x.Name == "Open Sans"); + FontFamily abFont = Assert.Single(families, x => x.Name == "SixLaborsSampleAB"); - Assert.Equal(2, descriptions.Count()); - FontDescription openSansDescription = Assert.Single(descriptions, x => x.FontNameInvariantCulture == "Open Sans"); - FontDescription abFontDescription = Assert.Single(descriptions, x => x.FontNameInvariantCulture == "SixLaborsSampleAB regular"); + Assert.Equal(2, descriptions.Length); + FontDescription openSansDescription = Assert.Single(descriptionArray, x => x.FontNameInvariantCulture == "Open Sans"); + FontDescription abFontDescription = Assert.Single(descriptionArray, x => x.FontNameInvariantCulture == "SixLaborsSampleAB regular"); } } diff --git a/tests/SixLabors.Fonts.Tests/Unicode/LineBreakEnumeratorTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/LineBreakEnumeratorTests.cs index 903ca8c6..6efe04a2 100644 --- a/tests/SixLabors.Fonts.Tests/Unicode/LineBreakEnumeratorTests.cs +++ b/tests/SixLabors.Fonts.Tests/Unicode/LineBreakEnumeratorTests.cs @@ -91,6 +91,32 @@ public void NonNumericSolidusKeepsDefaultBreakAfter() Assert.Contains(breaks, x => x.PositionWrap == 2); } + [Fact] + public void SoftHyphenBreakIsMarkedForLayoutTailoring() + { + const string text = "extra\u00ADordinary"; + List breaks = [.. new LineBreakEnumerator(text.AsSpan())]; + + int hyphenationBreaks = 0; + LineBreak hyphenationBreak = default; + for (int i = 0; i < breaks.Count; i++) + { + if (breaks[i].IsHyphenationBreak) + { + hyphenationBreaks++; + hyphenationBreak = breaks[i]; + } + } + + // UAX #14 exposes U+00AD as a discretionary break, but layout needs to + // distinguish it from ordinary opportunities so it can decide whether to + // materialize a visible marker for the chosen break. + Assert.Equal(1, hyphenationBreaks); + Assert.False(hyphenationBreak.Required); + Assert.Equal(6, hyphenationBreak.PositionMeasure); + Assert.Equal(6, hyphenationBreak.PositionWrap); + } + [Fact] public void ForwardTextWithOuterWhitespace() { diff --git a/tests/SixLabors.Fonts.Tests/Unicode/WordBreakEnumeratorTests.cs b/tests/SixLabors.Fonts.Tests/Unicode/WordBreakEnumeratorTests.cs new file mode 100644 index 00000000..70b6616d --- /dev/null +++ b/tests/SixLabors.Fonts.Tests/Unicode/WordBreakEnumeratorTests.cs @@ -0,0 +1,180 @@ +// Copyright (c) Six Labors. +// Licensed under the Six Labors Split License. + +using System.Runtime.InteropServices; +using System.Text; +using SixLabors.Fonts.Unicode; +using Xunit.Abstractions; + +namespace SixLabors.Fonts.Tests.Unicode; + +public class WordBreakEnumeratorTests +{ + private readonly ITestOutputHelper output; + + public WordBreakEnumeratorTests(ITestOutputHelper output) => this.output = output; + + [Fact] + public void EnumerateWordSegments_ReturnsDefaultWordBoundarySegments() + { + string text = "can't stop"; + List segments = []; + + // The apostrophe stays inside the word because UAX #29 treats it as + // mid-letter punctuation when letters appear on both sides. + foreach (WordSegment segment in text.AsSpan().EnumerateWordSegments()) + { + segments.Add(segment.Span.ToString()); + } + + Assert.Equal(["can't", " ", "stop"], segments); + } + + [Fact] + public void EnumerateWordSegments_KeepsEmojiZwjSequenceTogether() + { + string text = "a👩🏽‍🚒b"; + List segments = []; + + // WB3c keeps the emoji ZWJ sequence together as one word-boundary segment. + foreach (WordSegment segment in text.AsSpan().EnumerateWordSegments()) + { + segments.Add(segment.Span.ToString()); + } + + Assert.Equal(["a", "👩🏽‍🚒", "b"], segments); + } + + [Fact] + public void ICUTests() => Assert.True(this.ICUTestsImpl()); + + public bool ICUTestsImpl() + { + this.output.WriteLine("Word Break Tests"); + this.output.WriteLine("----------------"); + + string[] lines = File.ReadAllLines(Path.Combine(TestEnvironment.UnicodeTestDataFullPath, "WordBreakTest.txt")); + + List tests = []; + for (int lineNumber = 1; lineNumber < lines.Length + 1; lineNumber++) + { + string line = lines[lineNumber - 1].Split('#')[0].Trim(); + + if (string.IsNullOrWhiteSpace(line)) + { + continue; + } + + List codePoints = []; + List breakPoints = []; + + // The Unicode test file represents expected boundaries with ÷ and + // non-boundaries with ×, so the parser records only scalar values + // and the code point offsets where a break must be produced. + int p = 0; + while (p < line.Length) + { + if (char.IsWhiteSpace(line[p])) + { + p++; + continue; + } + + if (line[p] == '×') + { + p++; + continue; + } + + if (line[p] == '÷') + { + breakPoints.Add(codePoints.Count); + p++; + continue; + } + + int codePointPos = p; + while (p < line.Length && IsHexDigit(line[p])) + { + p++; + } + + string codePointStr = line[codePointPos..p]; + uint codePoint = Convert.ToUInt32(codePointStr, 16); + codePoints.Add(codePoint); + } + + Test test = new(lineNumber, [.. codePoints], [.. breakPoints]); + tests.Add(test); + } + + List foundBreaks = []; + + for (int testNumber = 0; testNumber < tests.Count; testNumber++) + { + Test t = tests[testNumber]; + + foundBreaks.Clear(); + + string text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); + + int boundary = 0; + foundBreaks.Add(boundary); + + foreach (WordSegment segment in text.AsSpan().EnumerateWordSegments()) + { + boundary += segment.CodePointCount; + foundBreaks.Add(boundary); + } + + bool pass = true; + if (foundBreaks.Count != t.BreakPoints.Length) + { + pass = false; + } + else + { + for (int i = 0; i < foundBreaks.Count; i++) + { + if (foundBreaks[i] != t.BreakPoints[i]) + { + pass = false; + } + } + } + + if (!pass) + { + WordBreakClass[] classes = [.. t.CodePoints.Select(UnicodeData.GetWordBreakClass)]; + + this.output.WriteLine($"Failed test on line {t.LineNumber}"); + this.output.WriteLine($" Code Points: {string.Join(" ", t.CodePoints)}"); + this.output.WriteLine($"Expected Breaks: {string.Join(" ", t.BreakPoints)}"); + this.output.WriteLine($" Actual Breaks: {string.Join(" ", foundBreaks)}"); + this.output.WriteLine($" Char Props: {string.Join(" ", classes)}"); + + return false; + } + } + + return true; + } + + private static bool IsHexDigit(char ch) => char.IsDigit(ch) || (ch >= 'A' && ch <= 'F') || (ch >= 'a' && ch <= 'f'); + + private sealed class Test + { + public Test(int lineNumber, uint[] codePoints, int[] breakPoints) + { + this.LineNumber = lineNumber; + this.CodePoints = codePoints; + this.BreakPoints = breakPoints; + } + + public int LineNumber { get; } + + public uint[] CodePoints { get; } + + public int[] BreakPoints { get; } + } +} diff --git a/tests/UnicodeTestData/README.md b/tests/UnicodeTestData/README.md index da613e9b..b6363230 100644 --- a/tests/UnicodeTestData/README.md +++ b/tests/UnicodeTestData/README.md @@ -1,2 +1,3 @@ -Test data downloaded from +Test data downloaded from https://www.unicode.org/Public/17.0.0/ucd/ +https://www.unicode.org/Public/17.0.0/ucd/auxiliary/ diff --git a/tests/UnicodeTestData/WordBreakTest.txt b/tests/UnicodeTestData/WordBreakTest.txt new file mode 100644 index 00000000..042b02e7 --- /dev/null +++ b/tests/UnicodeTestData/WordBreakTest.txt @@ -0,0 +1,1974 @@ +# WordBreakTest-17.0.0.txt +# Date: 2025-03-24, 14:46:35 GMT +# © 2025 Unicode®, Inc. +# Unicode and the Unicode Logo are registered trademarks of Unicode, Inc. in the U.S. and other countries. +# For terms of use and license, see https://www.unicode.org/terms_of_use.html +# +# Unicode Character Database +# For documentation, see https://www.unicode.org/reports/tr44/ +# +# Default Word_Break Test +# +# Format: +# (# )? +# contains hex Unicode code points, with +# ÷ wherever there is a break opportunity, and +# × wherever there is not. +# the format can change, but currently it shows: +# - the sample character name +# - (x) the Word_Break property value for the sample character and +# any other properties relevant to the algorithm, as described in +# WordBreakTest.html +# - [x] the rule that determines whether there is a break or not, +# as listed in the Rules section of WordBreakTest.html +# +# These samples may be extended or changed in the future. +# +÷ 000D ÷ 000D ÷ # ÷ [0.2] (CR) ÷ [3.1] (CR) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 000D ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 000D × 000A ÷ # ÷ [0.2] (CR) × [3.0] (LF) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 000A ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 000D ÷ 000B ÷ # ÷ [0.2] (CR) ÷ [3.1] (Newline) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 000B ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 000D ÷ 0300 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 000D ÷ 0308 × 0300 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 000D ÷ 00AD ÷ # ÷ [0.2] (CR) ÷ [3.1] SOFT HYPHEN (Format) ÷ [0.3] +÷ 000D ÷ 0308 × 00AD ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 000D ÷ 3031 ÷ # ÷ [0.2] (CR) ÷ [3.1] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 3031 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 000D ÷ 24C2 ÷ # ÷ [0.2] (CR) ÷ [3.1] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 24C2 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 000D ÷ 0041 ÷ # ÷ [0.2] (CR) ÷ [3.1] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0041 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 000D ÷ 003A ÷ # ÷ [0.2] (CR) ÷ [3.1] COLON (MidLetter) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 003A ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000D ÷ 002C ÷ # ÷ [0.2] (CR) ÷ [3.1] COMMA (MidNum) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 002C ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000D ÷ 002E ÷ # ÷ [0.2] (CR) ÷ [3.1] FULL STOP (MidNumLet) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 002E ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 000D ÷ 0030 ÷ # ÷ [0.2] (CR) ÷ [3.1] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0030 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 000D ÷ 005F ÷ # ÷ [0.2] (CR) ÷ [3.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 005F ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 000D ÷ 1F1E6 ÷ # ÷ [0.2] (CR) ÷ [3.1] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 1F1E6 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 000D ÷ 05D0 ÷ # ÷ [0.2] (CR) ÷ [3.1] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 05D0 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 000D ÷ 0022 ÷ # ÷ [0.2] (CR) ÷ [3.1] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0022 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 000D ÷ 0027 ÷ # ÷ [0.2] (CR) ÷ [3.1] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0027 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000D ÷ 200D ÷ # ÷ [0.2] (CR) ÷ [3.1] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 000D ÷ 0308 × 200D ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 000D ÷ 00A9 ÷ # ÷ [0.2] (CR) ÷ [3.1] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 00A9 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 000D ÷ 0020 ÷ # ÷ [0.2] (CR) ÷ [3.1] SPACE (WSegSpace) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0020 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 000D ÷ 0000 ÷ # ÷ [0.2] (CR) ÷ [3.1] (XXmExtPict) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0000 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 000D ÷ 0061 × 2060 ÷ # ÷ [0.2] (CR) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000D ÷ 0061 ÷ 003A ÷ # ÷ [0.2] (CR) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000D ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] (CR) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000D ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] (CR) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000D ÷ 0061 ÷ 002C ÷ # ÷ [0.2] (CR) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000D ÷ 0031 ÷ 003A ÷ # ÷ [0.2] (CR) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000D ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] (CR) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000D ÷ 0031 ÷ 002C ÷ # ÷ [0.2] (CR) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000D ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] (CR) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000D ÷ 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] (CR) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000A ÷ 000D ÷ # ÷ [0.2] (LF) ÷ [3.1] (CR) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 000D ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 000A ÷ 000A ÷ # ÷ [0.2] (LF) ÷ [3.1] (LF) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 000A ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 000A ÷ 000B ÷ # ÷ [0.2] (LF) ÷ [3.1] (Newline) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 000B ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 000A ÷ 0300 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 000A ÷ 0308 × 0300 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 000A ÷ 00AD ÷ # ÷ [0.2] (LF) ÷ [3.1] SOFT HYPHEN (Format) ÷ [0.3] +÷ 000A ÷ 0308 × 00AD ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 000A ÷ 3031 ÷ # ÷ [0.2] (LF) ÷ [3.1] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 3031 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 000A ÷ 24C2 ÷ # ÷ [0.2] (LF) ÷ [3.1] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 24C2 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 000A ÷ 0041 ÷ # ÷ [0.2] (LF) ÷ [3.1] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0041 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 000A ÷ 003A ÷ # ÷ [0.2] (LF) ÷ [3.1] COLON (MidLetter) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 003A ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000A ÷ 002C ÷ # ÷ [0.2] (LF) ÷ [3.1] COMMA (MidNum) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 002C ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000A ÷ 002E ÷ # ÷ [0.2] (LF) ÷ [3.1] FULL STOP (MidNumLet) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 002E ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 000A ÷ 0030 ÷ # ÷ [0.2] (LF) ÷ [3.1] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0030 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 000A ÷ 005F ÷ # ÷ [0.2] (LF) ÷ [3.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 005F ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 000A ÷ 1F1E6 ÷ # ÷ [0.2] (LF) ÷ [3.1] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 1F1E6 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 000A ÷ 05D0 ÷ # ÷ [0.2] (LF) ÷ [3.1] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 05D0 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 000A ÷ 0022 ÷ # ÷ [0.2] (LF) ÷ [3.1] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0022 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 000A ÷ 0027 ÷ # ÷ [0.2] (LF) ÷ [3.1] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0027 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000A ÷ 200D ÷ # ÷ [0.2] (LF) ÷ [3.1] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 000A ÷ 0308 × 200D ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 000A ÷ 00A9 ÷ # ÷ [0.2] (LF) ÷ [3.1] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 00A9 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 000A ÷ 0020 ÷ # ÷ [0.2] (LF) ÷ [3.1] SPACE (WSegSpace) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0020 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 000A ÷ 0000 ÷ # ÷ [0.2] (LF) ÷ [3.1] (XXmExtPict) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0000 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 000A ÷ 0061 × 2060 ÷ # ÷ [0.2] (LF) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000A ÷ 0061 ÷ 003A ÷ # ÷ [0.2] (LF) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000A ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] (LF) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000A ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] (LF) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000A ÷ 0061 ÷ 002C ÷ # ÷ [0.2] (LF) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000A ÷ 0031 ÷ 003A ÷ # ÷ [0.2] (LF) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000A ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] (LF) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000A ÷ 0031 ÷ 002C ÷ # ÷ [0.2] (LF) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000A ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] (LF) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000A ÷ 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000B ÷ 000D ÷ # ÷ [0.2] (Newline) ÷ [3.1] (CR) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 000D ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 000B ÷ 000A ÷ # ÷ [0.2] (Newline) ÷ [3.1] (LF) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 000A ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 000B ÷ 000B ÷ # ÷ [0.2] (Newline) ÷ [3.1] (Newline) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 000B ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 000B ÷ 0300 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 000B ÷ 0308 × 0300 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 000B ÷ 00AD ÷ # ÷ [0.2] (Newline) ÷ [3.1] SOFT HYPHEN (Format) ÷ [0.3] +÷ 000B ÷ 0308 × 00AD ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 000B ÷ 3031 ÷ # ÷ [0.2] (Newline) ÷ [3.1] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 3031 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 000B ÷ 24C2 ÷ # ÷ [0.2] (Newline) ÷ [3.1] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 24C2 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 000B ÷ 0041 ÷ # ÷ [0.2] (Newline) ÷ [3.1] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0041 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 000B ÷ 003A ÷ # ÷ [0.2] (Newline) ÷ [3.1] COLON (MidLetter) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 003A ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000B ÷ 002C ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMMA (MidNum) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 002C ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000B ÷ 002E ÷ # ÷ [0.2] (Newline) ÷ [3.1] FULL STOP (MidNumLet) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 002E ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 000B ÷ 0030 ÷ # ÷ [0.2] (Newline) ÷ [3.1] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0030 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 000B ÷ 005F ÷ # ÷ [0.2] (Newline) ÷ [3.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 005F ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 000B ÷ 1F1E6 ÷ # ÷ [0.2] (Newline) ÷ [3.1] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 1F1E6 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 000B ÷ 05D0 ÷ # ÷ [0.2] (Newline) ÷ [3.1] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 05D0 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 000B ÷ 0022 ÷ # ÷ [0.2] (Newline) ÷ [3.1] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0022 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 000B ÷ 0027 ÷ # ÷ [0.2] (Newline) ÷ [3.1] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0027 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000B ÷ 200D ÷ # ÷ [0.2] (Newline) ÷ [3.1] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 000B ÷ 0308 × 200D ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 000B ÷ 00A9 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 00A9 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 000B ÷ 0020 ÷ # ÷ [0.2] (Newline) ÷ [3.1] SPACE (WSegSpace) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0020 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 000B ÷ 0000 ÷ # ÷ [0.2] (Newline) ÷ [3.1] (XXmExtPict) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0000 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 000B ÷ 0061 × 2060 ÷ # ÷ [0.2] (Newline) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000B ÷ 0061 ÷ 003A ÷ # ÷ [0.2] (Newline) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000B ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] (Newline) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000B ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] (Newline) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000B ÷ 0061 ÷ 002C ÷ # ÷ [0.2] (Newline) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000B ÷ 0031 ÷ 003A ÷ # ÷ [0.2] (Newline) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 000B ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] (Newline) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 000B ÷ 0031 ÷ 002C ÷ # ÷ [0.2] (Newline) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 000B ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] (Newline) ÷ [3.1] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000B ÷ 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] (Newline) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0300 ÷ 000D ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0300 × 0308 ÷ 000D ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0300 ÷ 000A ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0300 × 0308 ÷ 000A ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0300 ÷ 000B ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0300 × 0308 ÷ 000B ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0300 × 0300 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0300 × 0308 × 0300 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0300 × 00AD ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0300 × 0308 × 00AD ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0300 ÷ 3031 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0300 × 0308 ÷ 3031 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0300 ÷ 24C2 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0300 × 0308 ÷ 24C2 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0300 ÷ 0041 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0300 × 0308 ÷ 0041 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0300 ÷ 003A ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0300 × 0308 ÷ 003A ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0300 ÷ 002C ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0300 × 0308 ÷ 002C ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0300 ÷ 002E ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0300 × 0308 ÷ 002E ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0300 ÷ 0030 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0300 × 0308 ÷ 0030 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0300 ÷ 005F ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0300 × 0308 ÷ 005F ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0300 ÷ 1F1E6 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0300 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0300 ÷ 05D0 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0300 × 0308 ÷ 05D0 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0300 ÷ 0022 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0300 × 0308 ÷ 0022 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0300 ÷ 0027 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0300 × 0308 ÷ 0027 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0300 × 200D ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0300 × 0308 × 200D ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0300 ÷ 00A9 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0300 × 0308 ÷ 00A9 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0300 ÷ 0020 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0300 × 0308 ÷ 0020 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0300 ÷ 0000 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0300 × 0308 ÷ 0000 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0300 ÷ 0061 × 2060 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0300 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0300 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0300 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0300 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0300 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0300 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0300 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0300 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0300 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0300 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0300 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0300 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0300 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0300 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0300 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0300 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0300 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] COMBINING GRAVE ACCENT (Extend) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00AD ÷ 000D ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [3.2] (CR) ÷ [0.3] +÷ 00AD × 0308 ÷ 000D ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 00AD ÷ 000A ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [3.2] (LF) ÷ [0.3] +÷ 00AD × 0308 ÷ 000A ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 00AD ÷ 000B ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [3.2] (Newline) ÷ [0.3] +÷ 00AD × 0308 ÷ 000B ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 00AD × 0300 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 00AD × 0308 × 0300 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 00AD × 00AD ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 00AD × 0308 × 00AD ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 00AD ÷ 3031 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 00AD × 0308 ÷ 3031 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 00AD ÷ 24C2 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 00AD × 0308 ÷ 24C2 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 00AD ÷ 0041 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 00AD × 0308 ÷ 0041 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 00AD ÷ 003A ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00AD × 0308 ÷ 003A ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00AD ÷ 002C ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00AD × 0308 ÷ 002C ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00AD ÷ 002E ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 00AD × 0308 ÷ 002E ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 00AD ÷ 0030 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 00AD × 0308 ÷ 0030 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 00AD ÷ 005F ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 00AD × 0308 ÷ 005F ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 00AD ÷ 1F1E6 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 00AD × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 00AD ÷ 05D0 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 00AD × 0308 ÷ 05D0 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 00AD ÷ 0022 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 00AD × 0308 ÷ 0022 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 00AD ÷ 0027 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00AD × 0308 ÷ 0027 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00AD × 200D ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 00AD × 0308 × 200D ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 00AD ÷ 00A9 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 00AD × 0308 ÷ 00A9 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 00AD ÷ 0020 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 00AD × 0308 ÷ 0020 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 00AD ÷ 0000 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 00AD × 0308 ÷ 0000 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 00AD ÷ 0061 × 2060 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00AD × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00AD ÷ 0061 ÷ 003A ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00AD × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00AD ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00AD × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00AD ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00AD × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00AD ÷ 0061 ÷ 002C ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00AD × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00AD ÷ 0031 ÷ 003A ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00AD × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00AD ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00AD × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00AD ÷ 0031 ÷ 002C ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00AD × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00AD ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] SOFT HYPHEN (Format) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00AD × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] SOFT HYPHEN (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 3031 ÷ 000D ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [3.2] (CR) ÷ [0.3] +÷ 3031 × 0308 ÷ 000D ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 3031 ÷ 000A ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [3.2] (LF) ÷ [0.3] +÷ 3031 × 0308 ÷ 000A ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 3031 ÷ 000B ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [3.2] (Newline) ÷ [0.3] +÷ 3031 × 0308 ÷ 000B ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 3031 × 0300 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 3031 × 0308 × 0300 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 3031 × 00AD ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 3031 × 0308 × 00AD ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 3031 × 3031 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [13.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 3031 × 0308 × 3031 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) × [13.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 3031 ÷ 24C2 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 3031 × 0308 ÷ 24C2 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 3031 ÷ 0041 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 3031 × 0308 ÷ 0041 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 3031 ÷ 003A ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 3031 × 0308 ÷ 003A ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 3031 ÷ 002C ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 3031 × 0308 ÷ 002C ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 3031 ÷ 002E ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 3031 × 0308 ÷ 002E ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 3031 ÷ 0030 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 3031 × 0308 ÷ 0030 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 3031 × 005F ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 3031 × 0308 × 005F ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 3031 ÷ 1F1E6 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 3031 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 3031 ÷ 05D0 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 3031 × 0308 ÷ 05D0 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 3031 ÷ 0022 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 3031 × 0308 ÷ 0022 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 3031 ÷ 0027 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 3031 × 0308 ÷ 0027 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 3031 × 200D ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 3031 × 0308 × 200D ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 3031 ÷ 00A9 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 3031 × 0308 ÷ 00A9 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 3031 ÷ 0020 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 3031 × 0308 ÷ 0020 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 3031 ÷ 0000 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 3031 × 0308 ÷ 0000 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 3031 ÷ 0061 × 2060 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 3031 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 3031 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 3031 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 3031 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 3031 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 3031 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 3031 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 3031 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 3031 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 3031 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 3031 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 3031 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 3031 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 3031 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 3031 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 3031 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 3031 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 24C2 ÷ 000D ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [3.2] (CR) ÷ [0.3] +÷ 24C2 × 0308 ÷ 000D ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 24C2 ÷ 000A ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [3.2] (LF) ÷ [0.3] +÷ 24C2 × 0308 ÷ 000A ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 24C2 ÷ 000B ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [3.2] (Newline) ÷ [0.3] +÷ 24C2 × 0308 ÷ 000B ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 24C2 × 0300 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 24C2 × 0308 × 0300 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 24C2 × 00AD ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 24C2 × 0308 × 00AD ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 24C2 ÷ 3031 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 24C2 × 0308 ÷ 3031 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 24C2 × 24C2 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [5.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 24C2 × 0308 × 24C2 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 24C2 × 0041 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 24C2 × 0308 × 0041 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 24C2 ÷ 003A ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 24C2 × 0308 ÷ 003A ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 24C2 ÷ 002C ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 24C2 × 0308 ÷ 002C ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 24C2 ÷ 002E ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 24C2 × 0308 ÷ 002E ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 24C2 × 0030 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [9.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 24C2 × 0308 × 0030 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 24C2 × 005F ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 24C2 × 0308 × 005F ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 24C2 ÷ 1F1E6 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 24C2 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 24C2 × 05D0 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [5.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 24C2 × 0308 × 05D0 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 24C2 ÷ 0022 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 24C2 × 0308 ÷ 0022 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 24C2 ÷ 0027 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 24C2 × 0308 ÷ 0027 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 24C2 × 200D ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 24C2 × 0308 × 200D ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 24C2 ÷ 00A9 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 24C2 × 0308 ÷ 00A9 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 24C2 ÷ 0020 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 24C2 × 0308 ÷ 0020 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 24C2 ÷ 0000 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 24C2 × 0308 ÷ 0000 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 24C2 × 0061 × 2060 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 24C2 × 0308 × 0061 × 2060 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 24C2 × 0061 ÷ 003A ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 24C2 × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 24C2 × 0061 ÷ 0027 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 24C2 × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 24C2 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 24C2 × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 24C2 × 0061 ÷ 002C ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 24C2 × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 24C2 × 0031 ÷ 003A ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 24C2 × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 24C2 × 0031 ÷ 0027 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 24C2 × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 24C2 × 0031 ÷ 002C ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 24C2 × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 24C2 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 24C2 × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0041 ÷ 000D ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [3.2] (CR) ÷ [0.3] +÷ 0041 × 0308 ÷ 000D ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0041 ÷ 000A ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [3.2] (LF) ÷ [0.3] +÷ 0041 × 0308 ÷ 000A ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0041 ÷ 000B ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0041 × 0308 ÷ 000B ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0041 × 0300 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0041 × 0308 × 0300 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0041 × 00AD ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0041 × 0308 × 00AD ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0041 ÷ 3031 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0041 × 0308 ÷ 3031 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0041 × 24C2 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0041 × 0308 × 24C2 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0041 × 0041 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0041 × 0308 × 0041 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0041 ÷ 003A ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0041 × 0308 ÷ 003A ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0041 ÷ 002C ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0041 × 0308 ÷ 002C ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0041 ÷ 002E ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0041 × 0308 ÷ 002E ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0041 × 0030 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [9.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0041 × 0308 × 0030 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0041 × 005F ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0041 × 0308 × 005F ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0041 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0041 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0041 × 05D0 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0041 × 0308 × 05D0 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0041 ÷ 0022 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0041 × 0308 ÷ 0022 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0041 ÷ 0027 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0041 × 0308 ÷ 0027 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0041 × 200D ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0041 × 0308 × 200D ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0041 ÷ 00A9 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0041 × 0308 ÷ 00A9 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0041 ÷ 0020 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0041 × 0308 ÷ 0020 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0041 ÷ 0000 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0041 × 0308 ÷ 0000 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0041 × 0061 × 2060 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0041 × 0308 × 0061 × 2060 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0041 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0041 × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0041 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0041 × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0041 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0041 × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0041 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0041 × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0041 × 0031 ÷ 003A ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0041 × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0041 × 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0041 × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0041 × 0031 ÷ 002C ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0041 × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0041 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0041 × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 003A ÷ 000D ÷ # ÷ [0.2] COLON (MidLetter) ÷ [3.2] (CR) ÷ [0.3] +÷ 003A × 0308 ÷ 000D ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 003A ÷ 000A ÷ # ÷ [0.2] COLON (MidLetter) ÷ [3.2] (LF) ÷ [0.3] +÷ 003A × 0308 ÷ 000A ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 003A ÷ 000B ÷ # ÷ [0.2] COLON (MidLetter) ÷ [3.2] (Newline) ÷ [0.3] +÷ 003A × 0308 ÷ 000B ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 003A × 0300 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 003A × 0308 × 0300 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 003A × 00AD ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 003A × 0308 × 00AD ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 003A ÷ 3031 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 003A × 0308 ÷ 3031 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 003A ÷ 24C2 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 003A × 0308 ÷ 24C2 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 003A ÷ 0041 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 003A × 0308 ÷ 0041 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 003A ÷ 003A ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 003A × 0308 ÷ 003A ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 003A ÷ 002C ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 003A × 0308 ÷ 002C ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 003A ÷ 002E ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 003A × 0308 ÷ 002E ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 003A ÷ 0030 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 003A × 0308 ÷ 0030 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 003A ÷ 005F ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 003A × 0308 ÷ 005F ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 003A ÷ 1F1E6 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 003A × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 003A ÷ 05D0 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 003A × 0308 ÷ 05D0 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 003A ÷ 0022 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 003A × 0308 ÷ 0022 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 003A ÷ 0027 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 003A × 0308 ÷ 0027 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 003A × 200D ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 003A × 0308 × 200D ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 003A ÷ 00A9 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 003A × 0308 ÷ 00A9 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 003A ÷ 0020 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 003A × 0308 ÷ 0020 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 003A ÷ 0000 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 003A × 0308 ÷ 0000 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 003A ÷ 0061 × 2060 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 003A × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 003A ÷ 0061 ÷ 003A ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 003A × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 003A ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 003A × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 003A ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 003A × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 003A ÷ 0061 ÷ 002C ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 003A × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 003A ÷ 0031 ÷ 003A ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 003A × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 003A ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 003A × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 003A ÷ 0031 ÷ 002C ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 003A × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 003A ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 003A × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002C ÷ 000D ÷ # ÷ [0.2] COMMA (MidNum) ÷ [3.2] (CR) ÷ [0.3] +÷ 002C × 0308 ÷ 000D ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 002C ÷ 000A ÷ # ÷ [0.2] COMMA (MidNum) ÷ [3.2] (LF) ÷ [0.3] +÷ 002C × 0308 ÷ 000A ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 002C ÷ 000B ÷ # ÷ [0.2] COMMA (MidNum) ÷ [3.2] (Newline) ÷ [0.3] +÷ 002C × 0308 ÷ 000B ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 002C × 0300 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 002C × 0308 × 0300 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 002C × 00AD ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 002C × 0308 × 00AD ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 002C ÷ 3031 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 002C × 0308 ÷ 3031 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 002C ÷ 24C2 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 002C × 0308 ÷ 24C2 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 002C ÷ 0041 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 002C × 0308 ÷ 0041 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 002C ÷ 003A ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002C × 0308 ÷ 003A ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002C ÷ 002C ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002C × 0308 ÷ 002C ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002C ÷ 002E ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 002C × 0308 ÷ 002E ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 002C ÷ 0030 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 002C × 0308 ÷ 0030 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 002C ÷ 005F ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 002C × 0308 ÷ 005F ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 002C ÷ 1F1E6 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 002C × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 002C ÷ 05D0 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 002C × 0308 ÷ 05D0 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 002C ÷ 0022 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 002C × 0308 ÷ 0022 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 002C ÷ 0027 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002C × 0308 ÷ 0027 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002C × 200D ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 002C × 0308 × 200D ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 002C ÷ 00A9 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 002C × 0308 ÷ 00A9 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 002C ÷ 0020 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 002C × 0308 ÷ 0020 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 002C ÷ 0000 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 002C × 0308 ÷ 0000 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 002C ÷ 0061 × 2060 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002C × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002C ÷ 0061 ÷ 003A ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002C × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002C ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002C × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002C ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002C × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002C ÷ 0061 ÷ 002C ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002C × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002C ÷ 0031 ÷ 003A ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002C × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002C ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002C × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002C ÷ 0031 ÷ 002C ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002C × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002C ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002C × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002E ÷ 000D ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [3.2] (CR) ÷ [0.3] +÷ 002E × 0308 ÷ 000D ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 002E ÷ 000A ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [3.2] (LF) ÷ [0.3] +÷ 002E × 0308 ÷ 000A ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 002E ÷ 000B ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [3.2] (Newline) ÷ [0.3] +÷ 002E × 0308 ÷ 000B ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 002E × 0300 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 002E × 0308 × 0300 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 002E × 00AD ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 002E × 0308 × 00AD ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 002E ÷ 3031 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 002E × 0308 ÷ 3031 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 002E ÷ 24C2 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 002E × 0308 ÷ 24C2 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 002E ÷ 0041 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 002E × 0308 ÷ 0041 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 002E ÷ 003A ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002E × 0308 ÷ 003A ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002E ÷ 002C ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002E × 0308 ÷ 002C ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002E ÷ 002E ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 002E × 0308 ÷ 002E ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 002E ÷ 0030 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 002E × 0308 ÷ 0030 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 002E ÷ 005F ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 002E × 0308 ÷ 005F ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 002E ÷ 1F1E6 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 002E × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 002E ÷ 05D0 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 002E × 0308 ÷ 05D0 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 002E ÷ 0022 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 002E × 0308 ÷ 0022 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 002E ÷ 0027 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002E × 0308 ÷ 0027 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002E × 200D ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 002E × 0308 × 200D ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 002E ÷ 00A9 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 002E × 0308 ÷ 00A9 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 002E ÷ 0020 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 002E × 0308 ÷ 0020 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 002E ÷ 0000 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 002E × 0308 ÷ 0000 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 002E ÷ 0061 × 2060 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002E × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002E ÷ 0061 ÷ 003A ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002E × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002E ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002E × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002E ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002E × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002E ÷ 0061 ÷ 002C ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002E × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002E ÷ 0031 ÷ 003A ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002E × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 002E ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002E × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 002E ÷ 0031 ÷ 002C ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002E × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 002E ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 002E × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] FULL STOP (MidNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0030 ÷ 000D ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [3.2] (CR) ÷ [0.3] +÷ 0030 × 0308 ÷ 000D ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0030 ÷ 000A ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [3.2] (LF) ÷ [0.3] +÷ 0030 × 0308 ÷ 000A ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0030 ÷ 000B ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0030 × 0308 ÷ 000B ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0030 × 0300 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0030 × 0308 × 0300 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0030 × 00AD ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0030 × 0308 × 00AD ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0030 ÷ 3031 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0030 × 0308 ÷ 3031 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0030 × 24C2 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [10.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0030 × 0308 × 24C2 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [10.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0030 × 0041 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [10.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0030 × 0308 × 0041 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [10.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0030 ÷ 003A ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0030 × 0308 ÷ 003A ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0030 ÷ 002C ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0030 × 0308 ÷ 002C ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0030 ÷ 002E ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0030 × 0308 ÷ 002E ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0030 × 0030 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [8.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0030 × 0308 × 0030 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [8.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0030 × 005F ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0030 × 0308 × 005F ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0030 ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0030 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0030 × 05D0 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [10.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0030 × 0308 × 05D0 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [10.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0030 ÷ 0022 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0030 × 0308 ÷ 0022 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0030 ÷ 0027 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0030 × 0308 ÷ 0027 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0030 × 200D ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0030 × 0308 × 200D ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0030 ÷ 00A9 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0030 × 0308 ÷ 00A9 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0030 ÷ 0020 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0030 × 0308 ÷ 0020 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0030 ÷ 0000 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0030 × 0308 ÷ 0000 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0030 × 0061 × 2060 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0030 × 0308 × 0061 × 2060 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0030 × 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0030 × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0030 × 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0030 × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0030 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0030 × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0030 × 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0030 × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [10.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0030 × 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [8.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0030 × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [8.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0030 × 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [8.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0030 × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [8.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0030 × 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [8.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0030 × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [8.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0030 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [8.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0030 × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [4.0] COMBINING DIAERESIS (Extend) × [8.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 005F ÷ 000D ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [3.2] (CR) ÷ [0.3] +÷ 005F × 0308 ÷ 000D ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 005F ÷ 000A ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [3.2] (LF) ÷ [0.3] +÷ 005F × 0308 ÷ 000A ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 005F ÷ 000B ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [3.2] (Newline) ÷ [0.3] +÷ 005F × 0308 ÷ 000B ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 005F × 0300 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 005F × 0308 × 0300 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 005F × 00AD ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 005F × 0308 × 00AD ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 005F × 3031 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 005F × 0308 × 3031 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 005F × 24C2 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 005F × 0308 × 24C2 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 005F × 0041 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 005F × 0308 × 0041 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 005F ÷ 003A ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 005F × 0308 ÷ 003A ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 005F ÷ 002C ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 005F × 0308 ÷ 002C ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 005F ÷ 002E ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 005F × 0308 ÷ 002E ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 005F × 0030 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 005F × 0308 × 0030 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 005F × 005F ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 005F × 0308 × 005F ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 005F ÷ 1F1E6 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 005F × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 005F × 05D0 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 005F × 0308 × 05D0 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 005F ÷ 0022 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 005F × 0308 ÷ 0022 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 005F ÷ 0027 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 005F × 0308 ÷ 0027 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 005F × 200D ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 005F × 0308 × 200D ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 005F ÷ 00A9 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 005F × 0308 ÷ 00A9 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 005F ÷ 0020 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 005F × 0308 ÷ 0020 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 005F ÷ 0000 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 005F × 0308 ÷ 0000 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 005F × 0061 × 2060 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 005F × 0308 × 0061 × 2060 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 005F × 0061 ÷ 003A ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 005F × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 005F × 0061 ÷ 0027 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 005F × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 005F × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 005F × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 005F × 0061 ÷ 002C ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 005F × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 005F × 0031 ÷ 003A ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 005F × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 005F × 0031 ÷ 0027 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 005F × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 005F × 0031 ÷ 002C ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 005F × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 005F × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 005F × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LOW LINE (ExtendNumLet) × [4.0] COMBINING DIAERESIS (Extend) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 1F1E6 ÷ 000D ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [3.2] (CR) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 000D ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 1F1E6 ÷ 000A ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [3.2] (LF) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 000A ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 1F1E6 ÷ 000B ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [3.2] (Newline) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 000B ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 1F1E6 × 0300 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 1F1E6 × 0308 × 0300 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 1F1E6 × 00AD ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 1F1E6 × 0308 × 00AD ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 1F1E6 ÷ 3031 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 3031 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 1F1E6 ÷ 24C2 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 24C2 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 1F1E6 ÷ 0041 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0041 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 1F1E6 ÷ 003A ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 003A ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 1F1E6 ÷ 002C ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 002C ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 1F1E6 ÷ 002E ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 002E ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 1F1E6 ÷ 0030 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0030 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 1F1E6 ÷ 005F ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 005F ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 1F1E6 × 1F1E6 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [15.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 1F1E6 × 0308 × 1F1E6 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) × [15.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 1F1E6 ÷ 05D0 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 05D0 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 1F1E6 ÷ 0022 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0022 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 1F1E6 ÷ 0027 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0027 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 1F1E6 × 200D ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 1F1E6 × 0308 × 200D ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 1F1E6 ÷ 00A9 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 00A9 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 1F1E6 ÷ 0020 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0020 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 1F1E6 ÷ 0000 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0000 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 1F1E6 ÷ 0061 × 2060 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 1F1E6 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 1F1E6 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 1F1E6 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 1F1E6 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 1F1E6 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 1F1E6 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 1F1E6 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 1F1E6 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 1F1E6 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 05D0 ÷ 000D ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [3.2] (CR) ÷ [0.3] +÷ 05D0 × 0308 ÷ 000D ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 05D0 ÷ 000A ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [3.2] (LF) ÷ [0.3] +÷ 05D0 × 0308 ÷ 000A ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 05D0 ÷ 000B ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [3.2] (Newline) ÷ [0.3] +÷ 05D0 × 0308 ÷ 000B ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 05D0 × 0300 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 05D0 × 0308 × 0300 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 05D0 × 00AD ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 05D0 × 0308 × 00AD ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 05D0 ÷ 3031 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 05D0 × 0308 ÷ 3031 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 05D0 × 24C2 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [5.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 05D0 × 0308 × 24C2 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 05D0 × 0041 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 05D0 × 0308 × 0041 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 05D0 ÷ 003A ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 05D0 × 0308 ÷ 003A ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 05D0 ÷ 002C ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 05D0 × 0308 ÷ 002C ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 05D0 ÷ 002E ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 05D0 × 0308 ÷ 002E ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 05D0 × 0030 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [9.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 05D0 × 0308 × 0030 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 05D0 × 005F ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 05D0 × 0308 × 005F ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 05D0 ÷ 1F1E6 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 05D0 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 05D0 × 05D0 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [5.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 05D0 × 0308 × 05D0 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 05D0 ÷ 0022 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 05D0 × 0308 ÷ 0022 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 05D0 × 0027 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [7.1] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 05D0 × 0308 × 0027 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [7.1] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 05D0 × 200D ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 05D0 × 0308 × 200D ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 05D0 ÷ 00A9 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 05D0 × 0308 ÷ 00A9 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 05D0 ÷ 0020 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 05D0 × 0308 ÷ 0020 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 05D0 ÷ 0000 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 05D0 × 0308 ÷ 0000 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 05D0 × 0061 × 2060 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 05D0 × 0308 × 0061 × 2060 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 05D0 × 0061 ÷ 003A ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 05D0 × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 05D0 × 0061 ÷ 0027 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 05D0 × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 05D0 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 05D0 × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 05D0 × 0061 ÷ 002C ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 05D0 × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 05D0 × 0031 ÷ 003A ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 05D0 × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 05D0 × 0031 ÷ 0027 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 05D0 × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 05D0 × 0031 ÷ 002C ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 05D0 × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 05D0 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 05D0 × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0022 ÷ 000D ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [3.2] (CR) ÷ [0.3] +÷ 0022 × 0308 ÷ 000D ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0022 ÷ 000A ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [3.2] (LF) ÷ [0.3] +÷ 0022 × 0308 ÷ 000A ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0022 ÷ 000B ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0022 × 0308 ÷ 000B ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0022 × 0300 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0022 × 0308 × 0300 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0022 × 00AD ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0022 × 0308 × 00AD ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0022 ÷ 3031 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0022 × 0308 ÷ 3031 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0022 ÷ 24C2 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0022 × 0308 ÷ 24C2 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0022 ÷ 0041 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0022 × 0308 ÷ 0041 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0022 ÷ 003A ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0022 × 0308 ÷ 003A ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0022 ÷ 002C ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0022 × 0308 ÷ 002C ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0022 ÷ 002E ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0022 × 0308 ÷ 002E ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0022 ÷ 0030 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0022 × 0308 ÷ 0030 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0022 ÷ 005F ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0022 × 0308 ÷ 005F ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0022 ÷ 1F1E6 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0022 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0022 ÷ 05D0 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0022 × 0308 ÷ 05D0 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0022 ÷ 0022 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0022 × 0308 ÷ 0022 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0022 ÷ 0027 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0022 × 0308 ÷ 0027 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0022 × 200D ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0022 × 0308 × 200D ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0022 ÷ 00A9 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0022 × 0308 ÷ 00A9 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0022 ÷ 0020 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0022 × 0308 ÷ 0020 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0022 ÷ 0000 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0022 × 0308 ÷ 0000 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0022 ÷ 0061 × 2060 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0022 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0022 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0022 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0022 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0022 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0022 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0022 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0022 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0022 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0022 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0022 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0022 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0022 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0022 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0022 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0022 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0022 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] QUOTATION MARK (Double_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0027 ÷ 000D ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [3.2] (CR) ÷ [0.3] +÷ 0027 × 0308 ÷ 000D ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0027 ÷ 000A ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [3.2] (LF) ÷ [0.3] +÷ 0027 × 0308 ÷ 000A ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0027 ÷ 000B ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0027 × 0308 ÷ 000B ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0027 × 0300 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0027 × 0308 × 0300 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0027 × 00AD ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0027 × 0308 × 00AD ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0027 ÷ 3031 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0027 × 0308 ÷ 3031 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0027 ÷ 24C2 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0027 × 0308 ÷ 24C2 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0027 ÷ 0041 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0027 × 0308 ÷ 0041 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0027 ÷ 003A ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0027 × 0308 ÷ 003A ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0027 ÷ 002C ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0027 × 0308 ÷ 002C ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0027 ÷ 002E ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0027 × 0308 ÷ 002E ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0027 ÷ 0030 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0027 × 0308 ÷ 0030 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0027 ÷ 005F ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0027 × 0308 ÷ 005F ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0027 ÷ 1F1E6 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0027 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0027 ÷ 05D0 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0027 × 0308 ÷ 05D0 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0027 ÷ 0022 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0027 × 0308 ÷ 0022 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0027 ÷ 0027 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0027 × 0308 ÷ 0027 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0027 × 200D ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0027 × 0308 × 200D ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0027 ÷ 00A9 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0027 × 0308 ÷ 00A9 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0027 ÷ 0020 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0027 × 0308 ÷ 0020 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0027 ÷ 0000 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0027 × 0308 ÷ 0000 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0027 ÷ 0061 × 2060 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0027 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0027 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0027 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0027 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0027 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0027 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0027 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0027 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0027 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0027 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0027 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0027 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0027 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0027 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0027 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0027 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0027 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 200D ÷ 000D ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [3.2] (CR) ÷ [0.3] +÷ 200D × 0308 ÷ 000D ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 200D ÷ 000A ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [3.2] (LF) ÷ [0.3] +÷ 200D × 0308 ÷ 000A ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 200D ÷ 000B ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [3.2] (Newline) ÷ [0.3] +÷ 200D × 0308 ÷ 000B ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 200D × 0300 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 200D × 0308 × 0300 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 200D × 00AD ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 200D × 0308 × 00AD ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 200D ÷ 3031 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 200D × 0308 ÷ 3031 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 200D × 24C2 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [3.3] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 200D × 0308 ÷ 24C2 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 200D ÷ 0041 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 200D × 0308 ÷ 0041 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 200D ÷ 003A ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 200D × 0308 ÷ 003A ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 200D ÷ 002C ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 200D × 0308 ÷ 002C ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 200D ÷ 002E ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 200D × 0308 ÷ 002E ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 200D ÷ 0030 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 200D × 0308 ÷ 0030 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 200D ÷ 005F ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 200D × 0308 ÷ 005F ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 200D ÷ 1F1E6 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 200D × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 200D ÷ 05D0 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 200D × 0308 ÷ 05D0 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 200D ÷ 0022 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 200D × 0308 ÷ 0022 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 200D ÷ 0027 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 200D × 0308 ÷ 0027 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 200D × 200D ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 200D × 0308 × 200D ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 200D × 00A9 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [3.3] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 200D × 0308 ÷ 00A9 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 200D ÷ 0020 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 200D × 0308 ÷ 0020 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 200D ÷ 0000 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 200D × 0308 ÷ 0000 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 200D ÷ 0061 × 2060 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 200D × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 200D ÷ 0061 ÷ 003A ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 200D × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 200D ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 200D × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 200D ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 200D × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 200D ÷ 0061 ÷ 002C ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 200D × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 200D ÷ 0031 ÷ 003A ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 200D × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 200D ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 200D × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 200D ÷ 0031 ÷ 002C ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 200D × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 200D ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 200D × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00A9 ÷ 000D ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [3.2] (CR) ÷ [0.3] +÷ 00A9 × 0308 ÷ 000D ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 00A9 ÷ 000A ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [3.2] (LF) ÷ [0.3] +÷ 00A9 × 0308 ÷ 000A ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 00A9 ÷ 000B ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [3.2] (Newline) ÷ [0.3] +÷ 00A9 × 0308 ÷ 000B ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 00A9 × 0300 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 00A9 × 0308 × 0300 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 00A9 × 00AD ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 00A9 × 0308 × 00AD ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 00A9 ÷ 3031 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 00A9 × 0308 ÷ 3031 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 00A9 ÷ 24C2 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 00A9 × 0308 ÷ 24C2 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 00A9 ÷ 0041 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0041 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 00A9 ÷ 003A ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00A9 × 0308 ÷ 003A ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00A9 ÷ 002C ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00A9 × 0308 ÷ 002C ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00A9 ÷ 002E ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 00A9 × 0308 ÷ 002E ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 00A9 ÷ 0030 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0030 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 00A9 ÷ 005F ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 00A9 × 0308 ÷ 005F ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 00A9 ÷ 1F1E6 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 00A9 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 00A9 ÷ 05D0 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 00A9 × 0308 ÷ 05D0 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 00A9 ÷ 0022 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0022 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 00A9 ÷ 0027 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0027 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00A9 × 200D ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 00A9 × 0308 × 200D ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 00A9 ÷ 00A9 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 00A9 × 0308 ÷ 00A9 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 00A9 ÷ 0020 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0020 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 00A9 ÷ 0000 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0000 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 00A9 ÷ 0061 × 2060 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00A9 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00A9 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00A9 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00A9 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00A9 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 00A9 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 00A9 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 00A9 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 00A9 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] COPYRIGHT SIGN (ExtPictmALetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0020 ÷ 000D ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [3.2] (CR) ÷ [0.3] +÷ 0020 × 0308 ÷ 000D ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0020 ÷ 000A ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [3.2] (LF) ÷ [0.3] +÷ 0020 × 0308 ÷ 000A ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0020 ÷ 000B ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0020 × 0308 ÷ 000B ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0020 × 0300 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0020 × 0308 × 0300 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0020 × 00AD ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0020 × 0308 × 00AD ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0020 ÷ 3031 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0020 × 0308 ÷ 3031 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0020 ÷ 24C2 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0020 × 0308 ÷ 24C2 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0020 ÷ 0041 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0020 × 0308 ÷ 0041 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0020 ÷ 003A ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0020 × 0308 ÷ 003A ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0020 ÷ 002C ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0020 × 0308 ÷ 002C ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0020 ÷ 002E ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0020 × 0308 ÷ 002E ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0020 ÷ 0030 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0020 × 0308 ÷ 0030 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0020 ÷ 005F ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0020 × 0308 ÷ 005F ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0020 ÷ 1F1E6 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0020 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0020 ÷ 05D0 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0020 × 0308 ÷ 05D0 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0020 ÷ 0022 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0020 × 0308 ÷ 0022 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0020 ÷ 0027 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0020 × 0308 ÷ 0027 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0020 × 200D ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0020 × 0308 × 200D ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0020 ÷ 00A9 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0020 × 0308 ÷ 00A9 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0020 × 0020 ÷ # ÷ [0.2] SPACE (WSegSpace) × [3.4] SPACE (WSegSpace) ÷ [0.3] +÷ 0020 × 0308 ÷ 0020 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0020 ÷ 0000 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0020 × 0308 ÷ 0000 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0020 ÷ 0061 × 2060 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0020 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0020 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0020 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0020 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0020 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0020 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0020 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0020 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0020 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0020 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0020 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0020 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0020 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0020 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0020 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0020 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] SPACE (WSegSpace) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0020 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0000 ÷ 000D ÷ # ÷ [0.2] (XXmExtPict) ÷ [3.2] (CR) ÷ [0.3] +÷ 0000 × 0308 ÷ 000D ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0000 ÷ 000A ÷ # ÷ [0.2] (XXmExtPict) ÷ [3.2] (LF) ÷ [0.3] +÷ 0000 × 0308 ÷ 000A ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0000 ÷ 000B ÷ # ÷ [0.2] (XXmExtPict) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0000 × 0308 ÷ 000B ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0000 × 0300 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0000 × 0308 × 0300 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0000 × 00AD ÷ # ÷ [0.2] (XXmExtPict) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0000 × 0308 × 00AD ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0000 ÷ 3031 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0000 × 0308 ÷ 3031 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0000 ÷ 24C2 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0000 × 0308 ÷ 24C2 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0000 ÷ 0041 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0000 × 0308 ÷ 0041 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0000 ÷ 003A ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0000 × 0308 ÷ 003A ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0000 ÷ 002C ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0000 × 0308 ÷ 002C ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0000 ÷ 002E ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0000 × 0308 ÷ 002E ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0000 ÷ 0030 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0000 × 0308 ÷ 0030 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0000 ÷ 005F ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0000 × 0308 ÷ 005F ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0000 ÷ 1F1E6 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0000 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0000 ÷ 05D0 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0000 × 0308 ÷ 05D0 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0000 ÷ 0022 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0000 × 0308 ÷ 0022 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0000 ÷ 0027 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0000 × 0308 ÷ 0027 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0000 × 200D ÷ # ÷ [0.2] (XXmExtPict) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0000 × 0308 × 200D ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0000 ÷ 00A9 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0000 × 0308 ÷ 00A9 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0000 ÷ 0020 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0000 × 0308 ÷ 0020 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0000 ÷ 0000 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0000 × 0308 ÷ 0000 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0000 ÷ 0061 × 2060 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0000 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0000 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0000 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0000 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0000 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0000 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0000 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0000 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0000 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0000 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0000 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0000 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0000 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0000 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0000 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0000 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] (XXmExtPict) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0000 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] (XXmExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 2060 ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 × 2060 ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 × 2060 ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 × 2060 × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 × 2060 × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 × 2060 ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 × 2060 × 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [5.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 × 2060 × 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 2060 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 2060 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 2060 ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 × 2060 × 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [9.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 × 2060 × 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 × 2060 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 × 2060 × 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [5.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 × 2060 ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 × 2060 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 2060 × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 × 2060 ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 × 2060 ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 × 2060 ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 × 2060 × 0308 ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 × 2060 × 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 2060 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 2060 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 2060 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 2060 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 2060 × 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 2060 × 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 2060 × 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 2060 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 2060 × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [9.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 ÷ 003A × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 ÷ 003A × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 × 003A × 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 × 003A × 0308 × 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 × 003A × 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 003A × 0308 × 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 × 003A × 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 × 003A × 0308 × 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 003A × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 × 003A × 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 003A × 0308 × 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 003A × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 003A × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 003A × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 003A × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 003A × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 003A × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 003A × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 003A × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 003A × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 ÷ 0027 × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 ÷ 0027 × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 × 0027 × 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [7.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 × 0027 × 0308 × 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 × 0027 × 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [7.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 0027 × 0308 × 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 × 0027 × 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [7.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 × 0027 × 0308 × 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 × 0027 × 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 0027 × 0308 × 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 0027 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 0027 × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 0027 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 0027 × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 0027 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 0027 × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 0027 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 0027 × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 0027 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [7.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0308 × 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [7.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0308 × 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [7.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0308 × 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0308 × 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0308 × 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0308 × 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0308 × 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 × 0027 × 2060 × 0308 × 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [6.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [7.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 0027 × 2060 × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 000D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 000A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 000B ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0061 ÷ 002C × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 × 0300 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0061 ÷ 002C × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 × 00AD ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 3031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 24C2 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0041 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 002E ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0030 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 005F ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 05D0 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0022 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 002C × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 × 200D ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 00A9 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0020 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0000 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0061 ÷ 002C × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 000D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [3.2] (CR) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 000D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 000A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [3.2] (LF) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 000A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 000B ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 000B ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0031 ÷ 003A × 0300 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 × 0300 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0031 ÷ 003A × 00AD ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 × 00AD ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 3031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 3031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 24C2 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 24C2 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0041 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0041 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 002E ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 002E ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0030 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0030 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 005F ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 005F ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 05D0 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 05D0 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0022 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0022 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 003A × 200D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 × 200D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 00A9 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 00A9 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0020 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0020 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0000 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0000 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0061 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 003A × 0308 ÷ 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 000D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [3.2] (CR) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 000D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 000A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [3.2] (LF) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 000A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 000B ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 000B ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0031 ÷ 0027 × 0300 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 × 0300 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0031 ÷ 0027 × 00AD ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 × 00AD ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 3031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 3031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 24C2 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 24C2 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0041 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0041 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 002E ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 002E ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0031 × 0027 × 0030 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [11.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0031 × 0027 × 0308 × 0030 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 005F ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 005F ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 05D0 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 05D0 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0022 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0022 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 0027 × 200D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 × 200D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 00A9 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 00A9 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0020 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0020 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0000 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0000 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0061 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 0027 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 0027 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 0027 × 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 × 0027 × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 × 0027 × 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 × 0027 × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 × 0027 × 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 0027 × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 0027 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 × 0027 × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] APOSTROPHE (Single_Quote) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 000D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [3.2] (CR) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 000D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 000A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [3.2] (LF) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 000A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 000B ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 000B ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0031 ÷ 002C × 0300 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 × 0300 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0031 ÷ 002C × 00AD ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 × 00AD ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 3031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 3031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 24C2 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 24C2 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0041 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0041 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 002E ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 002E ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0031 × 002C × 0030 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [11.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0031 × 002C × 0308 × 0030 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 005F ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 005F ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 05D0 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 05D0 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0022 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0022 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 002C × 200D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 × 200D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 00A9 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 00A9 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0020 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0020 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0000 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0000 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0061 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 002C × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 002C × 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 × 002C × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 × 002C × 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 × 002C × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 × 002C × 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 002C × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 002C × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 × 002C × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] COMMA (MidNum) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 000D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [3.2] (CR) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 000D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (CR) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 000A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [3.2] (LF) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 000A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (LF) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 000B ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 000B ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [3.2] (Newline) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0300 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 × 0300 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] COMBINING GRAVE ACCENT (Extend) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 00AD ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 × 00AD ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] SOFT HYPHEN (Format) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 3031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 3031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 24C2 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 24C2 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] CIRCLED LATIN CAPITAL LETTER M (ALetter_ExtPict) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0041 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0041 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 002E ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 002E ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] FULL STOP (MidNumLet) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0030 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [11.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0308 × 0030 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 005F ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 005F ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 1F1E6 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 05D0 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 05D0 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0022 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0022 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] QUOTATION MARK (Double_Quote) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 200D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 × 200D ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 00A9 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 00A9 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] COPYRIGHT SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0020 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0020 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0000 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0000 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] (XXmExtPict) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0061 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0061 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0061 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0061 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0061 ÷ 0027 × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] APOSTROPHE (Single_Quote) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 ÷ 002E × 2060 × 0308 ÷ 0061 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0308 × 0031 ÷ 003A ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0308 × 0031 ÷ 0027 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0308 × 0031 ÷ 002C ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 0031 × 002E × 2060 × 0308 × 0031 ÷ 002E × 2060 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [12.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) × [4.0] COMBINING DIAERESIS (Extend) × [11.0] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) × [4.0] WORD JOINER (Format) ÷ [0.3] +÷ 000D × 000A ÷ 0061 ÷ 000A ÷ 0308 ÷ # ÷ [0.2] (CR) × [3.0] (LF) ÷ [3.1] LATIN SMALL LETTER A (ALettermExtPict) ÷ [3.2] (LF) ÷ [3.1] COMBINING DIAERESIS (Extend) ÷ [0.3] +÷ 0061 × 0308 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) ÷ [0.3] +÷ 0020 × 200D ÷ 0646 ÷ # ÷ [0.2] SPACE (WSegSpace) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] ARABIC LETTER NOON (ALettermExtPict) ÷ [0.3] +÷ 0646 × 200D ÷ 0020 ÷ # ÷ [0.2] ARABIC LETTER NOON (ALettermExtPict) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] SPACE (WSegSpace) ÷ [0.3] +÷ 0671 × 0644 × 0631 × 064E × 0651 × 062D × 0650 × 064A × 0645 × 0650 ÷ 0020 ÷ 06DD × 0661 ÷ # ÷ [0.2] ARABIC LETTER ALEF WASLA (ALettermExtPict) × [5.0] ARABIC LETTER LAM (ALettermExtPict) × [5.0] ARABIC LETTER REH (ALettermExtPict) × [4.0] ARABIC FATHA (Extend) × [4.0] ARABIC SHADDA (Extend) × [5.0] ARABIC LETTER HAH (ALettermExtPict) × [4.0] ARABIC KASRA (Extend) × [5.0] ARABIC LETTER YEH (ALettermExtPict) × [5.0] ARABIC LETTER MEEM (ALettermExtPict) × [4.0] ARABIC KASRA (Extend) ÷ [999.0] SPACE (WSegSpace) ÷ [999.0] ARABIC END OF AYAH (Numeric) × [8.0] ARABIC-INDIC DIGIT ONE (Numeric) ÷ [0.3] +÷ 0721 × 0719 × 0721 × 0718 × 072A × 0710 ÷ 0020 ÷ 070F × 071D × 0717 ÷ # ÷ [0.2] SYRIAC LETTER MIM (ALettermExtPict) × [5.0] SYRIAC LETTER ZAIN (ALettermExtPict) × [5.0] SYRIAC LETTER MIM (ALettermExtPict) × [5.0] SYRIAC LETTER WAW (ALettermExtPict) × [5.0] SYRIAC LETTER RISH (ALettermExtPict) × [5.0] SYRIAC LETTER ALAPH (ALettermExtPict) ÷ [999.0] SPACE (WSegSpace) ÷ [999.0] SYRIAC ABBREVIATION MARK (ALettermExtPict) × [5.0] SYRIAC LETTER YUDH (ALettermExtPict) × [5.0] SYRIAC LETTER HE (ALettermExtPict) ÷ [0.3] +÷ 072C × 070F × 072B × 0712 × 0718 ÷ # ÷ [0.2] SYRIAC LETTER TAW (ALettermExtPict) × [5.0] SYRIAC ABBREVIATION MARK (ALettermExtPict) × [5.0] SYRIAC LETTER SHIN (ALettermExtPict) × [5.0] SYRIAC LETTER BETH (ALettermExtPict) × [5.0] SYRIAC LETTER WAW (ALettermExtPict) ÷ [0.3] +÷ 0041 × 0041 × 0041 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) × [5.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0041 × 003A × 0041 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [6.0] COLON (MidLetter) × [7.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0041 ÷ 003A ÷ 003A ÷ 0041 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 05D0 × 0027 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [7.1] APOSTROPHE (Single_Quote) ÷ [0.3] +÷ 05D0 × 0022 × 05D0 ÷ # ÷ [0.2] HEBREW LETTER ALEF (Hebrew_Letter) × [7.2] QUOTATION MARK (Double_Quote) × [7.3] HEBREW LETTER ALEF (Hebrew_Letter) ÷ [0.3] +÷ 0041 × 0030 × 0030 × 0041 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [9.0] DIGIT ZERO (Numeric) × [8.0] DIGIT ZERO (Numeric) × [10.0] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0030 × 002C × 0030 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) × [12.0] COMMA (MidNum) × [11.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 0030 ÷ 002C ÷ 002C ÷ 0030 ÷ # ÷ [0.2] DIGIT ZERO (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ZERO (Numeric) ÷ [0.3] +÷ 3031 × 3031 ÷ # ÷ [0.2] VERTICAL KANA REPEAT MARK (Katakana) × [13.0] VERTICAL KANA REPEAT MARK (Katakana) ÷ [0.3] +÷ 0041 × 005F × 0030 × 005F × 3031 × 005F ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ZERO (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] VERTICAL KANA REPEAT MARK (Katakana) × [13.1] LOW LINE (ExtendNumLet) ÷ [0.3] +÷ 0041 × 005F × 005F × 0041 ÷ # ÷ [0.2] LATIN CAPITAL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN CAPITAL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 1F1E6 × 1F1E7 ÷ 1F1E8 ÷ 0062 ÷ # ÷ [0.2] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [15.0] REGIONAL INDICATOR SYMBOL LETTER B (RI) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER C (RI) ÷ [999.0] LATIN SMALL LETTER B (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 1F1E6 × 1F1E7 ÷ 1F1E8 ÷ 0062 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [16.0] REGIONAL INDICATOR SYMBOL LETTER B (RI) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER C (RI) ÷ [999.0] LATIN SMALL LETTER B (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 1F1E6 × 1F1E7 × 200D ÷ 1F1E8 ÷ 0062 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [16.0] REGIONAL INDICATOR SYMBOL LETTER B (RI) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER C (RI) ÷ [999.0] LATIN SMALL LETTER B (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 1F1E6 × 200D × 1F1E7 ÷ 1F1E8 ÷ 0062 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [4.0] ZERO WIDTH JOINER (ZWJ) × [16.0] REGIONAL INDICATOR SYMBOL LETTER B (RI) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER C (RI) ÷ [999.0] LATIN SMALL LETTER B (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 1F1E6 × 1F1E7 ÷ 1F1E8 × 1F1E9 ÷ 0062 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER A (RI) × [16.0] REGIONAL INDICATOR SYMBOL LETTER B (RI) ÷ [999.0] REGIONAL INDICATOR SYMBOL LETTER C (RI) × [16.0] REGIONAL INDICATOR SYMBOL LETTER D (RI) ÷ [999.0] LATIN SMALL LETTER B (ALettermExtPict) ÷ [0.3] +÷ 1F476 × 1F3FF ÷ 1F476 ÷ # ÷ [0.2] BABY (ExtPictmALetter) × [4.0] EMOJI MODIFIER FITZPATRICK TYPE-6 (Extend) ÷ [999.0] BABY (ExtPictmALetter) ÷ [0.3] +÷ 1F6D1 × 200D × 1F6D1 ÷ # ÷ [0.2] OCTAGONAL SIGN (ExtPictmALetter) × [4.0] ZERO WIDTH JOINER (ZWJ) × [3.3] OCTAGONAL SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 × 200D × 1F6D1 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] ZERO WIDTH JOINER (ZWJ) × [3.3] OCTAGONAL SIGN (ExtPictmALetter) ÷ [0.3] +÷ 2701 × 200D ÷ 2701 ÷ # ÷ [0.2] UPPER BLADE SCISSORS (XXmExtPict) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] UPPER BLADE SCISSORS (XXmExtPict) ÷ [0.3] +÷ 0061 × 200D ÷ 2701 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] ZERO WIDTH JOINER (ZWJ) ÷ [999.0] UPPER BLADE SCISSORS (XXmExtPict) ÷ [0.3] +÷ 1F476 × 1F3FF × 0308 × 200D × 1F476 × 1F3FF ÷ # ÷ [0.2] BABY (ExtPictmALetter) × [4.0] EMOJI MODIFIER FITZPATRICK TYPE-6 (Extend) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) × [3.3] BABY (ExtPictmALetter) × [4.0] EMOJI MODIFIER FITZPATRICK TYPE-6 (Extend) ÷ [0.3] +÷ 1F6D1 × 1F3FF ÷ # ÷ [0.2] OCTAGONAL SIGN (ExtPictmALetter) × [4.0] EMOJI MODIFIER FITZPATRICK TYPE-6 (Extend) ÷ [0.3] +÷ 200D × 1F6D1 × 1F3FF ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [3.3] OCTAGONAL SIGN (ExtPictmALetter) × [4.0] EMOJI MODIFIER FITZPATRICK TYPE-6 (Extend) ÷ [0.3] +÷ 200D × 1F6D1 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [3.3] OCTAGONAL SIGN (ExtPictmALetter) ÷ [0.3] +÷ 200D × 1F6D1 ÷ # ÷ [0.2] ZERO WIDTH JOINER (ZWJ) × [3.3] OCTAGONAL SIGN (ExtPictmALetter) ÷ [0.3] +÷ 1F6D1 ÷ 1F6D1 ÷ # ÷ [0.2] OCTAGONAL SIGN (ExtPictmALetter) ÷ [999.0] OCTAGONAL SIGN (ExtPictmALetter) ÷ [0.3] +÷ 0061 × 0308 × 200D × 0308 × 0062 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [4.0] COMBINING DIAERESIS (Extend) × [4.0] ZERO WIDTH JOINER (ZWJ) × [4.0] COMBINING DIAERESIS (Extend) × [5.0] LATIN SMALL LETTER B (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 0020 × 0020 ÷ 0062 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] SPACE (WSegSpace) × [3.4] SPACE (WSegSpace) ÷ [999.0] LATIN SMALL LETTER B (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 003A ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 003A ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 003A ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 003A ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 003A ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 003A ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 003A ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 003A ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 003A ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 003A ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 003A ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 003A ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 003A ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002E ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002E ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002E ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 002E ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002E ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002E ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002E ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002E ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002E ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 002E ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002E ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002E ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002E ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002E ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002E ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 002E ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002E ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002E ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002C ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002C ÷ 003A ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002C ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002C ÷ 003A ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002C ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002C ÷ 002E ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002C ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002C ÷ 002E ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002C ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002C ÷ 002C ÷ 0031 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0031 ÷ 002C ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0031 ÷ 002C ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0031 × 005F × 0061 ÷ 002C ÷ 002C ÷ 0061 ÷ # ÷ [0.2] DIGIT ONE (Numeric) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 003A ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 003A ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 003A ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 003A ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 003A ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 003A ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 003A ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 003A ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 003A ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 003A ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 003A ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 003A ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 003A ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COLON (MidLetter) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 002E ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002E ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002E ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 002E ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002E ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002E ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 002E ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002E ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002E ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 002E ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002E ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002E ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 002E ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002E ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002E ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 002E ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002E ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002E ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002C ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002C ÷ 003A ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002C ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002C ÷ 003A ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COLON (MidLetter) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002C ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002C ÷ 002E ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002C ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002C ÷ 002E ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] FULL STOP (MidNumLet) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002C ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002C ÷ 002C ÷ 0031 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] DIGIT ONE (Numeric) ÷ [0.3] +÷ 0061 ÷ 002C ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0031 ÷ 002C ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] DIGIT ONE (Numeric) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +÷ 0061 × 005F × 0061 ÷ 002C ÷ 002C ÷ 0061 ÷ # ÷ [0.2] LATIN SMALL LETTER A (ALettermExtPict) × [13.1] LOW LINE (ExtendNumLet) × [13.2] LATIN SMALL LETTER A (ALettermExtPict) ÷ [999.0] COMMA (MidNum) ÷ [999.0] COMMA (MidNum) ÷ [999.0] LATIN SMALL LETTER A (ALettermExtPict) ÷ [0.3] +# +# Lines: 1944 +# +# EOF