Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

API proposal for color outlines #48

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open

Conversation

dfrg
Copy link
Member

@dfrg dfrg commented Apr 26, 2023

Proposed interface for implementation of color outlines in skrifa.

rendered

dfrg added 2 commits April 26, 2023 15:19
Implementation of color outlines in skrifa.
Copy link

@drott drott left a comment

Choose a reason for hiding this comment

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

Thanks for this well-written proposal. I read it and left a few comments below.

> `canvas->restore();`<br>

```rust
fn push_clip(&mut self, glyph_id: Glyph_id, path: &Path)
Copy link

Choose a reason for hiding this comment

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

Is path a fully built path? This creates some redundancy with the scaler's path pen / callbacks. In the existing Skia COLRv1 implementation we branch out to the regular path drawing/retrieval with FreeType. Perhaps this callback should just provide the glyph id that should be fetched for the path to clip?
This would also allow other settings such as hinting mode etc. to still be in principle applicable when fetching those paths as a separate stage.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, the path has been fully loaded at this point. I settled on this design for a couple reasons.

First, while inside the ColorPen callbacks, the Scaler object (and it’s dependencies) have been mutably borrowed and Rust requires that access to be exclusive so you can’t effectively use it to load an outline without resorting to unsafe code.

Second, we need to load the paths internally anyway to compute the bounding box when clip boxes are missing from the table. This design allows us to do that only once and also cache outlines so that we’re not doing redundant work when one is used multiple times in a paint graph.

The Path type does have a method that takes a Pen so code for conversion to SkPath should be reusable.

Copy link
Member Author

Choose a reason for hiding this comment

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

I missed the point about hinting. Skia doesn’t currently hint these outlines. If that requirement changes, we can also handle that internally using the hinting mode that is set when configuring the scaler.

Copy link

Choose a reason for hiding this comment

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

In Fontations my impression is we generally follow an approach of providing independent, functional building blocks. I suggest we try to do the same here and decouple the color glyph processing and drawing from other independent parts at useful interfaces.

I am in favor of creating a separate GlyphPainter (name TBD) interface, that's independent of Scaler operations. Where the context of the Scaler is useful for outline caching, the client can use it and pass it in.

The GlyphPainter for example, regarding the need for glyph outline caching, the GlyphPainter can provide a bounds() method that accepts a Scaler as an argument, as it needs glyph contour access with glyph rasterisation settings configured. So, I'd suggest to remove the bounds callback from the ColorPen trait . Not only for the reason of decoupling this from the Scaler but because I think the bounds method does not really semantically belong in the ColorPen as that's not a typical Canvas-like operation.

First, while inside the ColorPen callbacks, the Scaler object (and it’s dependencies) have been mutably borrowed and Rust requires that access to be exclusive so you can’t effectively use it to load an outline without resorting to unsafe code.

See above, are there obstacles to decoupling this from Scaler?

Copy link

Choose a reason for hiding this comment

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

With this decoupled, we can remove the path: argument and have the client use Scaler for the purpose.

Copy link

Choose a reason for hiding this comment

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

Second, we need to load the paths internally anyway to compute the bounding box when clip boxes are missing from the table. This design allows us to do that only once and also cache outlines so that we’re not doing redundant work when one is used multiple times in a paint graph.

If outlines are needed, they can be cached in a passed-in Scaler's context, but for the purpose of bounding box calculation in the simple TrueType case, contour retrieval is not needed, is it? For the bounding box, then union of each glyphs xMin, xMax, yMin, yMax should be sufficient, which does not require path retrieval? - May be different for COLRv0/v1 + CFF.

Copy link
Member Author

Choose a reason for hiding this comment

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

In Fontations my impression is we generally follow an approach of providing independent, functional building blocks. I suggest we try to do the same here and decouple the color glyph processing and drawing from other independent parts at useful interfaces.

I think there is a slightly blurry line when discussing fontations— read-fonts is intended to provide low level building blocks but skrifa is meant to be a more complete solution. That said, I agree with decoupling this in some way. Finding the correct boundary is a bit of a challenge while taking constraints into account.

If outlines are needed, they can be cached in a passed-in Scaler's context, but for the purpose of bounding box calculation in the simple TrueType case, contour retrieval is not needed, is it?

In addition to CFF, variable fonts also need to load outlines for this.

Copy link

Choose a reason for hiding this comment

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

If outlines are needed, they can be cached in a passed-in Scaler's context, but for the purpose of bounding box calculation in the simple TrueType case, contour retrieval is not needed, is it? For the bounding box, then union of each glyphs xMin, xMax, yMin, yMax should be sufficient, which does not require path retrieval? - May be different for COLRv0/v1 + CFF.

Depends on whether you want tight or loose bounds for transformed glyphs.

Copy link
Member Author

Choose a reason for hiding this comment

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

Ah, this is a good point. We didn’t consider transforms in the graph.

/// Enumeration containing data for solid or gradient fills.
pub enum Brush<'a>;

/// Contains a glyph path for clips or fills. Offers a method that can feed
Copy link

Choose a reason for hiding this comment

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

See below, I think we can branch out for the existing scaler API for that?

pub struct Color;

/// Enumeration containing data for solid or gradient fills.
pub enum Brush<'a>;
Copy link

Choose a reason for hiding this comment

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

As discussed, it would be useful to see a bit more detail here on the value type and coordinates used here. Making this work well and be easily adaptable with the current Skia implementation would mean that ideally they'd stay quite close to what's in the font.

I think the adaptations to Cairo or Direct2D patterns/shaders and their constraints are different to the ones for Skia, for example.

Copy link
Member Author

Choose a reason for hiding this comment

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

I’ll add some definitions for this type. In the current prototype, the structure and values should exactly match those returned by FreeType with two exceptions: we do the 16.16 to f32 conversion for all values and the line projection for linear gradients (using the same code as Skia but without the y flip).

```rust
fn push_layer(&mut self, mode: Option<CompositeMode>)
```
Push a new layer to the state stack, optionally with a composite mode.
Copy link

Choose a reason for hiding this comment

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

Might make sense to describe here which side of the COLRv1 graph this traverses / which layer becomes which: I.e. what's sourcePaint what's backdrop paint and this usually maps a PaintComposite operation.

Copy link
Member Author

Choose a reason for hiding this comment

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

Great point! I’ll add some notes about exactly how PaintComposite maps to a sequence of callbacks.

```
Fill the specified path with the given brush. Note that this is an optimization
for the pattern `Clip Transform* Fill` in the graph. The intermediate
transforms are captured in the optional `brush_transform` parameter.
Copy link

Choose a reason for hiding this comment

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

Just FYI, the Skia version of this optimization does not support intermediate transforms, and would just drop the optimization and follow to the standard sequence of operations if there is a clip + transform + Paint*Gradient. So this latter parameter we might not need, or at least I haven't evaluated the performance benefit of that case.

Copy link
Member Author

Choose a reason for hiding this comment

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

Our own renderer does support this fast path so it would be nice to have it available if it’s not too cumbersome for Skia to special case here based on the presence of the brush transform.

Skia does a significant amount of
[processing](https://skia.googlesource.com/skia/+/265a074c910a0a4ac29176bca0a34615247f10f7/src/ports/SkFontHost_FreeType_common.cpp#597)
on gradient fills to match the expectations of the relevant shaders. The
applied transformations appear to produce equivalent brushes with color stops
Copy link

Choose a reason for hiding this comment

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

As discussed, my suspicion here is that other 2D libs have different expectations, in particular for the sweep type of gradients. And while the output is hopefully really equivalent, it may then introduce two layers of value conversion (first in Skrifa, then on the client side) when using this functionality with other 2D libraries. Perhaps we can at least look at Cairo and Direct2D to get a sense for that.

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, I need to do more investigation on this. My suspicion is that the work Skia does here is generally useful for most renderers but I don’t have the data to back that up. In the meantime, it seems reasonable to return gradient data as contained in the font and revisit when I have a better understanding of the API landscape.

@khaledhosny
Copy link

FWIW, the corresponding HarfBuzz API https://harfbuzz.github.io/harfbuzz-hb-paint.html

@dfrg
Copy link
Member Author

dfrg commented May 2, 2023

FWIW, the corresponding HarfBuzz API https://harfbuzz.github.io/harfbuzz-hb-paint.html

Thanks for this! It looks like there are a lot of similarities, particularly around how composite nodes are handled.

pub struct Transform;

/// 32-bit RGBA unpremultiplied color.
pub struct Color;
Copy link

Choose a reason for hiding this comment

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

If we don't do any gradient stop normalization or preparation for Skia specific shader behavior (which I find preferable due to differences), we don't need to interpolate any colors and we can decouple CPAL handling as well, and return color indices, and leave the rest to the client. I think this would be consistent with returning more values raw and as from the font.

As a side note, blackrenderer backends may be a good reference, but beware of gradient bugs in it, at least sweep is still broken (BlackFoundryCom/black-renderer#17).

Copy link
Member Author

Choose a reason for hiding this comment

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

Returning palette indices is definitely an option. If we do so, I would like skrifa to have a convenience method for resolving them.

@drott
Copy link

drott commented Oct 9, 2023

Separately, as discussed in b/301562314 and as the HarfBuzz API does it, it is probably useful to add a bitmap drawing / placement method to the ColorPen interface - but that can be a separate step.

@dfrg
Copy link
Member Author

dfrg commented Oct 10, 2023

Separately, as discussed in b/301562314 and as the HarfBuzz API does it, it is probably useful to add a bitmap drawing / placement method to the ColorPen interface - but that can be a separate step.

Since landing googlefonts/fontations#609 and googlefonts/fontations#637 I have some ideas about how to support this. Let’s discuss f2f.

@drott
Copy link

drott commented Oct 10, 2023

@dfrg please feel free to amend, quick summary for this particular topic off the top of my head from our f2f:

  • We'll add a separate interface to Skrifa as a COLRv0 and COLRv1 driver that flattens the graph, drives callbacks.
    • We'll keep the optimization for fill_path, but TBD whether we call it for PaintGlyph + PaintTransform + PaintSweep - maybe yes?
    • We'll do some normalizations where it seems useful for other 2D APIs (things that come to mind, stop normalization, 2 point linear gradients instead of 3 point.)
    • We don't deal with Path objects or actual color values on this interface, but pass glyph ids and color indices back to the client.
  • We'll separate out bounds computation to a separate function, likely with passing in Scaler or a successor to Scaler, such as ScalerInstance (Skia has a stage where it does bounding box calculation before, then only later calls into drawing.)
  • We don't include bitmap drawing right now, but keep it in mind to be potentially added to this interface.
  • @drott will draft a PR for such a new interface and test and develop locally based on that.
  • For decision on which parts we abstract behind this interface, @dfrg is offering take a look at some other 2D APIs such as Direct2D to decide what works well, and what's too Skia specific.

Among the reasons for keeping the callback interface restricted to COLR:

  • In this way, we are likely able to avoid extra allocations and caching - we have a driver for COLRv0 and COLRv1 that has low memory overhead and only needs to do circularity checks in a Hashset.
  • We provide more control to the application on glyph format selection, background on that: For monochromatic vs. color presentation emoji (in a hybrid font), or potentially in future CSS use cases where CSS allows glyph format selection, it's useful to have control over which glyphs are drawn, see also https://g-issues.skia.org/issues/40044044 and https://gitlab.freedesktop.org/freetype/freetype/-/issues/1142 where lack of glyph format selection in FreeType is a problem for Skia.

@dfrg
Copy link
Member Author

dfrg commented Oct 11, 2023

@drott thanks for the summary. This all sounds good to me.

drott added a commit to drott/fontations that referenced this pull request Oct 25, 2023
Use COLRv0/v1 test fonts from [1] to aid in the work of developing a
COLRv1/COLRv0 painter callback interface, as outlined in [2].

[1] https://github.com/googlefonts/color-fonts
[2] googlefonts/oxidize#48 (comment)
drott added a commit to drott/fontations that referenced this pull request Oct 26, 2023
Use COLRv0/v1 test fonts from [1] to aid in the work of developing a
COLRv1/COLRv0 painter callback interface, as outlined in [2].

[1] https://github.com/googlefonts/color-fonts
[2] googlefonts/oxidize#48 (comment)
drott added a commit to googlefonts/fontations that referenced this pull request Oct 26, 2023
* Add COLR test fonts

Use COLRv0/v1 test fonts from [1] to aid in the work of developing a
COLRv1/COLRv0 painter callback interface, as outlined in [2].

[1] https://github.com/googlefonts/color-fonts
[2] googlefonts/oxidize#48 (comment)
@drott
Copy link

drott commented Nov 21, 2023

Regarding the stop normalization, it turns out we can only do that level of stop normalization and preparation that does not involve color interpolation.

  • For sweep: We can normalize stops to start at 0 and end at 1 by normalizing angles, changing the meaning of angles from counter-clockwise to clockwise, and "flipping" the color line if start angle is greater than end angle. The rest would be handled client side.
  • For linear: We compute a new P3, so that the gradient is running from P0 to P3, and we can scale the vector from P0 to P3 so that stops are ranging from exactly 0 to 1.
  • For radial: We can only do the stop normalization, but with the result that we may send a negative angle to the client side. The negative angle may involve the need to truncate a color line and interpolate a color between two color stops. Details in the discussion here: Radial gradients and negative radii with variations or non-normal color stops colr-gradients-spec#367 (comment) - I think it's good to keep the separation of leaving color interpolation to the client, as working only with color indexes here is compatible with a potential future improvement of the CPAL table.

@dfrg
Copy link
Member Author

dfrg commented Nov 22, 2023

Per f2f, this sounds like a great compromise to provide as many useful transformations as possible without doing color interpolation.

Discussion also touched on using a SmallVec like container to avoid some allocations for the color stop array. Skrifa already has one that might be useful, possibly with modifications.

drott added a commit to drott/fontations that referenced this pull request Dec 8, 2023
COLRv1 implementation based on a `ColorPainter` trait object that a
client implements. When calling the `paint()` method on a
`ColorPaintable` the COLRv1 graph is traversed. Callbacks are sent to
the client to execute typical COLRv1 transform, clip and fill
operations.

Implements googlefonts/oxidize#48
drott added a commit to drott/fontations that referenced this pull request Dec 8, 2023
COLRv1 implementation based on a `ColorPainter` trait object that a
client implements. When calling the `paint()` method on a
`ColorPaintable` the COLRv1 graph is traversed. Callbacks are sent to
the client to execute typical COLRv1 transform, clip and fill
operations.

Implements googlefonts/oxidize#48
drott added a commit to googlefonts/fontations that referenced this pull request Dec 11, 2023
[skrifa] Callback-based COLRv1 implementation

COLRv1 implementation based on a `ColorPainter` trait object that a
client implements. When calling the `paint()` method on a
`ColorPaintable` the COLRv1 graph is traversed. Callbacks are sent to
the client to execute typical COLRv1 transform, clip and fill
operations.

Implements googlefonts/oxidize#48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants