opentypejs / opentype.js

Read and write OpenType fonts using JavaScript.
https://opentype.js.org/
MIT License
4.36k stars 466 forks source link

Character rendering issues occur when stroking text. #707

Open ILOVEPIE opened 2 months ago

ILOVEPIE commented 2 months ago

Character rendering issues occur when stroking text.

Expected Behavior

The stroke should match the fill.

Current Behavior

Random dots of stroke appear sometimes, also some lines cut inwards into the fill. cutting_in cutting_in_2 dots

Possible Solution

Unknown

Steps to Reproduce (for bugs)

I'll provide a link to the other maintainers so we can narrow down a test-case.

Context

I need to provide psychovisually lossless rendering of Advanced SubStation Alpha, this bug is causing issues for me.

Your Environment

axkibe commented 1 month ago

Work for me.

index.html:

<html>
    <script src="opentype.js" defer></script>
    <script src="script.js" defer></script>
<body>
<canvas id="canvas"></canvas>
</body>
</html>

script.js:

window.onload =
    async function( )
{
    const buffer = await ( await fetch('franklin.ttf') ).arrayBuffer( );
    const font = opentype.parse( buffer );

    const canvas = document.getElementById( 'canvas' );
    canvas.width = 1000;
    canvas.height = 400;
    const cx = canvas.getContext( '2d' );

    const path = font.getPath( 'Japan! Tokyo', 10, 80, 100, { } );
    path.stroke = 'blue';
    path.strokeWidth = 3;
    path.fill = 'lightgray';
    cx.lineJoin = 'round';
    cx.lineCap = 'round';
    path.draw( cx );
}

franklin.ttf: from here: https://fontsgeek.com/fonts/Franklin-Gothic-Book-Regular

result: Screenshot from 2024-05-09 07-46-18

PS: What I do noticed, is that some glyph would benefit from calling closePath() what I believe is not done.

Otherwise can you modify my minimal example so it makes the error you are seeing?

PPS: for completness, just put the files in a directory for example called web and npm install http-server ./node_modules/.bin/http-server -c-1 web/ to run it.

ILOVEPIE commented 1 month ago

This issue was mostly created so that I could keep track of my progress looking into the issue, but if you're willing to help me track this down, thanks. The way I'm doing this is iterating through the glyphs and drawing them individually instead of the whole thing all at once (because I have to composite them with webgl). The version of the font I'm using is identical to the one you found. Here's a simplified version of the drawGlyph function I'm using:


function drawGlyph (glyph, offsetX, offsetY, stroke) {
    const fontSize = this._fontInfo.size;
    const fontUnitsScale = this._fontInfo.font.unitsPerEm || 1000;
    const fontSizeMultiplier = fontSize / fontUnitsScale;
    const yoffset =
        this._fontInfo.font.ascender * fontSizeMultiplier;
    const path = glyph.getPath(
        offsetX,
        offsetY + yoffset,
        fontSize
    );
    path.fill = null;
    path.stroke = null;
    path.draw(this._ctx);
    if (stroke) {
        this._ctx.stroke();
    } else {
        this._ctx.fill();
    }
}
ILOVEPIE commented 1 month ago

I'm going to have to adapt it for variable fonts, but that comes after resolving this bug. adding closePath doesn't seem to fix it

Connum commented 1 month ago

What I do noticed, is that some glyph would benefit from calling closePath() what I believe is not done.

Do you have examples for this? While implementing variable fonts, I added a performance optimisation so that closePath() is only called when a glyph has a stroke applied, because the command is automatically executed for filled fonts, which resulted in unnecessary overhead because it was then called twice.

axkibe commented 1 month ago

What I do noticed, is that some glyph would benefit from calling closePath() what I believe is not done.

Do you have examples for this? While implementing variable fonts, I added a performance optimisation so that closePath() is only called when a glyph has a stroke applied, because the command is automatically executed for filled fonts, which resulted in unnecessary overhead because it was then called twice.

Well I guess there you have the issue, ILOVEPIE introduced it by setting stroke to null, then stroking himself. So paths are not closed.

BTW: I really dislike the API design in the first place. The API should simply be path.sketch() or follow() or whatever verb matches, and the caller should then call stroke and or fill. As you seen in my example the lineJoin/lineCap things are missing for example anyway.

When I was testing this with lineCaps as butts the ! for example wasn't closed.

ILOVEPIE commented 1 month ago

Yeah, There's some issues with the drawing code. I'll rework it some.

axkibe commented 1 month ago

BTW: If all you need it filling and stroking of text.. do you need opentype at all? canvas.fillText and canvas.strokeText should be fine for this. (I needed opentype.js for rendering, because I needed endless zooming where these functions had a size limit and they are missing getAdvanceWith() for caret based editors, but otherwise for this example I see it as unnessary complication to use this library at all..)

ILOVEPIE commented 4 weeks ago

BTW: If all you need it filling and stroking of text.. do you need opentype at all? canvas.fillText and canvas.strokeText should be fine for this. (I needed opentype.js for rendering, because I needed endless zooming where these functions had a size limit and they are missing getAdvanceWith() for caret based editors, but otherwise for this example I see it as unnessary complication to use this library at all..)

I have to use OpenType.js because I have to have access to the information required to render it like Windows does because I have to emulate Windows font rendering for my project.

axkibe commented 3 weeks ago

OK, how is it different than canvas with converted to .woff does?

talltyler commented 1 week ago

From my understanding this line https://github.com/opentypejs/opentype.js/blob/master/src/glyph.mjs#L235 should be something like this } else if (cmd.type === 'Z' && ((p.stroke && p.strokeWidth) || options.closePaths)) { to enable drawing with completed paths outside of the internal drawing code, this maybe could also be done if options.drawSVG is true. What do you all think? Should I make a PR?