mono / libgdiplus

C-based implementation of the GDI+ API
http://www.mono-project.com/
MIT License
334 stars 171 forks source link

Graphics.GetHeight(Graphics g) returns incorrect result when DpiX/Y != 96 #622

Open tig opened 4 years ago

tig commented 4 years ago

See this example program:

using System;
using System.Drawing;
using System.Runtime.InteropServices;

namespace DrawTest
{
    class Program
    {
        const string fontFamily = "Source Code Pro";
        const float fontSize = 10;
        const FontStyle fontStyle = FontStyle.Regular;

        [DllImport("libgdiplus", ExactSpelling = true)]
        internal static extern string GetLibgdiplusVersion();

        static void Main(string[] args)
        {
            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
                Console.WriteLine($"Using the real GDI+");
            else
                Console.WriteLine($"Using libgdiplus version: {GetLibgdiplusVersion()}");

            int dpi = 96; // Typical screen dpi
            Console.WriteLine($"------ Test @ {dpi} dpi ------");
            DoTest(dpi, GraphicsUnit.Pixel);
            //DoTest(dpi, GraphicsUnit.Display);

            //dpi = 100; // low resolution printer dpi
            //Console.WriteLine($"------ Test @ {dpi} dpi ------");
            //DoTest(dpi, GraphicsUnit.Pixel);
            ////DoTest(dpi, GraphicsUnit.Display);

            dpi = 600; // high res printer dpi
            Console.WriteLine($"------ Test @ {dpi} dpi ------");
            DoTest(dpi, GraphicsUnit.Pixel);
            //DoTest(dpi, GraphicsUnit.Display);
        }

        private static void DoTest(int res, GraphicsUnit unit)
        {
            using Bitmap bitmap = new Bitmap(1, 1);
            bitmap.SetResolution(res, res);
            var g = Graphics.FromImage(bitmap);
            g.PageUnit = unit;

            Console.WriteLine($"Graphics: PageUnit = {g.PageUnit}, PageScale = {g.PageScale}, DPI = {g.DpiX} x {g.DpiY}");

            // Calculate the number of lines per page.
            var font = new System.Drawing.Font(fontFamily, fontSize, fontStyle, GraphicsUnit.Point);
            //var font = new System.Drawing.Font(fontFamily, fontSize / 72F * 96F, fontStyle, GraphicsUnit.Pixel);
            Console.WriteLine($"Font: {font.Name}, {font.Size} in {font.Unit}s ({font.SizeInPoints}pts), {font.Style}.");

            var fi = new FontInfo(g, font);
            fi.Dump();

            // Font.GetHeight() assumes 96dpi
            Console.WriteLine($"  Height: GetHeight() = {font.GetHeight()}");

            // Font.GetHeight(res) should use res (and does on both Windows and libgdiplus 6.1)
            float fExpectedHeight = font.GetHeight(res);
            Console.WriteLine($"          GetHeight({res}) = {fExpectedHeight}");

            // Font.GetHeight(Graphics) honors Graphics.DpiY on Windows, but with libgdiplus 6.1 uses 96
            Console.WriteLine($"          GetHeight(g) = {font.GetHeight(g)}" + (fExpectedHeight == font.GetHeight(g) ? "" : " --- FAIL!"));

            var lineSize = g.MeasureString(font.Name, font);
            Console.WriteLine($"Size of \"{font.Name}\": {lineSize.Width} x {lineSize.Height}");

        }
    }

    public class FontInfo 
    {
        // Heights and positions in pixels.
        public float EmHeightPixels;
        public float AscentPixels;
        public float DescentPixels;
        public float CellHeightPixels;
        public float InternalLeadingPixels;
        public float LineSpacingPixels;
        public float ExternalLeadingPixels;

        // Distances from the top of the cell in pixels.
        public float RelTop;
        public float RelBaseline;
        public float RelBottom;

        public void Dump()
        {
            Console.WriteLine($"        EmHeightPixels = {EmHeightPixels}");
            Console.WriteLine($"         AscentPixels = {AscentPixels}");
            Console.WriteLine($"        DescentPixels = {DescentPixels}");
            Console.WriteLine($"     CellHeightPixels = {CellHeightPixels}");
            Console.WriteLine($"InternalLeadingPixels = {InternalLeadingPixels}");
            Console.WriteLine($"    LineSpacingPixels = {LineSpacingPixels}");
            Console.WriteLine($"ExternalLeadingPixels = {ExternalLeadingPixels}");
            Console.WriteLine($"               RelTop = {RelTop}");
            Console.WriteLine($"          RelBaseline = {RelBaseline}");
            Console.WriteLine($"            RelBottom = {RelBottom}");
        }

        // Initialize the properties.
        public FontInfo(Graphics gr, Font the_font)
        {
            float em_height = the_font.FontFamily.GetEmHeight(the_font.Style);
            EmHeightPixels = ConvertUnits(gr, the_font.Size, the_font.Unit, GraphicsUnit.Pixel);
            float design_to_pixels = EmHeightPixels / em_height;

            AscentPixels = design_to_pixels * the_font.FontFamily.GetCellAscent(the_font.Style);
            DescentPixels = design_to_pixels * the_font.FontFamily.GetCellDescent(the_font.Style);
            CellHeightPixels = AscentPixels + DescentPixels;
            InternalLeadingPixels = CellHeightPixels - EmHeightPixels;
            LineSpacingPixels = design_to_pixels * the_font.FontFamily.GetLineSpacing(the_font.Style);
            ExternalLeadingPixels = LineSpacingPixels - CellHeightPixels;

            RelTop = InternalLeadingPixels;
            RelBaseline = AscentPixels;
            RelBottom = CellHeightPixels;
        }

