-
Notifications
You must be signed in to change notification settings - Fork 7
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
base: main
Are you sure you want to change the base?
Conversation
Implementation of color outlines in skrifa.
There was a problem hiding this 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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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, theScaler
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
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; |
There was a problem hiding this comment.
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).
There was a problem hiding this comment.
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.
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 |
Since landing googlefonts/fontations#609 and googlefonts/fontations#637 I have some ideas about how to support this. Let’s discuss f2f. |
@dfrg please feel free to amend, quick summary for this particular topic off the top of my head from our f2f:
Among the reasons for keeping the callback interface restricted to COLR:
|
@drott thanks for the summary. This all sounds good to me. |
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)
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)
* 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)
Regarding the stop normalization, it turns out we can only do that level of stop normalization and preparation that does not involve color interpolation.
|
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 |
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
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
[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
Proposed interface for implementation of color outlines in skrifa.
rendered