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

x/image/font/opentype: Face isn't compliant with font.Face #58252

Closed
ShadiestGoat opened this issue Feb 2, 2023 · 9 comments
Closed

x/image/font/opentype: Face isn't compliant with font.Face #58252

ShadiestGoat opened this issue Feb 2, 2023 · 9 comments
Labels
NeedsFix The path to resolution is known, but the work has not been done.
Milestone

Comments

@ShadiestGoat
Copy link

ShadiestGoat commented Feb 2, 2023

What version of Go are you using (go version)?

$ go version
go version go1.19.5 linux/amd64

Does this issue reproduce with the latest release?

Yes.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/shady/.cache/go-build"
GOENV="/home/shady/.config/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GOMODCACHE="/home/shady/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/shady/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/lib/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/lib/go/pkg/tool/linux_amd64"
GOVCS=""
GOVERSION="go1.19.5"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD="/dev/null"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -Wl,--no-gc-sections -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build457832407=/tmp/go-build -gno-record-gcc-switches"

What did you do?

I created a new face from opentype. Then, I called Glyph() on a non existing character.

What did you expect to see?

I expected it to return !ok, as stated in the font.Face documentation.

What did you see instead?

It returns ok.

@mknyszek mknyszek added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Feb 2, 2023
@mknyszek mknyszek changed the title golang.org/x/image/font/opentype: Face isn't compliant with font.Face x/image/font/opentype: Face isn't compliant with font.Face Feb 2, 2023
@gopherbot gopherbot added this to the Unreleased milestone Feb 2, 2023
@mknyszek
Copy link
Contributor

mknyszek commented Feb 2, 2023

I don't see an owner listed on https://dev.golang.org/owners, but most of the code seems to have been written by @nigeltao.

@ShadiestGoat
Copy link
Author

Hi! I proposed a small change here that should fix this issue.

@nigeltao
Copy link
Contributor

nigeltao commented Feb 4, 2023

I'm tempted to close this as Working As Intended.

Glyph index 0 is a valid index, and by convention it's the .notdef glyph used for missing characters. See:

Calling opentype.Face.Glyph with a rune that isn't covered by the font face should therefore arguably return ok == true and this .notdef glyph (typically an empty rectangle). The Face does contain a glyph for the rune argument. It just happens to be a fallback glyph.

There is a separate question of "if I have a string, what runes in it are covered by this Face (excluding .notdef glyphs)", but that is a much more complicated question, related to Harfbuzz-like layout and shaping and fontconfig-like font selection, and surely deserves separate API.

For example, Han Unification means that "does this Face have a non-zero glyph index for this rune" isn't sufficient to answer "is this the right font for this rune". At a minimum, this should probably involve some concept of the user's locale, but I don't think Go has a good cross-platform abstraction for this yet.

For example, Android's minikin library's author said that:

Much of the internal structure of Minikin is dedicated to itemization, which is done largely on the basis of the cmap coverage of the fonts in the fallback chain. Doing that query font-by-font for every character would be expensive, so there are fancy bitmap acceleration structures.

Again, font selection and coverage calculation is a bigger problem needing separate API.

@nigeltao
Copy link
Contributor

nigeltao commented Feb 4, 2023

Consider the opentype example_test.go file, changing the "e" in the "jelly" string to a Unicode U+2603 Snowman, and running go test:

diff --git a/font/opentype/example_test.go b/font/opentype/example_test.go
index 4d7ee85..c8d522a 100644
--- a/font/opentype/example_test.go
+++ b/font/opentype/example_test.go
@@ -46,7 +46,7 @@ func ExampleNewFace() {
                Dot:  fixed.P(startingDotX, startingDotY),
        }
        fmt.Printf("The dot is at %v\n", d.Dot)
-       d.DrawString("jel")
+       d.DrawString("j\u2603l")
        fmt.Printf("The dot is at %v\n", d.Dot)
        d.Src = image.NewUniform(color.Gray{0x7F})
        d.DrawString("ly")
  1. Without your patch, this draws something roughly like "j▯lly" with an empty rectangle.
  2. With your patch, this draws "jlly" with nothing at all between the "j" and the "l".

I think (1) is the better output.

@ShadiestGoat
Copy link
Author

ShadiestGoat commented Feb 4, 2023

Hello! Thank you for your response.

Glyph index 0 is a valid index, and by convention it's the .notdef glyph used for missing characters. See

