Coder Social home page Coder Social logo

Comments (5)

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024 2

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.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024 1

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.

from questpdf.

MarcinZiabek avatar MarcinZiabek commented on May 21, 2024

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?

from questpdf.

casperOne avatar casperOne commented on May 21, 2024

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.

from questpdf.

casperOne avatar casperOne commented on May 21, 2024

@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.

from questpdf.

Related Issues (20)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.