Skip to content

Added automatic character range detection that determines font - #3155

Merged
bsweeney merged 4 commits into
dompdf:masterfrom
robvanderlee:enhancement-css-fallback
Jun 23, 2023
Merged

bsweeney merged 4 commits into
dompdf:masterfrom
robvanderlee:enhancement-css-fallback

Conversation

@robvanderlee

Copy link
Copy Markdown

No description provided.

@bsweeney bsweeney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think these suggested changes should help with performance. The changes I made are:

  • Always perform font mapping. This removes duplicate processing of text in those cases where the full block of text was not supported by the font. This imparts a slight performance hit in the case where the full text is supported by the font, but it's negligible (with my limited testing).
  • Only process as much text as necessary when mapping the font. Since in this particular use case we only want the first block of mapped text there's no need to map beyond that.
  • Commented out the building the string for each mapped block. That adds overhead for something we don't currently use. I think it could still be worthwhile, just not in this particular use case. Maybe we can optionally generated that content based on an additional argument?

Worth noting that the additional frame count resulting from the font mapping still impacts performance, but less so than before.

It may be possible to squeeze a bit more performance out of this by processing the string one character at a time instead of splitting it at the start. However, I think this would only improve performance on extremely long strings, which seems like an outlier.

Comment thread src/FrameReflower/Text.php Outdated
Comment thread src/FontMetrics.php Outdated
@robvanderlee
robvanderlee force-pushed the enhancement-css-fallback branch from 381bd55 to 43f4b87 Compare March 24, 2023 13:55

@bsweeney bsweeney left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated suggestions. A lot of the other functions would not longer be needed, though once we go 'round a few more times we can figure out what can be extracted.

Comment thread src/FrameReflower/Text.php Outdated
Comment thread src/FontMetrics.php Outdated
Comment thread src/FontMetrics.php Outdated
@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch 3 times, most recently from a6a0df8 to b775f96 Compare April 2, 2023 03:05
@Mellthas

Mellthas commented Apr 2, 2023

Copy link
Copy Markdown
Member

If you think this is ready for review, I can take a look at the changes.

@bsweeney

bsweeney commented Apr 3, 2023 •

Copy link
Copy Markdown
Member

Thanks ... I'm doing some last minute testing to see if I can squeeze out a bit more performance. Plus I need to update the render tests. But I think the basic logic is ready for review so have at it.

@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch from b775f96 to ed44712 Compare April 3, 2023 13:12
@bsweeney

bsweeney commented Apr 3, 2023 •

Copy link
Copy Markdown
Member

@robvanderlee FYI I rewrote the history to make this change a bit easier to follow.

There will always be a performance impact with this change since we're a) parsing each character of a text frame, and b) generating more frames (when required by font matching). I don't have a clean system for performance testing, but what testing I did do shows minimal impact on most documents. The greatest impact would be in the target scenario where the user just wants to specify a sequence of fonts and let Dompdf pick the right one. But even there the impact is not significant.

I tested with both PHP 7.1 and 8.2 using the current release of Dompdf and this branch with each of the backend options. The document I used was fairly simple in structure. Times below are in milliseconds.

The base document looks like this:

<!doctype html>
<html>
    <head>
        <link rel="preconnect" href="/api/browser/proxy?url=https%3A%2F%2Ffonts.googleapis.com">
        <link rel="preconnect" href="/api/browser/proxy?url=https%3A%2F%2Ffonts.gstatic.com" crossorigin>
        <link href="/api/browser/proxy?url=https%3A%2F%2Ffonts.googleapis.com%2Fcss2%3Ffamily%3DSawarabi%2BGothic%26amp%3Bdisplay%3Dswap" rel="stylesheet">
        <style>
            @font-face {
                font-family: 'Great Vibes';
                font-style: normal;
                font-weight: 400;
                src: url('https://www.1001fonts.com/download/font/great-vibes.regular.ttf') format('truetype');
            }
        </style>
        <style>
            @font-face {
                font-family: 'Extraoin Zhurdlyou Anarchy';
                src: url('https://www.1001fonts.com/download/font/extraoin-zhurdlyou.anarchy.ttf') format('truetype');
                font-style: normal;
                font-weight: 400;
            }
        </style>
        <style>
            body {
                font-family: 'Great Vibes', 'Extraoin Zhurdlyou Anarchy', 'Sawarabi Gothic', serif;
            }
            .japanese {
                font-family: 'Sawarabi Gothic', serif;
            }
            .cyrillic {
                font-family: 'Extraoin Zhurdlyou Anarchy', serif;
                font-size: 32px;
                font-weight: 400;
            }
        </style>
    </head>
    <body>
        <!-- text goes here -->
    </body>
</html>

First test, 100 paragraphs of lorem ipsum:

PHP v2.0.3/Cpdf vNext/Cpdf v2.0.3/PDFLib vNext/PDFLib v2.0.3/GD vNext/GD
7.1 641 704 420 671 11108 11178
8.2 595 608 319 582 6952 6969

Next test, a document with lots of manual font styling to render the text with the appropriate font. The document was 100 of the following paragraphs. This is not ideal since there is no text variability, but there aren't many lorem ipsum generators that work with multiple languages in the body content. Let alone segment the text appropriately.

<p><span class="japanese">能登</span> - <span class="cyrillic">Каждый</span> - <span class="japanese">半島 能登</span> - <span class="cyrillic">Каждый</span> - <span class="japanese">半島 能登</span> - This is great vibes font rendered text <span class="cyrillic">Каждый</span> - <span class="japanese">半島 能登</span> - <span class="cyrillic">Каждый</span> - <span class="japanese">半島 能登</span> - <span class="cyrillic">Каждый</span> - <span class="japanese">半島 能登</span></p>
PHP v2.0.3/Cpdf vNext/Cpdf v2.0.3/PDFLib vNext/PDFLib v2.0.3/GD vNext/GD
7.1 1124 1133 473 1276 4822 4876
8.2 810 822 429 905 3047 3083

Final test, the same as above without the any manual text styling.

<p>能登 - Каждый - 半島 能登 - Каждый - 半島 能登 - This is great vibes font rendered text Каждый - 半島 能登 - Каждый - 半島 能登 - Каждый - 半島</p>
PHP v2.0.3/Cpdf vNext/Cpdf v2.0.3/PDFLib vNext/PDFLib v2.0.3/GD vNext/GD
7.1 375 835 341 884 1615 5265
8.2 341 706 315 698 1160 3178

@Mellthas

Mellthas commented Apr 3, 2023

Copy link
Copy Markdown
Member

As a quick note, the render tests fail because bold and italic font styles are not applied anymore.

@bsweeney

bsweeney commented Apr 3, 2023 •

Copy link
Copy Markdown
Member

Thank for finding that. An embarrassing oversight on my part for sure. I neglected to include the font style/weight as part of the font mapping logic. Should be OK now. I obviously was too focused on a particular test case. I'm sure I would have figured that out ... eventually.

@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch 3 times, most recently from 8a31640 to aad3206 Compare April 4, 2023 12:04
@bsweeney

bsweeney commented Apr 4, 2023

Copy link
Copy Markdown
Member

Unit tests seem happy and most of the "core" test I have up in the debug helper look OK on first pass. I am seeing a slight difference in the rendering of some document (e.g., https://eclecticgeek.com/dompdf/core_tests/encoding_utf-8_w3.html) that I need to investigate.

Comment thread src/Helpers.php
Comment thread src/Helpers.php Outdated
@Mellthas

Mellthas commented Apr 4, 2023

Copy link
Copy Markdown
Member

Will look at the rest of the code later this week.

@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch from aad3206 to de8257f Compare April 5, 2023 14:47
@bsweeney

bsweeney commented Apr 5, 2023 •

Copy link
Copy Markdown
Member

Unit tests seem happy and most of the "core" test I have up in the debug helper look OK on first pass. I am seeing a slight difference in the rendering of some document (e.g., https://eclecticgeek.com/dompdf/core_tests/encoding_utf-8_w3.html) that I need to investigate.

Found two issues.

  • Non-printable characters weren't excluded from the font mapping. This was causing unnecessary frame splitting which was particularly problematic for preformatted (white-space: pre;) text.
  • Frame splitting performed around font mapping exposed an issue where unsupported characters in a font were not being included in text width calculations when using Cpdf as the backend. This was causing frame overlap when a frame consisted entirely of unsupported characters for the used font.

@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch from de8257f to bab1064 Compare April 6, 2023 12:22
Comment thread src/Adapter/CPDF.php Outdated
Comment thread src/Adapter/CPDF.php Outdated
Comment thread src/Canvas.php Outdated
Comment thread src/Adapter/GD.php Outdated
Comment thread src/Adapter/PDFLib.php Outdated
Comment thread src/FrameReflower/Text.php Outdated
Comment thread src/FrameReflower/Text.php Outdated
Comment thread src/FrameReflower/Text.php Outdated
Comment thread lib/Cpdf.php Outdated
Comment thread lib/Cpdf.php
@Mellthas

Mellthas commented Apr 8, 2023

Copy link
Copy Markdown
Member

The font-resolve logic is now essentially duplicated: We have the existing logic in the Style class and the new mapFontsToText, which also takes the current text into account. Some ideas on how to resolve this:

  • Change the font-family logic in the Style class to compute the declaration to an array of the font names instead of the first available font. Maybe the final font paths, so non-existing fonts would be dropped already.
  • The Text reflower would then set the final font name as the used value of the property, as it does now. Whether the property has been resolved could then be determined based on whether it returns an array or a string.
  • The question would then be how to handle the other places where font-family is checked on non-text frames, e.g. when determining the height of inline frames. Might be right to use the first available font there, as it is essentially done now, but I haven’t verified that.

But maybe this does not need to be addressed right now, and can be tackled later.

Comment thread src/FrameReflower/Text.php Outdated
Comment thread src/FontMetrics.php Outdated
@Mellthas

Mellthas commented Apr 8, 2023

Copy link
Copy Markdown
Member

One more thing: Shrink-to-fit width determination (for table columns, float, etc.) is now incorrect when text is split via the new logic, since get_min_max_width is called before reflow. That should be solvable by applying the split early if needed, but would need a way to determine whether the final font has been determined for a specific frame to avoid checking the text twice.

@robvanderlee

Copy link
Copy Markdown
Author

@bsweeney, @Mellthas great to see the improvements! I'll be sure to run some testing on my part and see If i can contribute to the review and/or make some suggested changes. I've had a busy couple weeks behind me but I'll make some time in this week to give a proper look.

@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch from bab1064 to 2a48504 Compare April 17, 2023 14:11
@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch from 2a48504 to 5e972a6 Compare April 17, 2023 15:25
Comment thread src/Adapter/PDFLib.php
Comment thread src/FrameReflower/Text.php
@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch from 5e972a6 to 235fc6c Compare April 18, 2023 16:12
@bsweeney

Copy link
Copy Markdown
Member

One more thing: Shrink-to-fit width determination (for table columns, float, etc.) is now incorrect when text is split via the new logic, since get_min_max_width is called before reflow. That should be solvable by applying the split early if needed, but would need a way to determine whether the final font has been determined for a specific frame to avoid checking the text twice.

There appear to be two issues related to this?

  1. Incorrect width determination since the used font may have differing widths than the first valid.
  2. When a frame is split the new frame is pushed to the next line.

I think you're right that performing the font mapping earlier in the process is probably necessary. I'll need to look at this some more.

bsweeney and others added 4 commits May 5, 2023 09:11
This method allows the user to determine if the characters in a string are fully supported by the specified font. This is arguably a FontMetrics operation, but since support is also dependent on the PDF back end this is, perhaps, a more robust option. Complimentary logic will be added to FontMetrics.

GD does not have a built-in method for determing support so logic to retrieve the character glyph mapping is borrowed from Cpdf.
With this change the text reflower scans the text of the frame to determine which font from the styled font families supports the characters contained within. Font preference is based on the order the fonts in the style declarations value. When characters in the text are supported by different fonts the frame is split at the character where the font changes. The used value of the current frame's font-family style is set to the supporting font.

fixes dompdf#3142
should help enable full unicode-range support (dompdf#913)
...since these should be represented in the text with the replacement character. Excludes non-printable characters.
@bsweeney
bsweeney force-pushed the enhancement-css-fallback branch from 235fc6c to 5646d0c Compare May 5, 2023 13:11
@bsweeney

bsweeney commented May 5, 2023 •

Copy link
Copy Markdown
Member

Update to address the shrink-to-fit issue by moving the font mapping logic to the decorator so that it can be called as necessary. Duplicated calls are avoided by recording the mapped font in a private property. I do think it would work better to handle recognition of mapping being complete by inspecting the used value ... somehow. I didn't see an obvious good way to do this so I'll leave that as an optimization for another time.

@bsweeney bsweeney added this to the 2.0.4 milestone Jun 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The fallback fonts dont work in dompdf when rendering a pdf in laravel Automated unicode range determination in font loading and selection

4 participants