I agree! Glyph index 0 is a valid index, however, it implies that the glyph you are trying to draw isn't present. The fact that the .notdef is being returned should mean that ok == false, as stated in the font.Face documentation.

It returns !ok if the face does not contain a glyph for r.

(Glyph, GlyphAdvance, GlyphBounds)

To me, this sounds like it must return an ok == false, and the .notdef glyph bounds/advance/mask/etc. After all, what is the point of the ok variable if not to determine that it is using a fallback glyph? The only other thing I see is for error handling, and to me, in an ideal world errors would mean itd draw the .notdef glyph.

The benefit of this patch is that libraries like multiface could work properly. It would make users be able to use fallback glyphs without the need for called Index() ourselves.

There is a separate question of "if I have a string, what runes in it are covered by this Face (excluding .notdef glyphs)", but that is a much more complicated question, related to Harfbuzz-like layout and shaping and fontconfig-like font selection, and surely deserves separate API.

For example, Han Unification means that "does this Face have a non-zero glyph index for this rune" isn't sufficient to answer "is this the right font for this rune". At a minimum, this should probably involve some concept of the user's locale, but I don't think Go has a good cross-platform abstraction for this yet.

There could be a separate structure and API for this, but I'm not sure to what extent that is relevant to the issue at hand.

I don't think this patch is significantly slower than before. It introduces 1-2 checks per function. The index() calls were already done before, it was just inline.

Regarding the example_test.go, you are right. That test should output something like j▯lly. However, that sounds like an issue that should be fixed, rather than an issue that should be used to cancel this pr. I will look into how to fix this as soon as I can.

@ShadiestGoat
Copy link
Author

Consider the opentype example_test.go file, changing the "e" in the "jelly" string to a Unicode U+2603 Snowman, and running go test:

diff --git a/font/opentype/example_test.go b/font/opentype/example_test.go
index 4d7ee85..c8d522a 100644
--- a/font/opentype/example_test.go
+++ b/font/opentype/example_test.go
@@ -46,7 +46,7 @@ func ExampleNewFace() {
                Dot:  fixed.P(startingDotX, startingDotY),
        }
        fmt.Printf("The dot is at %v\n", d.Dot)
-       d.DrawString("jel")
+       d.DrawString("j\u2603l")
        fmt.Printf("The dot is at %v\n", d.Dot)
        d.Src = image.NewUniform(color.Gray{0x7F})
        d.DrawString("ly")
1. Without your patch, this draws something roughly like "j▯lly" with an empty rectangle.

2. With your patch, this draws "jlly" with nothing at all between the "j" and the "l".

I think (1) is the better output.

This is an issue due to image/font/font.go, DrawString, !ok has a TODO and a continue. When the check is removed, it would still draw ▯. Its better not to do the check regardless, as for errors return a 0 advance, and .notdef values still return ▯.
I am however unsure of this. It sounds a bit out of scope, what do you think @nigeltao?

@ShadiestGoat
Copy link
Author

Hello, may I please get an update on this, @nigeltao?

@gopherbot
Copy link
Contributor

Change https://go.dev/cl/474376 mentions this issue: font: have Glyph return !ok for U+FFFD substitute

@dmitshur dmitshur added NeedsFix The path to resolution is known, but the work has not been done. and removed NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. labels Mar 15, 2023
@gopherbot
Copy link
Contributor

Change https://go.dev/cl/552616 mentions this issue: shiny/text: drop panic when GlyphAdvance returns !ok

gopherbot pushed a commit to golang/exp that referenced this issue Dec 26, 2023
As the commit message of CL 474376 notes, the "TODO: is falling back
on the U+FFFD glyph the responsibility of the Drawer or the Face?"
was resolved. There's no need to panic anymore.

Updates golang/go#58252.

Change-Id: Ic4bcc3dfb8a463e7abcfccdbcf826644327f1aec
Reviewed-on: https://go-review.googlesource.com/c/exp/+/552616
Auto-Submit: Dmitri Shuralyov <dmitshur@golang.org>
Reviewed-by: Nigel Tao (INACTIVE; USE @golang.org INSTEAD) <nigeltao@google.com>
Reviewed-by: Nigel Tao <nigeltao@golang.org>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsFix The path to resolution is known, but the work has not been done.
Projects
None yet
Development

No branches or pull requests

5 participants