        // Convert from one type of unit to another.
        // I don't know how to do Display or World.
        private float ConvertUnits(Graphics gr, float value, GraphicsUnit from_unit, GraphicsUnit to_unit)
        {
            if (from_unit == to_unit) return value;

            // Convert to pixels. 
            switch (from_unit)
            {
                case GraphicsUnit.Document:
                    value *= gr.DpiX / 300;
                    break;
                case GraphicsUnit.Inch:
                    value *= gr.DpiX;
                    break;
                case GraphicsUnit.Millimeter:
                    value *= gr.DpiX / 25.4F;
                    break;
                case GraphicsUnit.Pixel:
                    // Do nothing.
                    break;
                case GraphicsUnit.Point:
                    value *= gr.DpiX / 72;
                    break;
                default:
                    throw new Exception("Unknown input unit " + from_unit.ToString() + " in FontInfo.ConvertUnits");
            }

            // Convert from pixels to the new units. 
            switch (to_unit)
            {
                case GraphicsUnit.Document:
                    value /= gr.DpiX / 300;
                    break;
                case GraphicsUnit.Inch:
                    value /= gr.DpiX;
                    break;
                case GraphicsUnit.Millimeter:
                    value /= gr.DpiX / 25.4F;
                    break;
                case GraphicsUnit.Pixel:
                    // Do nothing.
                    break;
                case GraphicsUnit.Point:
                    value /= gr.DpiX / 72;
                    break;
                default:
                    throw new Exception("Unknown output unit " + to_unit.ToString() + " in FontInfo.ConvertUnits");
            }

            return value;
        }
    }
}

Windows Result:

Using the real GDI+
------ Test @ 96 dpi ------
Graphics: PageUnit = Pixel, PageScale = 1, DPI = 96 x 96
Font: Source Code Pro, 10 in Points (10pts), Regular.
        EmHeightPixels = 13.333334
         AscentPixels = 13.12
        DescentPixels = 3.64
     CellHeightPixels = 16.76
InternalLeadingPixels = 3.4266663
    LineSpacingPixels = 16.76
ExternalLeadingPixels = 0
               RelTop = 3.4266663
          RelBaseline = 13.12
            RelBottom = 16.76
  Height: GetHeight() = 16.759996
          GetHeight(96) = 16.759996
          GetHeight(g) = 16.759996
Size of "Source Code Pro": 128.04442 x 18.426662
------ Test @ 600 dpi ------
Graphics: PageUnit = Pixel, PageScale = 1, DPI = 600 x 600
Font: Source Code Pro, 10 in Points (10pts), Regular.
        EmHeightPixels = 83.33333
         AscentPixels = 81.99999
        DescentPixels = 22.749998
     CellHeightPixels = 104.74999
InternalLeadingPixels = 21.416664
    LineSpacingPixels = 104.74999
ExternalLeadingPixels = 0
               RelTop = 21.416664
          RelBaseline = 81.99999
            RelBottom = 104.74999
  Height: GetHeight() = 16.759996
          GetHeight(600) = 104.749985
          GetHeight(g) = 104.749985
Size of "Source Code Pro": 800.2776 x 115.16665

Linux Result:

Using libgdiplus version: 6.1
------ Test @ 96 dpi ------
Graphics: PageUnit = Pixel, PageScale = 1, DPI = 96 x 96
Font: Source Code Pro, 10 in Points (10pts), Regular.
        EmHeightPixels = 13.333334
         AscentPixels = 13.12
        DescentPixels = 3.64
     CellHeightPixels = 16.76
InternalLeadingPixels = 3.4266663
    LineSpacingPixels = 16.76
ExternalLeadingPixels = 0
               RelTop = 3.4266663
          RelBaseline = 13.12
            RelBottom = 16.76
  Height: GetHeight() = 16.76
          GetHeight(96) = 16.76
          GetHeight(g) = 16.76
Size of "Source Code Pro": 120 x 18
------ Test @ 600 dpi ------
Graphics: PageUnit = Pixel, PageScale = 1, DPI = 600 x 600
Font: Source Code Pro, 10 in Points (10pts), Regular.
        EmHeightPixels = 83.33333
         AscentPixels = 81.99999
        DescentPixels = 22.749998
     CellHeightPixels = 104.74999
InternalLeadingPixels = 21.416664
    LineSpacingPixels = 104.74999
ExternalLeadingPixels = 0
               RelTop = 21.416664
          RelBaseline = 81.99999
            RelBottom = 104.74999
  Height: GetHeight() = 16.76
          GetHeight(600) = 104.75
          GetHeight(g) = 16.76 --- FAIL!
Size of "Source Code Pro": 120 x 18

Note this shows bugs in MeasureString too; I'll log a separate issue for that.

Workaround is to use Graphics.GetHeight(int resolution), but this doesn't help when you're using 3rd party assemblies.

tig commented 4 years ago

Related: https://github.com/mono/libgdiplus/issues/623