File: MS\Internal\FontCache\FontSource.cs
Web Access
Project: src\src\Microsoft.DotNet.Wpf\src\PresentationCore\PresentationCore.csproj (PresentationCore)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
//
// 
// Description: The FontSource class.
//
//
 
using System.IO;
using System.IO.Packaging;
using System.Net;
using System.Reflection;
using System.Resources;
using System.Runtime.InteropServices;
using MS.Internal.IO.Packaging;
using MS.Internal.Text.TextInterface;
 
namespace MS.Internal.FontCache
{
    internal class FontSourceFactory : IFontSourceFactory
    {
        public FontSourceFactory() { }
        
        public IFontSource Create(string uriString)
        {
            return new FontSource(new Uri(uriString));
        }
    }
 
    /// <summary>
    /// FontSource class encapsulates the logic for dealing with fonts in memory or on the disk.
    /// It may or may not have a Uri associated with it, but there has to be some way to obtain its contents.
    /// </summary>
    internal class FontSource : IFontSource
    {
        //------------------------------------------------------
        //
        //  Constructors
        //
        //------------------------------------------------------
 
        #region Constructors
 
        public FontSource(Uri fontUri)
        {
            Initialize(fontUri, false, isInternalCompositeFont: false);
        }
 
        public FontSource(Uri fontUri, bool isComposite)
        {
            Initialize(fontUri, isComposite, isInternalCompositeFont: false);
        }
 
        /// <summary>
        /// Allows WPF to construct its internal CompositeFonts from resource URIs.
        /// </summary>
        /// <param name="fontUri"></param>
        /// <param name="isComposite"></param>
        public FontSource(Uri fontUri, bool isComposite, bool isInternalCompositeFont)
        {
            Initialize(fontUri, isComposite, isInternalCompositeFont);
        }
 
        private void Initialize(Uri fontUri, bool isComposite, bool isInternalCompositeFont)
        {
            _fontUri = fontUri;
            _isComposite = isComposite;
            _isInternalCompositeFont = isInternalCompositeFont;
            Invariant.Assert(_isInternalCompositeFont || _fontUri.IsAbsoluteUri);
            Debug.Assert(_isInternalCompositeFont || String.IsNullOrEmpty(_fontUri.Fragment));
        }
 
        #endregion Constructors
 
        //------------------------------------------------------
        //
        //  Internal Methods
        //
        //------------------------------------------------------
 
        #region Internal Methods
 
        /// <summary>
        /// Use this to ensure we don't call Uri.IsFile on a relative URI.
        /// </summary>
        public bool IsFile
        {
            get
            {
                return !_isInternalCompositeFont && _fontUri.IsFile;
            }
        }
 
        public bool IsComposite
        {
            get
            {
                return _isComposite;
            }
        }
 
        public string GetUriString()
        {
            return _fontUri.GetComponents(UriComponents.AbsoluteUri, UriFormat.SafeUnescaped);
        }
 
        public string ToStringUpperInvariant()
        {
            return GetUriString().ToUpperInvariant();
        }
 
        public override int GetHashCode()
        {
            return HashFn.HashString(ToStringUpperInvariant(), 0);
        }
 
 
        public Uri Uri
        {
            get
            {
                return _fontUri;
            }
        }
 
        public bool IsAppSpecific
        {
            get
            {
                return Util.IsAppSpecificUri(_fontUri);
            }
        }
 
        internal long SkipLastWriteTime()
        {
            // clients may choose to use this temporary method because GetLastWriteTime call
            // results in touching the file system
            // we need to resurrect this code when we come up with a complete solution
            // for updating fonts on the fly
            return -1; // any non-zero value will do here
        }
 
        public DateTime GetLastWriteTimeUtc()
        {
            if (IsFile)
            {
                return Directory.GetLastWriteTimeUtc(_fontUri.LocalPath);
            }
 
            // Any special value will do here.
            return DateTime.MaxValue;
        }
 
        public UnmanagedMemoryStream GetUnmanagedStream()
        {
            if (IsFile)
            {
                FileMapping fileMapping = new FileMapping();
 
                fileMapping.OpenFile(_fontUri.LocalPath);
                return fileMapping;
            }
 
            byte[] bits;
 
            // Try our cache first.
            lock (_resourceCache)
            {
                bits = _resourceCache.Get(_fontUri);
            }
 
            if (bits == null)
            {
                Stream fontStream;
 
                if (_isInternalCompositeFont)
                {
                    // We should read this font from our framework resources
                    fontStream = GetCompositeFontResourceStream();
                }
                else
                {
                    WebResponse response = WpfWebRequestHelper.CreateRequestAndGetResponse(_fontUri);
                    fontStream = response.GetResponseStream();
                    if (String.Equals(response.ContentType, ObfuscatedContentType, StringComparison.Ordinal))
                    {
                        // The third parameter makes sure the original stream is closed
                        // when the deobfuscating stream is disposed.
                        fontStream = new DeobfuscatingStream(fontStream, _fontUri, false);
                    }
                }
 
                UnmanagedMemoryStream unmanagedStream = fontStream as UnmanagedMemoryStream;
 
                if (unmanagedStream != null)
                    return unmanagedStream;
 
                bits = StreamToByteArray(fontStream);
 
                fontStream?.Close();
            }
 
            lock (_resourceCache)
            {
                _resourceCache.Add(_fontUri, bits, false);
            }
 
            return ByteArrayToUnmanagedStream(bits);
        }
 
        /// <summary>
        /// Tries to open a file and throws exceptions in case of failures. This
        /// method is used to achieve the same exception throwing behavior after
        /// integrating DWrite.
        /// </summary>
        public void TestFileOpenable()
        {
            if (IsFile)
            {
                FileMapping fileMapping = new FileMapping();
 
                fileMapping.OpenFile(_fontUri.LocalPath);
                fileMapping.Close();
            }
        }
 
        public Stream GetStream()
        {
            if (IsFile)
            {
                FileMapping fileMapping = new FileMapping();
 
                fileMapping.OpenFile(_fontUri.LocalPath);
                return fileMapping;
            }
 
            byte[] bits;
 
            // Try our cache first.
            lock (_resourceCache)
            {
                bits = _resourceCache.Get(_fontUri);
            }
 
            if (bits != null)
                return new MemoryStream(bits);
 
            Stream fontStream;
 
            if (_isInternalCompositeFont)
            {
                // We should read this font from our framework resources
                fontStream = GetCompositeFontResourceStream();
            }
            else
            {
                WebRequest request = PackWebRequestFactory.CreateWebRequest(_fontUri);
                WebResponse response = request.GetResponse();
 
                fontStream = response.GetResponseStream();
                if (String.Equals(response.ContentType, ObfuscatedContentType, StringComparison.Ordinal))
                {
                    // The third parameter makes sure the original stream is closed
                    // when the deobfuscating stream is disposed.
                    fontStream = new DeobfuscatingStream(fontStream, _fontUri, false);
                }
            }
 
            return fontStream;
        }
 
        #endregion Internal Methods
 
        //------------------------------------------------------
        //
        //  Private Methods
        //
        //------------------------------------------------------
 
        #region Private Methods
 
        private static UnmanagedMemoryStream ByteArrayToUnmanagedStream(byte[] bits)
        {
            return new PinnedByteArrayStream(bits);
        }
 
        private static byte [] StreamToByteArray(Stream fontStream)
        {
            byte[] memoryFont;
 
            if (fontStream.CanSeek)
            {
                checked
                {
                    memoryFont = new byte[(int)fontStream.Length];
                    PackagingUtilities.ReliableRead(fontStream, memoryFont, 0, (int)fontStream.Length);
                }
            }
            else
            {
                // this is inefficient, but works for now
                // we need to spend more time to implement a more performant
                // version of this code
                // ideally this should be a part of loader functionality
 
                // Initial file read buffer size is set to 1MB.
                int fileReadBufferSize = 1024 * 1024;
                byte[] fileReadBuffer = new byte[fileReadBufferSize];
 
                // Actual number of bytes read from the file.
                int memoryFontSize = 0;
 
                for (; ; )
                {
                    int availableBytes = fileReadBufferSize - memoryFontSize;
                    if (availableBytes < fileReadBufferSize / 3)
                    {
                        // grow the fileReadBuffer
                        fileReadBufferSize *= 2;
                        byte[] newBuffer = new byte[fileReadBufferSize];
                        Array.Copy(fileReadBuffer, newBuffer, memoryFontSize);
                        fileReadBuffer = newBuffer;
                        availableBytes = fileReadBufferSize - memoryFontSize;
                    }
                    int numberOfBytesRead = fontStream.Read(fileReadBuffer, memoryFontSize, availableBytes);
                    if (numberOfBytesRead == 0)
                        break;
 
                    memoryFontSize += numberOfBytesRead;
                }
 
                // Actual number of bytes read from the file is less or equal to the file read buffer size.
                Debug.Assert(memoryFontSize <= fileReadBufferSize);
 
                if (memoryFontSize == fileReadBufferSize)
                    memoryFont = fileReadBuffer;
                else
                {
                    // Trim the array if needed to that it contains the right length.
                    memoryFont = new byte[memoryFontSize];
                    Array.Copy(fileReadBuffer, memoryFont, memoryFontSize);
                }
            }
 
            return memoryFont;
        }
 
        /// <summary>
        /// Retrieves internal CompositeFont resources from the appropriate DLL resources.
        /// </summary>
        /// <returns>A stream to the requested CompositeFont resources.</returns>
        private Stream GetCompositeFontResourceStream()
        {
            string fontFilename = _fontUri.OriginalString.Substring(_fontUri.OriginalString.LastIndexOf('/') + 1).ToLowerInvariant();
 
            var fontResourceAssembly = Assembly.GetExecutingAssembly();
            ResourceManager rm = new ResourceManager($"{fontResourceAssembly.GetName().Name}.g", fontResourceAssembly);
 
            return rm?.GetStream($"fonts/{fontFilename}");
        }
 
        #endregion Private Methods
 
        //------------------------------------------------------
        //
        //  Private Classes
        //
        //------------------------------------------------------
 
        #region Private Classes
 
        private class PinnedByteArrayStream : UnmanagedMemoryStream
        {
            internal PinnedByteArrayStream(byte [] bits)
            {
                _memoryHandle = GCHandle.Alloc(bits, GCHandleType.Pinned);
                
                unsafe
                {
                    Initialize(
	                    (byte *)_memoryHandle.AddrOfPinnedObject(),
	                    bits.Length, 
	                    bits.Length, 
	                    FileAccess.Read
                    );
                }
            }
 
            ~PinnedByteArrayStream()
            {
                Dispose(false);
            }
 
            protected override void Dispose(bool disposing)
            {
                base.Dispose(disposing);
 
                Debug.Assert(_memoryHandle.IsAllocated);
                _memoryHandle.Free();
            }
 
            private GCHandle    _memoryHandle;
        }
 
        #endregion Private Classes
 
        //------------------------------------------------------
        //
        //  Private Fields
        //
        //------------------------------------------------------
 
        #region Private Fields
 
        private bool _isComposite;
 
        /// <summary>
        /// Indicates that this composite font is to be read from internal WPF resources.
        /// </summary>
        private bool _isInternalCompositeFont;
 
        private Uri     _fontUri;
 
        private static SizeLimitedCache<Uri, byte[]> _resourceCache = new SizeLimitedCache<Uri, byte[]>(MaximumCacheItems);
 
        /// <summary>
        /// The maximum number of fonts downloaded from pack:// Uris.
        /// </summary>
        private const int MaximumCacheItems = 10;
        private const string ObfuscatedContentType = "application/vnd.ms-package.obfuscated-opentype";
 
        #endregion Private Fields
    }
}