Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow rendering of glyphs directly #61

Open
casperOne opened this issue Nov 25, 2021 · 5 comments
Open

Allow rendering of glyphs directly #61

casperOne opened this issue Nov 25, 2021 · 5 comments

Comments

@casperOne
Copy link

casperOne commented Nov 25, 2021

I'm currently working with QuestPDF to try and render PDFs that may contain multiple languages.

These documents are dynamically generated from external content outside of my control, so I won't know the characters/languages that are needed, but I have a good idea of what combinations of fonts can be used to provide the coverage I need.

That said, can use SkiaSharp to get the font information and provide fallbacks by querying for glyph support in the fonts that I have.

This means that I'll have different array of glyphs, each which I'd like to render using a different font.

What I'm looking for from QuestPDF is a way to be able to pass an array of ushort (or whatever you feel is appropriate) representing the glyphs of a font to render.

Something analogous to Text, like so:

// Obtained from Skiasharp
ushort[] glyphs = ...;

container.Glyphs(glyphs /*, optional TextStyle here */);

As well as an overload that takes a descriptor, like Text:

// Obtained from Skiasharp
ushort[] glyphs = ...;

container.Glyphs(g => g.Span(glyphs /*, optional TextStyle here */));

The font used would be whatever is specified in the TextStyle or the global font (i.e. normal font fallbacks are used although often, I would be specifying TextStyle to indicate the font to use).

I should also note that what would help in this process is to add overloads taking ReadOnlySpan<ushort> as we'll probably have an array of glyphs which we'll want to send subsections of without allocating new arrays (also, in general, overloads of Text which take ReadOnlySpan<char> would be great as well).

@MarcinZiabek
Copy link
Member

Thank you for your proposition. Can you please describe exactly what problem this feature solves?

As far as I understand, QuestPDF needs to offer better text capabilities in future releases. Including font subsetting (to minimize final PDF size), text shaping and font fallback. Each of them is a difficult topic, not to mention it touches the most complex part of the library 😀 But, assuming that all three mentioned features are available, do you still have a use-case for the .Glyph() feature?

I should also note that what would help in this process is to add overloads taking ReadOnlySpan as we'll probably have an array of glyphs which we'll want to send subsections of without allocating new arrays (also, in general, overloads of Text which take ReadOnlySpan would be great as well).

The layouting engine used in QuestPDF is a multi-pass algorithm. It is needed to support page-related features (e.g. printing a total number of pages on each page). Therefore, all elements in the document need to exist in the memory during the entire rendering process. From this point, I am not sure if the ReadOnlySpan<char> will provide any optimization as entire text need to be allocated in the memory. What do you think?

@casperOne
Copy link
Author

casperOne commented Nov 28, 2021

The problem I'm trying to solve is font fallback.

For content that where we are unsure of what typeface needs to be used, we cycle through each character in the string, calling GetGlyphs on the SKTypeface class; if any of the glyphs returned is 0, we move to the next typeface and repeat the process.

This results in an array of pairs (of typefaces and glyphs) which we want to render. Since we already know which typeface and which glyphs we want to render, it's easier if we pass that directly to QuestPDF to render.

Because we have one large array of glyphs (ushort) at the end of this, being able to take a ReadOnlySpan<ushort> in this proposed overload would prevent us from having to allocate smaller subsections of that array for each individual section of glyphs/typefaces we want to render.

Speaking to the larger issues, if QuestPDF had some sort of font fallback mechanism, we wouldn't need this; however, I understand that font fallback is a tricky subject and don't expect that feature, as we have a mechanism to handle this now. Of course, if you feel the fallback feature is the more appropriate feature (over Glyph) then we'd be happy to use that feature.

@MarcinZiabek
Copy link
Member

MarcinZiabek commented Nov 28, 2021

I think that font fallback is more appropriate than the Glyph feature as it covers the problem for more users. There is no such functionality at this moment but I am well aware that it should be added at some point.

Right now, you can use the .Span() functionality to achieve similar results based on your data. It can glue together various text runs with different glyphs and fonts.

If you can share your code for font fallback implementation, it will help me start with this feature in the future 😁 I often have many experimental branches where I test new functionalities way before making them public.

@casperOne
Copy link
Author

@MarcinZiabek Warning, wall of code incoming:

using Ofl.Collections.Generic;
using SkiaSharp;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reactive.Disposables;

namespace Ofl.Core.MediaPack.Rendering.Pdf.Fonts
{
    public class FontManager : IDisposable
    {
        #region Instnace, read-only state

        private readonly CompositeDisposable _disposables = new();

        private readonly IReadOnlyList<SKTypeface> _typefaces;

        public readonly IReadOnlyDictionary<SKTypeface, string> TypefaceToFontNameMap;

        public static readonly FontManager Instance = new FontManager();

        #endregion

        #region Constructor

        private FontManager()
        {
            // The list of typefaces.
            var typefaces = new List<SKTypeface>();
            var typefaceToFontMap = new Dictionary<SKTypeface, string>();

            // Adds a typeface.
            void AddTypeface(string font)
            {
                // Open the stream.
                using Stream stream = Font.GetFont(font);

                // Get the typeface.
                var typeface = SKFontManager.Default.CreateTypeface(
                    stream
                );

                // Add to the disposables.
                _disposables.Add(typeface);

                // Add to the list.
                typefaces.Add(typeface);
                typefaceToFontMap.Add(typeface, font);
            }

            // Add all the typefaces.
            foreach (var typeface in Font.AllFonts)
                AddTypeface(typeface);

            // Set the typefaces.
            _typefaces = typefaces.WrapInReadOnlyCollection();
            TypefaceToFontNameMap = typefaceToFontMap.WrapInReadOnlyDictionary();
        }

        #endregion

        #region Helpers

        public IReadOnlyCollection<(int Start, int Length, string Font)> GetSpans(
            string value
        )
        {
            // SHORTCUT: If the length of the string is
            // 0, return empty.
            if (value.Length == 0)
                return Array.Empty<(int Start, int Length, string Font)>();

            // Get the string as a span.
            var valueSpan = value.AsSpan();

            // The spans.
            var spans = new List<(int Start, int Length, string Font)>();

            // The index in the string, start at -1 so we can increment
            // at the beginning.
            var index = -1;

            // The previous font; set to the first font.
            var previousFont = TypefaceToFontNameMap[_typefaces[0]];

            // The start.
            var start = 0;

            // While the index is less than the length of the span.
            while (++index < valueSpan.Length)
            {
                // Get the character span.
                var characterSpan = valueSpan.Slice(index, 1);

                // Cycle through the fonts, see which one has glyphs.
                foreach (var typeface in _typefaces)
                {
                    // Get the glyphs.
                    var glyphs = typeface.GetGlyphs(characterSpan);

                    // If any of the glyphs are 0, continue to
                    // the next font.
                    if (glyphs.Any(u => u == 0))
                        continue;

                    // This font slaps, get the name.
                    var font = TypefaceToFontNameMap[typeface];

                    // Is it different than the previous font?
                    if (previousFont != font)
                    {
                        // Create the span.
                        var span = (
                            start,
                            index - start,
                            previousFont
                        );

                        // Add the span if we are
                        // *not* at index 0.
                        if (index != 0)
                            spans.Add(span);

                        // Set the previous font and
                        // the start.
                        previousFont = font;
                        start = index;
                    }

                    // Break.
                    break;
                }
            }

            // Add the last span if there are any characters.
            spans.Add((
                start,
                index - start,
                previousFont
            ));

            // Wrap and return.
            return spans.WrapInReadOnlyCollection();
        }

        #endregion

        #region IDisposable implementation

        public void Dispose() => Dispose(true);

        protected virtual void Dispose(bool disposing)
        {
            // Dispose of unmanaged resources, etc.

            // If not disposing, bail.
            if (!disposing) return;

            // Dispose of IDisposable implementations.
            using (_disposables) { }
        }

        ~FontManager() => Dispose(false);

        #endregion
    }
}

Basically, we load SKTypeface instances from the Skiasharp library. We then go through each typeface, processing each character one at a time to ensure that all of the glyphs are valid. If they are not, we move to the next typeface and check that.

So the overall time is O(f * l) where f is the number of fonts and l is the length of the string.

@MarcinZiabek
Copy link
Member

Thank you! I will analyse this code and hopefully, it would be a great foundation for this feature 😁 I really appreciate your help in this regard.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants