ststeiger / PdfSharpCore

Port of the PdfSharp library to .NET Core - largely removed GDI+ (only missing GetFontData - which can be replaced with freetype2)
Other
1.08k stars 237 forks source link

MigraDoc Styles not applied properly #30

Closed Sappharad closed 5 years ago

Sappharad commented 5 years ago

When using Migradoc, all document styles appear to combined and the resulting PDF has a single font definition. Example code:

        Document currentDoc = new Document();
        DocumentRenderer renderer = new DocumentRenderer(currentDoc);
        Style normal = currentDoc.Styles["Normal"];
        normal.Font.Name = "Tahoma";
        normal.Font.Size = 10;
        normal.Font.Color = new Color(0, 0, 0);
        normal.Font.Bold = false;
        normal.Font.Italic = false;

        Style boldCap = currentDoc.Styles.AddStyle("BoldCaption", "Normal");
        boldCap.Font.Name = "Tahoma";
        boldCap.Font.Size = 10;
        boldCap.Font.Color = new Color(0, 0, 0);
        boldCap.Font.Bold = true;
        boldCap.Font.Italic = false;

        Section section = currentDoc.AddSection();
        section.PageSetup.Orientation = Orientation.Landscape;
        section.PageSetup.LeftMargin = Unit.FromInch(0.5);
        section.PageSetup.BottomMargin = Unit.FromInch(0.5);
        section.AddParagraph("This should be bold", boldCap.Name);
        section.AddParagraph("This is just normal", normal.Name);
        renderer.PrepareDocument();

        PdfDocument pdf = new PdfDocument();
        PdfPage page = pdf.AddPage();
        var gfx = XGraphics.FromPdfPage(page);
        renderer.RenderPage(gfx, 1, PageRenderOptions.All);
        pdf.Save("test.pdf");

Output: image Expected output: The normal style should not be bold.

I'm currently looking into this, but I wanted to post it here so it's documented and maybe someone already knows what's wrong.

P.S. This should be .NET Standard so it can be referenced from class libraries, not just application projects directly. .NET Core can only be referenced by .NET Core projects, Standard can be referenced from all projects. @onizet submitted a pull request for that change in December, it's probably worth using unless there's something wrong with it.

Sappharad commented 5 years ago

This is probably a given, but I neglected to mention that this works properly in the official PdfSharp 1.32 release.

My initial guess would be to look at the font resolver stuff, (https://github.com/ststeiger/PdfSharpCore/commit/e66aa21e79f4fe0b7faa5a70c95d1f8faef8d66d) but nothing jumps out.

Sappharad commented 5 years ago

Found the problem. FontResolver.cs ResolveTypeface() method.

In the loop through font files, there are cases to resolve Bold & Italic versions of the font which break out once it's found a matching font. But for plain (no bold, no italic) there is no case to break out once it has found a matching font, so it goes through the rest of the fonts in that family and whichever one matches last wins. In the case here, it seems bold is last so that wins.

To fix the problem, add an else clause for the Regular case with a break so that it stops looking once it's found the desired font.

There's probably more room for improvement as well, since it's looking by filename and expects a "b" ending for bold and Tahoma ends in bd. But adding the else gives me the behavior I was looking for.

I can submit a PR this week or next week unless you have additional advice.

startnow65 commented 5 years ago

Hi @Sappharad thanks for pointing this out. It would be great to have a PR from you to fix this. I'm sure @ststeiger would be happy to merge it.

Your note about having the library as .Net Standard is also very valid. Please take a look at the .NetStandard port and the nugget package

ststeiger commented 5 years ago

@Sappharad @startnow65: Yep, I would - if you send it today, I'm gonna merge it when I see it. The font-resolver is sketchy at best anyway, was just a quick hack that worked good enough for me.

@startnow65: What do you think - should I merge the request for netstandard 2.0 as well ? Or are any of you or that you know of still using NetCore < 2.0 ?

startnow65 commented 5 years ago

Thanks @ststeiger I think it can be merged. I use NetCore 2.1 myself, i'm sure most people would've moved to 2.0 and above. If not, they can still use an older version of the nugget anyway.

Sappharad commented 5 years ago

I'll try to get a PR in later today. Before I did that I was looking to see how ImageSharp reads fonts, since your brute force method seems like it would not match on fonts where the filename doesn't actually match the family name or was shortened to fit into the legacy 8.3 filename convention.

ImageSharp's code has multi-platform logic in SixLabors.Fonts that enumerates files in the fonts folder like yours does, but it actually reads the font files and gets the internal names & style information from them. Unfortunately you can't use their methods directly because they don't retain the path of the original font file or offer a way to access the raw data.

I'm not sure if anyone has already looked at that, but something like that seems like it would be a more permanent approach going forward. Would it be worth opening an issue with them to get something else exposed so you can pull font info via their library instead? Or just borrow the necessary code from them directly, but then you'd potentially have two libraries populating font lists.

The PR I'll submit is fine for the case I was trying to get working and I may stop there, but if you've got some plans for permanent solution to the font resolver I could try and help.

ststeiger commented 5 years ago

Merged it. I think on Windows, you could read the font file from the registry (HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Fonts i believe).

However, on Linux you'd have to read all the fontconfig files, and that's some work. I started once, but stopped after some time and never resumed the work to date. Now I just wonder where I left that test program :)

Don't know if Mac uses fontconfig, too, or if that would work on Android/iOS as well.

Sappharad commented 5 years ago

macOS uses fontconfig as well. I just tested fc-list on the latest macOS, as well as one from 7 years ago (oldest supported by .NET Core) and both worked. So I guess that's a feasible approach?

The reason I bought up the SixLabors.Fonts.* stuff is that you're already integrated with their code for images which uses it for IDrawingContext.DrawText() so you'd be letting them do all of the hard work of finding fonts for you. But even if you would be interested in an implementation against it, there's still a matter of getting them to expose the font data in a manner you can use. I haven't looked at vanilla PDFSharp to see how they get the font data, but I presume it's something similar to GetFontData. Since it's a valid use case, I would be willing to ask them for something like that or even send them a PR for it if you're interested.

ststeiger commented 5 years ago

@Sappharad: I've been that far. I've even digged in the wine-sources and implemented GetFontData with freetype (SharpFont is a C# binding for that - SharpFont doesn't work well with x64-windows, you need to change the data-types or use a patched version) https://gist.github.com/ststeiger/273341aebd29009f2b272b822b69563f

(it works - it returns the same byte-array as GetFontData on Windows - naturally only if you use the exact same .ttf file as windows does - ubuntu-restricted-extras apparently uses not the same ttf, or an old one)

ststeiger commented 5 years ago

As I fixed Issue 37, I took a look at SixLabors.Fonts. The font-resolver there seems not to be noteworthy more advanced than my own. All it does better is that it searches in more directories.

For later reference: implementation is in FontCollection

public FontFamily Find(string fontFamily)
{
    if (this.TryFind(fontFamily, out FontFamily result))
    {
        return result;
    }

    throw new FontFamilyNotFoundException(fontFamily);
}

public bool TryFind(string fontFamily, out FontFamily family)
{
    return this.families.TryGetValue(fontFamily, out family);
}

which is called from SystemFontCollection,

string[] paths = new[]
            {
                // windows directories
                "%SYSTEMROOT%\\Fonts",

                // linux directlty list
                "~/.fonts/",
                "/usr/local/share/fonts/",
                "/usr/share/fonts/",

                // mac fonts
                "~/Library/Fonts/",
                "/Library/Fonts/",
                "/Network/Library/Fonts/",
                "/System/Library/Fonts/",
                "/System Folder/Fonts/",
            };

which is called from SystemFonts

private static Lazy<SystemFontCollection> lazySystemFonts = new Lazy<SystemFontCollection>(() => new SystemFontCollection());
public static IReadOnlyFontCollection Collection => lazySystemFonts.Value;

which is called from the example program RenderText(new RendererOptions(SystemFonts.CreateFont("consolas", 72)) { TabWidth = 4 }, "xxxxxxxxxxxxxxxx\n\txxxx\txxxx\n\t\txxxxxxxx\n\t\t\txxxx");

Sappharad commented 5 years ago

I thought their code actually opens the font files and looks at the internal embedded name / style, whereas you just match based on the file name itself. My only remaining concern was what about fonts whose filenames are the old 8.3 windows naming and thus you won't find a match if their real names are longer.

The PR you merged fixed my problem since I just needed Tahoma bold and you could probably close this. I only mentioned their code since they appeared to get the actual font names as opposed to the file names.

Sappharad commented 5 years ago

Closing this since it is basically resolved. Feel free to re-open if you disagree / think it should be better.

ststeiger commented 5 years ago

@Sappharad: Yea, that would have been nice, but no... If it solved your problem, then it's good enough - for now ;)

gaganharjai commented 4 years ago

@ststeiger, @Sappharad: Thanks for the great work on PDFsharpcore. Though I am facing a problem when using Migradoc, it seems that I am unable to use bold, italic font definition. It works well for a windows environment, but when hosted on docker bold/italic properties are ignored.

Bogdancev commented 4 years ago

Hi gaganharjai

I'm not familiar with dockers, but I had the same problem with shared windows hostings.
They simply did not have fonts that my laptop has. I ended up adding font files (.ttf) to my project. For example, for normal, bold, italic and bold-italic "Arial" font you would need following files: arial.ttf, arialbd.ttf, ariali.ttf and arialbi.ttf These variations are handles in FontResolver:

public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
{
    // Ignore case of font names.
    var name = familyName.ToLower();

    // Deal with the fonts we know.
    switch (name)
    {
        case "arial":
            if (isBold)
            {
                if (isItalic)
                    return new FontResolverInfo("arialbi");
                return new FontResolverInfo("arialbd");
            }
            if (isItalic)
                return new FontResolverInfo("ariali");
            return new FontResolverInfo("arial");
...

I hope this helps.

gaganharjai commented 4 years ago

Thanks for the prompt response @Bogdancev. I believe You mean implementing a separate FontResolver Class like:

public class FileFontResolver : IFontResolver // FontResolverBase { public string DefaultFontName => "Tinos";

    public byte[] GetFont(string faceName)
    {
        using (var ms = new MemoryStream())
        {
            using (var fs = File.Open(faceName, FileMode.Open))
            {
                fs.CopyTo(ms);
                ms.Position = 0;
                return ms.ToArray();
            }
        }
    }

    public FontResolverInfo ResolveTypeface(string familyName, bool isBold, bool isItalic)
    {
        if (familyName.Equals("Tinos", StringComparison.CurrentCultureIgnoreCase))
        {
            if (isBold && isItalic)
            {
                return new FontResolverInfo("Fonts/Tinos-BoldItalic.ttf");
            }
            else if (isBold)
            {
                return new FontResolverInfo("Fonts/Tinos-Bold.ttf");
            }
            else if (isItalic)
            {
                return new FontResolverInfo("Fonts/Tinos-Italic.ttf");
            }
            else
            {
                return new FontResolverInfo("Fonts/Tinos-Regular.ttf");
            }
        }
        return null;
    }
}

}

and then using it in while printing like: GlobalFontSettings.FontResolver = new FileFontResolver(); GlobalFontSettings.FontResolver.ResolveTypeface("Tinos", true, true);

Here I get an Exception that: System.ArgumentException: 'An item with the same key has already been added. Key: rcon'.
If you think I am missing something here. If possible Could you please share the complete code snippet/details with me..?

Bogdancev commented 4 years ago

I do not use this line:

GlobalFontSettings.FontResolver.ResolveTypeface("Tinos", true, true);

My code is:

//Injecting it in constructor
public Report(PrivateFontResolver FontResolver)
{
    mFontResolver = FontResolver;
}

...

//Later assign it, probably I could do this in constructor above
GlobalFontSettings.FontResolver = mFontResolver;

And that is it.

gaganharjai commented 4 years ago

While using this i am getting an exception: System.InvalidOperationException: 'No appropriate font found. :(

Bogdancev commented 4 years ago

I remember I had this message too. Go into your FontResolver and put breakpoints in GetFont and ResolveTypeface methods. Just see step by step where it does not match.

gaganharjai commented 4 years ago

Breakpoint does not come in GetFont method. Could you confirm did you put the font file in the solution directory? like and have they any build action set:

The problem is when control comes in ResolveTypeface method. familyName has a value set to: "PlatformDefault" so it does not go into if block:

f (familyName.Equals("Tinos", StringComparison.CurrentCultureIgnoreCase)) {

image

Bogdancev commented 4 years ago

OK, that is your problem. First it comes to ResolveTypeface method, then to GetFont. But to go to GetFont, your font name must first match inside ResolveTypeface method.

Try to add this:

if("PlatformDefault" == familyName) 
    familyName = "Tinos";

before you check for a name in FontResolver.

I embedded my files into assembly, but that is not important, you are not even getting there yet. First match name.

gaganharjai commented 4 years ago

Also, If I use "Tinos" as default font like in font resolver class: public string DefaultFontName => "Tinos"; i debugged and see ResolveTypeface returns valid bold font. and GetFont also returns byte[] filled. but pdfRenderer.RenderDocument(); throws error: System.ArgumentException: 'An item with the same key has already been added. Key: rcon'

Bogdancev commented 4 years ago

This is how I render it:

Document document = CreateDocument();
PdfDocumentRenderer renderer = new PdfDocumentRenderer(true) { Document = document };
renderer.RenderDocument();

Just fix problems one by one. Once you did font, just render document with 1 paragraph of any text. Get something from MigraDoc examples.

gaganharjai commented 4 years ago

Yeah you are right, I have for have following code for printing: and now its going into GetFont method of the resolver and we receive a byte array for font as well.

GlobalFontSettings.FontResolver = new FileFontResolver(); Document document = new Document(); Section section = document.AddSection(); var abc = section.AddParagraph(); abc.Format.Font.Bold = true; abc.AddText("Data"); section.PageSetup.PageFormat = PageFormat.A4; PdfDocumentRenderer renderer = new PdfDocumentRenderer(true) { Document = document }; renderer.RenderDocument();

and I debugged in the PDFsharpCore dll and see Error comes after Getting the font from our custom resolver at XFontSource.GetOrCreateFrom:

System.ArgumentException: 'An item with the same key has already been added. Key: rcon'

// Case: Get font from custom font resolver and create font source. byte[] bytes = customFontResolver.GetFont(fontResolverInfo.FaceName); XFontSource fontSource = XFontSource.GetOrCreateFrom(bytes);

ststeiger commented 4 years ago

I think it should be possible to write a better font resolver using the code from LayoutFarm/Typography

Here are the important places to draw inspiration from: https://github.com/ststeiger/SvgRenderer/blob/master/SvgRenderer/Shared/TextServices/FontManagement.cs#L807 https://github.com/ststeiger/SvgRenderer/blob/master/SvgRenderer/Shared/TextServices/FontManagement.cs#L420 https://github.com/ststeiger/SvgRenderer/blob/master/SvgRenderer/Shared/TextServices/FontManagement.cs#L364 https://github.com/ststeiger/SvgRenderer/blob/master/SvgRenderer/Helpers/FontHelper.cs#L59