feat: render initials avatar using media server (#3513)

This commit is contained in:
yqrashawn 2023-08-14 21:04:04 +08:00 committed by GitHub
parent 57b4036da3
commit 6cf0162877
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 8394 additions and 133 deletions

95
images/color.go Normal file
View File

@ -0,0 +1,95 @@
package images
import (
"fmt"
"image/color"
"strconv"
"strings"
)
func ParseColor(colorStr string) (color.RGBA, error) {
var c color.RGBA
if strings.HasPrefix(colorStr, "#") {
// Parse hex color string
// Remove "#" prefix
colorStr = colorStr[1:]
// Convert to RGBA
val, err := strconv.ParseUint(colorStr, 16, 32)
if err != nil {
return c, err
}
c.R = uint8(val >> 16)
c.G = uint8(val >> 8)
c.B = uint8(val)
c.A = 255
} else if strings.HasPrefix(colorStr, "rgb(") {
// Parse RGB color string
// Remove prefix and suffix
colorStr = strings.TrimSuffix(strings.TrimPrefix(colorStr, "rgb("), ")")
// Split the string into comma separated parts
parts := strings.Split(colorStr, ",")
if len(parts) != 3 {
return c, fmt.Errorf("invalid RGB color string")
}
// Convert to RGBA
r, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return c, err
}
g, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return c, err
}
b, err := strconv.Atoi(strings.TrimSpace(parts[2]))
if err != nil {
return c, err
}
c.R = uint8(r)
c.G = uint8(g)
c.B = uint8(b)
c.A = 255
} else if strings.HasPrefix(colorStr, "rgba(") {
// Parse RGBA color string
// Remove prefix and suffix
colorStr = strings.TrimSuffix(strings.TrimPrefix(colorStr, "rgba("), ")")
// Split the string into comma separated parts
parts := strings.Split(colorStr, ",")
if len(parts) != 4 {
return c, fmt.Errorf("invalid RGBA color string")
}
// Convert to RGBA
r, err := strconv.Atoi(strings.TrimSpace(parts[0]))
if err != nil {
return c, err
}
g, err := strconv.Atoi(strings.TrimSpace(parts[1]))
if err != nil {
return c, err
}
b, err := strconv.Atoi(strings.TrimSpace(parts[2]))
if err != nil {
return c, err
}
a, err := strconv.ParseFloat(strings.TrimSpace(parts[3]), 64)
if err != nil {
return c, err
}
if a < 0 || a > 1 {
return c, fmt.Errorf("invalid RGBA alpha value")
}
c.R = uint8(r)
c.G = uint8(g)
c.B = uint8(b)
c.A = uint8(a * 255)
} else {
return c, fmt.Errorf("invalid color string format")
}
return c, nil
}

48
images/color_test.go Normal file
View File

@ -0,0 +1,48 @@
package images
import (
"image/color"
"testing"
)
func TestParseColor(t *testing.T) {
// Test hex color string format
hexColor := "#FF00FF"
expectedResult := color.RGBA{R: 255, G: 0, B: 255, A: 255}
result, err := ParseColor(hexColor)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if result != expectedResult {
t.Errorf("unexpected result: %v (expected %v)", result, expectedResult)
}
// Test RGB color string format
rgbColor := "rgb(255, 0, 255)"
expectedResult = color.RGBA{R: 255, G: 0, B: 255, A: 255}
result, err = ParseColor(rgbColor)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if result != expectedResult {
t.Errorf("unexpected result: %v (expected %v)", result, expectedResult)
}
// Test RGBA color string format
rgbaColor := "rgba(255, 0, 255, 1)"
expectedResult = color.RGBA{R: 255, G: 0, B: 255, A: 255}
result, err = ParseColor(rgbaColor)
if err != nil {
t.Errorf("unexpected error: %s", err)
}
if result != expectedResult {
t.Errorf("unexpected result: %v (expected %v)", result, expectedResult)
}
// Test invalid color string format
invalidColor := "blah"
_, err = ParseColor(invalidColor)
if err == nil {
t.Errorf("expected error, but got none")
}
}

84
images/initials.go Normal file
View File

@ -0,0 +1,84 @@
package images
import (
"bytes"
"image/color"
"image/png"
"io/ioutil"
"strings"
"github.com/fogleman/gg"
"golang.org/x/image/font"
"golang.org/x/image/font/opentype"
)
type RGBA struct {
R, G, B, A float64
}
var parsedFont *opentype.Font = nil
func ExtractInitials(fullName string, amountInitials int) string {
if fullName == "" {
return ""
}
var initials strings.Builder
namesList := strings.Fields(fullName)
for _, name := range namesList {
if len(initials.String()) >= amountInitials {
break
}
if name != "" {
initials.WriteString(strings.ToUpper(name[0:1]))
}
}
return initials.String()
}
// GenerateInitialsImage uppercaseRatio is <height of any upper case> / dc.FontHeight() (line height)
// 0.60386123 for Inter-UI-Medium.otf
func GenerateInitialsImage(initials string, bgColor, fontColor color.Color, fontFile string, size int, fontSize float64, uppercaseRatio float64) ([]byte, error) {
// Load otf file
fontBytes, err := ioutil.ReadFile(fontFile)
if err != nil {
return nil, err
}
if parsedFont == nil {
parsedFont, err = opentype.Parse(fontBytes)
if err != nil {
return nil, err
}
}
halfSize := float64(size / 2)
dc := gg.NewContext(size, size)
dc.DrawCircle(halfSize, halfSize, halfSize)
dc.SetColor(bgColor)
dc.Fill()
// Load font
face, err := opentype.NewFace(parsedFont, &opentype.FaceOptions{
Size: fontSize,
DPI: 72,
Hinting: font.HintingNone,
})
if err != nil {
return nil, err
}
dc.SetFontFace(face)
// Draw initials
dc.SetColor(fontColor)
dc.DrawStringAnchored(initials, halfSize, halfSize, 0.5, uppercaseRatio/2)
img := dc.Image()
buffer := new(bytes.Buffer)
err = png.Encode(buffer, img)
if err != nil {
return nil, err
}
return buffer.Bytes(), nil
}

27
images/initials_test.go Normal file
View File

@ -0,0 +1,27 @@
package images
import (
"testing"
)
func TestExtractInitials(t *testing.T) {
testCases := []struct {
fullName string
amountInitials int
expectedInitials string
}{
{"John Doe", 1, "J"},
{"John Doe", 2, "JD"},
{"John Doe", 2, "JD"},
{"Jane ", 2, "J"},
{"Xxxx", 2, "X"},
{"", 2, ""},
}
for _, tc := range testCases {
actualInitials := ExtractInitials(tc.fullName, tc.amountInitials)
if actualInitials != tc.expectedInitials {
t.Errorf("Unexpected result for %q with %d initials, expected %q but got %q", tc.fullName, tc.amountInitials, tc.expectedInitials, actualInitials)
}
}
}

View File

@ -137,6 +137,33 @@ func ImageToBytes(imagePath string) ([]byte, error) {
return imgBuffer.Bytes(), nil return imgBuffer.Bytes(), nil
} }
func ImageToBytesAndImage(imagePath string) ([]byte, image.Image, error) {
// Open the image file
file, err := os.Open(imagePath)
if err != nil {
return nil, nil, err
}
defer file.Close()
// Decode the image
img, _, err := image.Decode(file)
if err != nil {
return nil, nil, err
}
// Create a new buffer to hold the image data
var imgBuffer bytes.Buffer
// Encode the image to the desired format and save it in the buffer
err = png.Encode(&imgBuffer, img)
if err != nil {
return nil, nil, err
}
// Return the image data as a byte slice
return imgBuffer.Bytes(), img, nil
}
func AddPadding(img image.Image, padding int) *image.RGBA { func AddPadding(img image.Image, padding int) *image.RGBA {
bounds := img.Bounds() bounds := img.Bounds()
newBounds := image.Rect(bounds.Min.X-padding, bounds.Min.Y-padding, bounds.Max.X+padding, bounds.Max.Y+padding) newBounds := image.Rect(bounds.Min.X-padding, bounds.Min.Y-padding, bounds.Max.X+padding, bounds.Max.Y+padding)
@ -192,6 +219,21 @@ func CreateCircleWithPadding(img image.Image, padding int) *image.RGBA {
return circle return circle
} }
func RoundCrop(inputImage []byte) ([]byte, error) {
img, _, err := image.Decode(bytes.NewReader(inputImage))
if err != nil {
return nil, err
}
result := CreateCircleWithPadding(img, 0)
var outputImage bytes.Buffer
err = png.Encode(&outputImage, result)
if err != nil {
return nil, err
}
return outputImage.Bytes(), nil
}
func PlaceCircleInCenter(paddedImg, circle *image.RGBA) *image.RGBA { func PlaceCircleInCenter(paddedImg, circle *image.RGBA) *image.RGBA {
bounds := circle.Bounds() bounds := circle.Bounds()
centerX := (paddedImg.Bounds().Min.X + paddedImg.Bounds().Max.X) / 2 centerX := (paddedImg.Bounds().Min.X + paddedImg.Bounds().Max.X) / 2

View File

@ -0,0 +1,56 @@
package images
import (
"bytes"
"image"
"image/color"
"image/png"
"math"
"github.com/fogleman/gg"
)
func AddStatusIndicatorToImage(inputImage []byte, innerColor color.Color, indicatorSize, indicatorBorder float64) ([]byte, error) {
// decode the input image
img, _, err := image.Decode(bytes.NewReader(inputImage))
if err != nil {
return nil, err
}
// get the dimensions of the image
width := img.Bounds().Max.X
height := img.Bounds().Max.Y
indicatorRadius := indicatorSize / 2
// calculate the center point
x := float64(width) - indicatorRadius
y := float64(height) - indicatorRadius
// create a new gg.Context instance
dc := gg.NewContext(width, height)
dc.DrawImage(img, 0, 0)
// Loop through each pixel in the hole and set it to transparent
dc.SetColor(color.Transparent)
for i := x - indicatorRadius; i <= x+indicatorRadius; i++ {
for j := y - indicatorRadius; j <= y+indicatorRadius; j++ {
if math.Pow(i-x, 2)+math.Pow(j-y, 2) <= math.Pow(indicatorRadius, 2) {
dc.SetPixel(int(i), int(j))
}
}
}
// draw inner circle
dc.DrawCircle(x, y, indicatorRadius-indicatorBorder)
dc.SetColor(innerColor)
dc.Fill()
// encode the modified image as PNG and return as []byte
var outputImage bytes.Buffer
err = png.Encode(&outputImage, dc.Image())
if err != nil {
return nil, err
}
return outputImage.Bytes(), nil
}

View File

@ -4,10 +4,14 @@ import (
"bytes" "bytes"
"database/sql" "database/sql"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"image" "image"
"image/color"
"net/http" "net/http"
"net/url" "net/url"
"os"
"path/filepath"
"strconv" "strconv"
"time" "time"
@ -34,6 +38,7 @@ const (
// Handler routes for pairing // Handler routes for pairing
accountImagesPath = "/accountImages" accountImagesPath = "/accountImages"
accountInitialsPath = "/accountInitials"
contactImagesPath = "/contactImages" contactImagesPath = "/contactImages"
generateQRCode = "/GenerateQRCode" generateQRCode = "/GenerateQRCode"
) )
@ -52,57 +57,403 @@ func handleRequestDownloaderMissing(logger *zap.Logger) http.HandlerFunc {
} }
} }
func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc { type ImageParams struct {
return func(w http.ResponseWriter, r *http.Request) { KeyUID string
params := r.URL.Query() PublicKey string
ImageName string
ImagePath string
FullName string
InitialsLength int
FontFile string
FontSize float64
Color color.Color
BgSize int
BgColor color.Color
UppercaseRatio float64
Theme ring.Theme
Ring bool
IndicatorSize float64
IndicatorBorder float64
IndicatorColor color.Color
keyUids, ok := params["keyUid"] AuthorID string
if !ok || len(keyUids) == 0 { URL string
logger.Error("no keyUid") MessageID string
return AttachmentID string
}
imageNames, ok := params["imageName"] Hash string
if !ok || len(imageNames) == 0 { Download bool
logger.Error("no imageName")
return
} }
identityImage, err := multiaccountsDB.GetIdentityImage(keyUids[0], imageNames[0]) func ParseImageParams(logger *zap.Logger, params url.Values) ImageParams {
parsed := ImageParams{}
parsed.Color = color.Transparent
parsed.BgColor = color.Transparent
parsed.IndicatorColor = color.Transparent
parsed.UppercaseRatio = 1.0
keyUids := params["keyUid"]
if len(keyUids) != 0 {
parsed.KeyUID = keyUids[0]
}
pks := params["publicKey"]
if len(pks) != 0 {
parsed.PublicKey = pks[0]
}
imageNames := params["imageName"]
if len(imageNames) != 0 {
if filepath.IsAbs(imageNames[0]) {
if _, err := os.Stat(imageNames[0]); err == nil {
parsed.ImagePath = imageNames[0]
} else if errors.Is(err, os.ErrNotExist) {
logger.Error("ParseParams: image not exit", zap.String("imageName", imageNames[0]))
return parsed
} else {
logger.Error("ParseParams: failed to read image", zap.String("imageName", imageNames[0]), zap.Error(err))
return parsed
}
} else {
parsed.ImageName = imageNames[0]
}
}
names := params["name"]
if len(names) != 0 {
parsed.FullName = names[0]
}
parsed.InitialsLength = 2
amountInitialsStr := params["length"]
if len(amountInitialsStr) != 0 {
amountInitials, err := strconv.Atoi(amountInitialsStr[0])
if err != nil { if err != nil {
logger.Error("handleAccountImages: failed to load image.", zap.String("keyUid", keyUids[0]), zap.String("imageName", imageNames[0]), zap.Error(err)) logger.Error("ParseParams: invalid initials length")
return parsed
}
parsed.InitialsLength = amountInitials
}
fontFiles := params["fontFile"]
if len(fontFiles) != 0 {
if _, err := os.Stat(fontFiles[0]); err == nil {
parsed.FontFile = fontFiles[0]
} else if errors.Is(err, os.ErrNotExist) {
logger.Error("ParseParams: font file not exit", zap.String("FontFile", fontFiles[0]))
return parsed
} else {
logger.Error("ParseParams: font file not exit", zap.String("FontFile", fontFiles[0]), zap.Error(err))
return parsed
}
}
fontSizeStr := params["fontSize"]
if len(fontSizeStr) != 0 {
fontSize, err := strconv.ParseFloat(fontSizeStr[0], 64)
if err != nil {
logger.Error("ParseParams: invalid fontSize", zap.String("FontSize", fontSizeStr[0]))
return parsed
}
parsed.FontSize = fontSize
}
colors := params["color"]
if len(colors) != 0 {
textColor, err := images.ParseColor(colors[0])
if err != nil {
logger.Error("ParseParams: invalid color", zap.String("Color", colors[0]))
return parsed
}
parsed.Color = textColor
}
sizeStrs := params["size"]
if len(sizeStrs) != 0 {
size, err := strconv.Atoi(sizeStrs[0])
if err != nil {
logger.Error("ParseParams: invalid size", zap.String("size", sizeStrs[0]))
return parsed
}
parsed.BgSize = size
}
bgColors := params["bgColor"]
if len(bgColors) != 0 {
bgColor, err := images.ParseColor(bgColors[0])
if err != nil {
logger.Error("ParseParams: invalid bgColor", zap.String("BgColor", bgColors[0]))
return parsed
}
parsed.BgColor = bgColor
}
uppercaseRatioStr := params["uppercaseRatio"]
if len(uppercaseRatioStr) != 0 {
uppercaseRatio, err := strconv.ParseFloat(uppercaseRatioStr[0], 64)
if err != nil {
logger.Error("ParseParams: invalid uppercaseRatio", zap.String("uppercaseRatio", uppercaseRatioStr[0]))
return parsed
}
parsed.UppercaseRatio = uppercaseRatio
}
indicatorColors := params["indicatorColor"]
if len(indicatorColors) != 0 {
indicatorColor, err := images.ParseColor(indicatorColors[0])
if err != nil {
logger.Error("ParseParams: invalid indicatorColor", zap.String("IndicatorColor", indicatorColors[0]))
return parsed
}
parsed.IndicatorColor = indicatorColor
}
indicatorSizeStrs := params["indicatorSize"]
if len(indicatorSizeStrs) != 0 {
indicatorSize, err := strconv.ParseFloat(indicatorSizeStrs[0], 64)
if err != nil {
logger.Error("ParseParams: invalid indicatorSize", zap.String("indicatorSize", indicatorSizeStrs[0]))
indicatorSize = 0
}
parsed.IndicatorSize = indicatorSize
}
indicatorBorderStrs := params["indicatorBorder"]
if len(indicatorBorderStrs) != 0 {
indicatorBorder, err := strconv.ParseFloat(indicatorBorderStrs[0], 64)
if err != nil {
logger.Error("ParseParams: invalid indicatorBorder", zap.String("indicatorBorder", indicatorBorderStrs[0]))
indicatorBorder = 0
}
parsed.IndicatorBorder = indicatorBorder
}
parsed.Theme = getTheme(params, logger)
parsed.Ring = ringEnabled(params)
messageIDs := params["message-id"]
if len(messageIDs) != 0 {
parsed.MessageID = messageIDs[0]
}
messageIDs = params["messageId"]
if len(messageIDs) != 0 {
parsed.MessageID = messageIDs[0]
}
authorIds := params["authorId"]
if len(authorIds) != 0 {
parsed.AuthorID = authorIds[0]
}
urls := params["url"]
if len(urls) != 0 {
parsed.URL = urls[0]
}
hash := params["hash"]
if len(hash) != 0 {
parsed.Hash = hash[0]
}
_, download := params["download"]
parsed.Download = download
return parsed
}
func handleAccountImagesImpl(multiaccountsDB *multiaccounts.Database, logger *zap.Logger, w http.ResponseWriter, parsed ImageParams) {
if parsed.KeyUID == "" {
logger.Error("handleAccountImagesImpl: no keyUid")
return return
} }
var payload = identityImage.Payload if parsed.ImageName == "" {
logger.Error("handleAccountImagesImpl: no imageName")
if ringEnabled(params) { return
account, err := multiaccountsDB.GetAccount(keyUids[0]) }
identityImage, err := multiaccountsDB.GetIdentityImage(parsed.KeyUID, parsed.ImageName)
if err != nil { if err != nil {
logger.Error("handleAccountImages: failed to GetAccount .", zap.String("keyUid", keyUids[0]), zap.Error(err)) logger.Error("handleAccountImagesImpl: failed to load image.", zap.String("keyUid", parsed.KeyUID), zap.String("imageName", parsed.ImageName), zap.Error(err))
return
}
if parsed.BgSize == 0 {
parsed.BgSize = identityImage.Width
}
payload, err := images.RoundCrop(identityImage.Payload)
if err != nil {
logger.Error("handleAccountImagesImpl: failed to crop image.", zap.String("keyUid", parsed.KeyUID), zap.String("imageName", parsed.ImageName), zap.Error(err))
return
}
if parsed.Ring {
account, err := multiaccountsDB.GetAccount(parsed.KeyUID)
if err != nil {
logger.Error("handleAccountImagesImpl: failed to GetAccount .", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
return return
} }
accColorHash := account.ColorHash accColorHash := account.ColorHash
if accColorHash == nil { if accColorHash == nil {
pks, ok := params["publicKey"] if parsed.PublicKey == "" {
if !ok || len(pks) == 0 { logger.Error("handleAccountImagesImpl: no public key for color hash", zap.String("keyUid", parsed.KeyUID))
logger.Error("no publicKey")
return return
} }
accColorHash, err = colorhash.GenerateFor(pks[0]) accColorHash, err = colorhash.GenerateFor(parsed.PublicKey)
if err != nil { if err != nil {
logger.Error("could not generate color hash") logger.Error("handleAccountImagesImpl: could not generate color hash", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
return return
} }
} }
var theme = getTheme(params, logger)
payload, err = ring.DrawRing(&ring.DrawRingParam{ payload, err = ring.DrawRing(&ring.DrawRingParam{
Theme: theme, ColorHash: accColorHash, ImageBytes: identityImage.Payload, Height: identityImage.Height, Width: identityImage.Width, Theme: parsed.Theme, ColorHash: accColorHash, ImageBytes: payload, Height: identityImage.Height, Width: identityImage.Width,
})
if err != nil {
logger.Error("handleAccountImagesImpl: failed to draw ring for account identity", zap.Error(err))
return
}
}
if parsed.IndicatorSize != 0 {
// enlarge indicator size based on identity image size / desired size
// or we get a bad quality identity image
enlargeIndicatorRatio := float64(identityImage.Width / parsed.BgSize)
payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize*enlargeIndicatorRatio, parsed.IndicatorBorder*enlargeIndicatorRatio)
if err != nil {
logger.Error("handleAccountImagesImpl: failed to draw status-indicator for initials", zap.Error(err))
return
}
}
if len(payload) == 0 {
logger.Error("handleAccountImagesImpl: empty image")
return
}
mime, err := images.GetProtobufImageMime(payload)
if err != nil {
logger.Error("failed to get mime", zap.Error(err))
}
w.Header().Set("Content-Type", mime)
w.Header().Set("Cache-Control", "no-store")
_, err = w.Write(payload)
if err != nil {
logger.Error("handleAccountImagesImpl: failed to write image", zap.Error(err))
}
}
func handleAccountImagesPlaceholder(logger *zap.Logger, w http.ResponseWriter, parsed ImageParams) {
if parsed.ImagePath == "" {
logger.Error("handleAccountImagesPlaceholder: no imagePath")
return
}
payload, im, err := images.ImageToBytesAndImage(parsed.ImagePath)
if err != nil {
logger.Error("handleAccountImagesPlaceholder: failed to load image from disk", zap.String("imageName", parsed.ImagePath))
return
}
width := im.Bounds().Dx()
if parsed.BgSize == 0 {
parsed.BgSize = width
}
payload, err = images.RoundCrop(payload)
if err != nil {
logger.Error("handleAccountImagesPlaceholder: failed to crop image.", zap.String("imageName", parsed.ImagePath), zap.Error(err))
return
}
if parsed.IndicatorSize != 0 {
enlargeIndicatorRatio := float64(width / parsed.BgSize)
payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize*enlargeIndicatorRatio, parsed.IndicatorBorder*enlargeIndicatorRatio)
if err != nil {
logger.Error("handleAccountImagesPlaceholder: failed to draw status-indicator for initials", zap.Error(err))
return
}
}
if len(payload) == 0 {
logger.Error("handleAccountImagesPlaceholder: empty image")
return
}
mime, err := images.GetProtobufImageMime(payload)
if err != nil {
logger.Error("failed to get mime", zap.Error(err))
}
w.Header().Set("Content-Type", mime)
w.Header().Set("Cache-Control", "no-store")
_, err = w.Write(payload)
if err != nil {
logger.Error("handleAccountImagesPlaceholder: failed to write image", zap.Error(err))
}
}
// handleAccountImages render multiaccounts custom profile image
func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
parsed := ParseImageParams(logger, params)
if parsed.KeyUID == "" {
handleAccountImagesPlaceholder(logger, w, parsed)
} else {
handleAccountImagesImpl(multiaccountsDB, logger, w, parsed)
}
}
}
func handleAccountInitialsImpl(multiaccountsDB *multiaccounts.Database, logger *zap.Logger, w http.ResponseWriter, parsed ImageParams) {
var name = parsed.FullName
var accColorHash multiaccounts.ColorHash
var account *multiaccounts.Account
if parsed.KeyUID != "" {
account, err := multiaccountsDB.GetAccount(parsed.KeyUID)
if err != nil {
logger.Error("handleAccountInitialsImpl: failed to get account.", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
return
}
name = account.Name
accColorHash = account.ColorHash
}
initials := images.ExtractInitials(name, parsed.InitialsLength)
payload, err := images.GenerateInitialsImage(initials, parsed.BgColor, parsed.Color, parsed.FontFile, parsed.BgSize, parsed.FontSize, parsed.UppercaseRatio)
if err != nil {
logger.Error("handleAccountInitialsImpl: failed to generate initials image.", zap.String("keyUid", parsed.KeyUID), zap.String("name", account.Name), zap.Error(err))
return
}
if parsed.Ring {
if accColorHash == nil {
if parsed.PublicKey == "" {
logger.Error("handleAccountInitialsImpl: no public key, can't draw ring", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
return
}
accColorHash, err = colorhash.GenerateFor(parsed.PublicKey)
if err != nil {
logger.Error("handleAccountInitialsImpl: failed to generate color hash from pubkey", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
return
}
}
payload, err = ring.DrawRing(&ring.DrawRingParam{
Theme: parsed.Theme, ColorHash: accColorHash, ImageBytes: payload, Height: parsed.BgSize, Width: parsed.BgSize,
}) })
if err != nil { if err != nil {
@ -111,8 +462,16 @@ func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Lo
} }
} }
if parsed.IndicatorSize != 0 {
payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize, parsed.IndicatorBorder)
if err != nil {
logger.Error("failed to draw status-indicator for initials", zap.Error(err))
return
}
}
if len(payload) == 0 { if len(payload) == 0 {
logger.Error("empty image") logger.Error("handleAccountInitialsImpl: empty image", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
return return
} }
mime, err := images.GetProtobufImageMime(payload) mime, err := images.GetProtobufImageMime(payload)
@ -128,8 +487,84 @@ func handleAccountImages(multiaccountsDB *multiaccounts.Database, logger *zap.Lo
logger.Error("failed to write image", zap.Error(err)) logger.Error("failed to write image", zap.Error(err))
} }
} }
func handleAccountInitialsPlaceholder(logger *zap.Logger, w http.ResponseWriter, parsed ImageParams) {
if parsed.FullName == "" {
logger.Error("handleAccountInitialsPlaceholder: no full name")
return
} }
initials := images.ExtractInitials(parsed.FullName, parsed.InitialsLength)
payload, err := images.GenerateInitialsImage(initials, parsed.BgColor, parsed.Color, parsed.FontFile, parsed.BgSize, parsed.FontSize, parsed.UppercaseRatio)
if err != nil {
logger.Error("handleAccountInitialsPlaceholder: failed to generate initials image.", zap.String("keyUid", parsed.KeyUID), zap.String("name", parsed.FullName), zap.Error(err))
return
}
if parsed.IndicatorSize != 0 {
payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize, parsed.IndicatorBorder)
if err != nil {
logger.Error("failed to draw status-indicator for initials", zap.Error(err))
return
}
}
if len(payload) == 0 {
logger.Error("handleAccountInitialsPlaceholder: empty image", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
return
}
mime, err := images.GetProtobufImageMime(payload)
if err != nil {
logger.Error("failed to get mime", zap.Error(err))
}
w.Header().Set("Content-Type", mime)
w.Header().Set("Cache-Control", "no-store")
_, err = w.Write(payload)
if err != nil {
logger.Error("failed to write image", zap.Error(err))
}
}
// handleAccountInitials render multiaccounts/contacts initials avatar image
func handleAccountInitials(multiaccountsDB *multiaccounts.Database, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query()
parsed := ParseImageParams(logger, params)
if parsed.FontFile == "" {
logger.Error("handleAccountInitials: no fontFile")
return
}
if parsed.FontSize == 0 {
logger.Error("handleAccountInitials: no fontSize")
return
}
if parsed.Color == color.Transparent {
logger.Error("handleAccountInitials: no color")
return
}
if parsed.BgSize == 0 {
logger.Error("handleAccountInitials: no size")
return
}
if parsed.BgColor == color.Transparent {
logger.Error("handleAccountInitials: no bgColor")
return
}
if parsed.KeyUID == "" && parsed.PublicKey == "" {
handleAccountInitialsPlaceholder(logger, w, parsed)
} else {
handleAccountInitialsImpl(multiaccountsDB, logger, w, parsed)
}
}
}
// handleContactImages render contacts custom profile image
func handleContactImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc { func handleContactImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
if db == nil { if db == nil {
return handleRequestDBMissing(logger) return handleRequestDBMissing(logger)
@ -137,40 +572,51 @@ func handleContactImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query() params := r.URL.Query()
pks, ok := params["publicKey"] parsed := ParseImageParams(logger, params)
if !ok || len(pks) == 0 {
if parsed.PublicKey == "" {
logger.Error("no publicKey") logger.Error("no publicKey")
return return
} }
imageNames, ok := params["imageName"]
if !ok || len(imageNames) == 0 { if parsed.ImageName == "" {
logger.Error("no imageName") logger.Error("no imageName")
return return
} }
var payload []byte var payload []byte
err := db.QueryRow(`SELECT payload FROM chat_identity_contacts WHERE contact_id = ? and image_type = ?`, pks[0], imageNames[0]).Scan(&payload) err := db.QueryRow(`SELECT payload FROM chat_identity_contacts WHERE contact_id = ? and image_type = ?`, parsed.PublicKey, parsed.ImageName).Scan(&payload)
if err != nil { if err != nil {
logger.Error("failed to load image.", zap.String("contact id", pks[0]), zap.String("image type", imageNames[0]), zap.Error(err)) logger.Error("failed to load image.", zap.String("contact id", parsed.PublicKey), zap.String("image type", parsed.ImageName), zap.Error(err))
return return
} }
if ringEnabled(params) { img, _, err := image.Decode(bytes.NewReader(payload))
colorHash, err := colorhash.GenerateFor(pks[0]) if err != nil {
logger.Error("failed to decode config.", zap.String("contact id", parsed.PublicKey), zap.String("image type", parsed.ImageName), zap.Error(err))
return
}
width := img.Bounds().Dx()
if parsed.BgSize == 0 {
parsed.BgSize = width
}
payload, err = images.RoundCrop(payload)
if err != nil {
logger.Error("handleContactImages: failed to crop image.", zap.Error(err))
return
}
if parsed.Ring {
colorHash, err := colorhash.GenerateFor(parsed.PublicKey)
if err != nil { if err != nil {
logger.Error("could not generate color hash") logger.Error("could not generate color hash")
return return
} }
var theme = getTheme(params, logger)
config, _, err := image.DecodeConfig(bytes.NewReader(payload))
if err != nil {
logger.Error("failed to decode config.", zap.String("contact id", pks[0]), zap.String("image type", imageNames[0]), zap.Error(err))
return
}
payload, err = ring.DrawRing(&ring.DrawRingParam{ payload, err = ring.DrawRing(&ring.DrawRingParam{
Theme: theme, ColorHash: colorHash, ImageBytes: payload, Height: config.Height, Width: config.Width, Theme: parsed.Theme, ColorHash: colorHash, ImageBytes: payload, Height: width, Width: width,
}) })
if err != nil { if err != nil {
@ -179,6 +625,15 @@ func handleContactImages(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
} }
} }
if parsed.IndicatorSize != 0 {
enlargeIndicatorRatio := float64(width / parsed.BgSize)
payload, err = images.AddStatusIndicatorToImage(payload, parsed.IndicatorColor, parsed.IndicatorSize*enlargeIndicatorRatio, parsed.IndicatorBorder*enlargeIndicatorRatio)
if err != nil {
logger.Error("handleAccountImagesImpl: failed to draw status-indicator for initials", zap.Error(err))
return
}
}
if len(payload) == 0 { if len(payload) == 0 {
logger.Error("empty image") logger.Error("empty image")
return return
@ -220,27 +675,27 @@ func getTheme(params url.Values, logger *zap.Logger) ring.Theme {
func handleIdenticon(logger *zap.Logger) http.HandlerFunc { func handleIdenticon(logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
params := r.URL.Query() params := r.URL.Query()
pks, ok := params["publicKey"] parsed := ParseImageParams(logger, params)
if !ok || len(pks) == 0 {
if parsed.PublicKey == "" {
logger.Error("no publicKey") logger.Error("no publicKey")
return return
} }
pk := pks[0]
image, err := identicon.Generate(pk) identiconImage, err := identicon.Generate(parsed.PublicKey)
if err != nil { if err != nil {
logger.Error("could not generate identicon") logger.Error("could not generate identicon")
} }
if image != nil && ringEnabled(params) { if identiconImage != nil && parsed.Ring {
colorHash, err := colorhash.GenerateFor(pk) colorHash, err := colorhash.GenerateFor(parsed.PublicKey)
if err != nil { if err != nil {
logger.Error("could not generate color hash") logger.Error("could not generate color hash")
return return
} }
theme := getTheme(params, logger) identiconImage, err = ring.DrawRing(&ring.DrawRingParam{
image, err = ring.DrawRing(&ring.DrawRingParam{ Theme: parsed.Theme, ColorHash: colorHash, ImageBytes: identiconImage, Height: identicon.Height, Width: identicon.Width,
Theme: theme, ColorHash: colorHash, ImageBytes: image, Height: identicon.Height, Width: identicon.Width,
}) })
if err != nil { if err != nil {
logger.Error("failed to draw ring", zap.Error(err)) logger.Error("failed to draw ring", zap.Error(err))
@ -251,7 +706,7 @@ func handleIdenticon(logger *zap.Logger) http.HandlerFunc {
w.Header().Set("Cache-Control", "max-age:290304000, public") w.Header().Set("Cache-Control", "max-age:290304000, public")
w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat)) w.Header().Set("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat))
_, err = w.Write(image) _, err = w.Write(identiconImage)
if err != nil { if err != nil {
logger.Error("failed to write image", zap.Error(err)) logger.Error("failed to write image", zap.Error(err))
} }
@ -264,15 +719,16 @@ func handleDiscordAuthorAvatar(db *sql.DB, logger *zap.Logger) http.HandlerFunc
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
authorIDs, ok := r.URL.Query()["authorId"] params := r.URL.Query()
if !ok || len(authorIDs) == 0 { parsed := ParseImageParams(logger, params)
if parsed.AuthorID == "" {
logger.Error("no authorIDs") logger.Error("no authorIDs")
return return
} }
authorID := authorIDs[0]
var image []byte var image []byte
err := db.QueryRow(`SELECT avatar_image_payload FROM discord_message_authors WHERE id = ?`, authorID).Scan(&image) err := db.QueryRow(`SELECT avatar_image_payload FROM discord_message_authors WHERE id = ?`, parsed.AuthorID).Scan(&image)
if err != nil { if err != nil {
logger.Error("failed to find image", zap.Error(err)) logger.Error("failed to find image", zap.Error(err))
return return
@ -302,20 +758,20 @@ func handleDiscordAttachment(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
messageIDs, ok := r.URL.Query()["messageId"] params := r.URL.Query()
if !ok || len(messageIDs) == 0 { parsed := ParseImageParams(logger, params)
if parsed.MessageID == "" {
logger.Error("no messageID") logger.Error("no messageID")
return return
} }
attachmentIDs, ok := r.URL.Query()["attachmentId"] if parsed.AttachmentID == "" {
if !ok || len(attachmentIDs) == 0 {
logger.Error("no attachmentID") logger.Error("no attachmentID")
return return
} }
messageID := messageIDs[0]
attachmentID := attachmentIDs[0]
var image []byte var image []byte
err := db.QueryRow(`SELECT payload FROM discord_message_attachments WHERE discord_message_id = ? AND id = ?`, messageID, attachmentID).Scan(&image) err := db.QueryRow(`SELECT payload FROM discord_message_attachments WHERE discord_message_id = ? AND id = ?`, parsed.MessageID, parsed.AttachmentID).Scan(&image)
if err != nil { if err != nil {
logger.Error("failed to find image", zap.Error(err)) logger.Error("failed to find image", zap.Error(err))
return return
@ -345,14 +801,16 @@ func handleImage(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
messageIDs, ok := r.URL.Query()["messageId"] params := r.URL.Query()
if !ok || len(messageIDs) == 0 { parsed := ParseImageParams(logger, params)
if parsed.MessageID == "" {
logger.Error("no messageID") logger.Error("no messageID")
return return
} }
messageID := messageIDs[0]
var image []byte var image []byte
err := db.QueryRow(`SELECT image_payload FROM user_messages WHERE id = ?`, messageID).Scan(&image) err := db.QueryRow(`SELECT image_payload FROM user_messages WHERE id = ?`, parsed.MessageID).Scan(&image)
if err != nil { if err != nil {
logger.Error("failed to find image", zap.Error(err)) logger.Error("failed to find image", zap.Error(err))
return return
@ -382,14 +840,16 @@ func handleAudio(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
messageIDs, ok := r.URL.Query()["messageId"] params := r.URL.Query()
if !ok || len(messageIDs) == 0 { parsed := ParseImageParams(logger, params)
if parsed.MessageID == "" {
logger.Error("no messageID") logger.Error("no messageID")
return return
} }
messageID := messageIDs[0]
var audio []byte var audio []byte
err := db.QueryRow(`SELECT audio_payload FROM user_messages WHERE id = ?`, messageID).Scan(&audio) err := db.QueryRow(`SELECT audio_payload FROM user_messages WHERE id = ?`, parsed.MessageID).Scan(&audio)
if err != nil { if err != nil {
logger.Error("failed to find image", zap.Error(err)) logger.Error("failed to find image", zap.Error(err))
return return
@ -415,15 +875,15 @@ func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) http.HandlerFun
} }
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
hashes, ok := r.URL.Query()["hash"] params := r.URL.Query()
if !ok || len(hashes) == 0 { parsed := ParseImageParams(logger, params)
if parsed.Hash == "" {
logger.Error("no hash") logger.Error("no hash")
return return
} }
_, download := r.URL.Query()["download"] content, err := downloader.Get(parsed.Hash, parsed.Download)
content, err := downloader.Get(hashes[0], download)
if err != nil { if err != nil {
logger.Error("could not download hash", zap.Error(err)) logger.Error("could not download hash", zap.Error(err))
return return
@ -488,26 +948,22 @@ func getThumbnailPayload(db *sql.DB, logger *zap.Logger, msgID string, thumbnail
func handleLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc { func handleLinkPreviewThumbnail(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
queryParams := r.URL.Query() params := r.URL.Query()
parsed := ParseImageParams(logger, params)
paramID, ok := queryParams["message-id"] if parsed.MessageID == "" {
if !ok || len(paramID) == 0 {
http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest) http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest)
return return
} }
paramURL, ok := queryParams["url"] if parsed.URL == "" {
if !ok || len(paramURL) == 0 {
http.Error(w, "missing query parameter 'url'", http.StatusBadRequest) http.Error(w, "missing query parameter 'url'", http.StatusBadRequest)
return return
} }
msgID := paramID[0] thumbnail, err := getThumbnailPayload(db, logger, parsed.MessageID, parsed.URL)
thumbnailURL := paramURL[0]
thumbnail, err := getThumbnailPayload(db, logger, msgID, thumbnailURL)
if err != nil { if err != nil {
logger.Error("failed to get thumbnail", zap.String("msgID", msgID)) logger.Error("failed to get thumbnail", zap.String("msgID", parsed.MessageID))
http.Error(w, "failed to get thumbnail", http.StatusInternalServerError) http.Error(w, "failed to get thumbnail", http.StatusInternalServerError)
return return
} }

View File

@ -38,6 +38,7 @@ func NewMediaServer(db *sql.DB, downloader *ipfs.Downloader, multiaccountsDB *mu
} }
s.SetHandlers(HandlerPatternMap{ s.SetHandlers(HandlerPatternMap{
accountImagesPath: handleAccountImages(s.multiaccountsDB, s.logger), accountImagesPath: handleAccountImages(s.multiaccountsDB, s.logger),
accountInitialsPath: handleAccountInitials(s.multiaccountsDB, s.logger),
audioPath: handleAudio(s.db, s.logger), audioPath: handleAudio(s.db, s.logger),
contactImagesPath: handleContactImages(s.db, s.logger), contactImagesPath: handleContactImages(s.db, s.logger),
discordAttachmentsPath: handleDiscordAttachment(s.db, s.logger), discordAttachmentsPath: handleDiscordAttachment(s.db, s.logger),

272
vendor/golang.org/x/image/font/opentype/opentype.go generated vendored Normal file
View File

@ -0,0 +1,272 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Package opentype implements a glyph rasterizer for TTF (TrueType Fonts) and
// OTF (OpenType Fonts).
//
// This package provides a high-level API, centered on the NewFace function,
// implementing the golang.org/x/image/font.Face interface.
//
// The sibling golang.org/x/image/font/sfnt package provides a low-level API.
package opentype // import "golang.org/x/image/font/opentype"
import (
"image"
"image/draw"
"io"
"golang.org/x/image/font"
"golang.org/x/image/font/sfnt"
"golang.org/x/image/math/fixed"
"golang.org/x/image/vector"
)
// ParseCollection parses an OpenType font collection, such as TTC or OTC data,
// from a []byte data source.
//
// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
// will return a collection containing 1 font.
func ParseCollection(src []byte) (*Collection, error) {
return sfnt.ParseCollection(src)
}
// ParseCollectionReaderAt parses an OpenType collection, such as TTC or OTC
// data, from an io.ReaderAt data source.
//
// If passed data for a single font, a TTF or OTF instead of a TTC or OTC, it
// will return a collection containing 1 font.
func ParseCollectionReaderAt(src io.ReaderAt) (*Collection, error) {
return sfnt.ParseCollectionReaderAt(src)
}
// Collection is a collection of one or more fonts.
//
// All of the Collection methods are safe to call concurrently.
type Collection = sfnt.Collection
// Parse parses an OpenType font, such as TTF or OTF data, from a []byte data
// source.
func Parse(src []byte) (*Font, error) {
return sfnt.Parse(src)
}
// ParseReaderAt parses an OpenType font, such as TTF or OTF data, from an
// io.ReaderAt data source.
func ParseReaderAt(src io.ReaderAt) (*Font, error) {
return sfnt.ParseReaderAt(src)
}
// Font is an OpenType font, also known as an SFNT font.
//
// All of the Font methods are safe to call concurrently, as long as each call
// has a different *sfnt.Buffer (or nil).
//
// The Font methods that don't take a *sfnt.Buffer argument are always safe to
// call concurrently.
type Font = sfnt.Font
// FaceOptions describes the possible options given to NewFace when
// creating a new font.Face from a Font.
type FaceOptions struct {
Size float64 // Size is the font size in points
DPI float64 // DPI is the dots per inch resolution
Hinting font.Hinting // Hinting selects how to quantize a vector font's glyph nodes
}
func defaultFaceOptions() *FaceOptions {
return &FaceOptions{
Size: 12,
DPI: 72,
Hinting: font.HintingNone,
}
}
// Face implements the font.Face interface for Font values.
//
// A Face is not safe to use concurrently.
type Face struct {
f *Font
hinting font.Hinting
scale fixed.Int26_6
metrics font.Metrics
metricsSet bool
buf sfnt.Buffer
rast vector.Rasterizer
mask image.Alpha
}
// NewFace returns a new font.Face for the given Font.
//
// If opts is nil, sensible defaults will be used.
func NewFace(f *Font, opts *FaceOptions) (font.Face, error) {
if opts == nil {
opts = defaultFaceOptions()
}
face := &Face{
f: f,
hinting: opts.Hinting,
scale: fixed.Int26_6(0.5 + (opts.Size * opts.DPI * 64 / 72)),
}
return face, nil
}
// Close satisfies the font.Face interface.
func (f *Face) Close() error {
return nil
}
// Metrics satisfies the font.Face interface.
func (f *Face) Metrics() font.Metrics {
if !f.metricsSet {
var err error
f.metrics, err = f.f.Metrics(&f.buf, f.scale, f.hinting)
if err != nil {
f.metrics = font.Metrics{}
}
f.metricsSet = true
}
return f.metrics
}
// Kern satisfies the font.Face interface.
func (f *Face) Kern(r0, r1 rune) fixed.Int26_6 {
x0 := f.index(r0)
x1 := f.index(r1)
k, err := f.f.Kern(&f.buf, x0, x1, fixed.Int26_6(f.f.UnitsPerEm()), f.hinting)
if err != nil {
return 0
}
return k
}
// Glyph satisfies the font.Face interface.
func (f *Face) Glyph(dot fixed.Point26_6, r rune) (dr image.Rectangle, mask image.Image, maskp image.Point, advance fixed.Int26_6, ok bool) {
x, err := f.f.GlyphIndex(&f.buf, r)
if err != nil {
return image.Rectangle{}, nil, image.Point{}, 0, false
}
// Call f.f.GlyphAdvance before f.f.LoadGlyph because the LoadGlyph docs
// say this about the &f.buf argument: the segments become invalid to use
// once [the buffer] is re-used.
advance, err = f.f.GlyphAdvance(&f.buf, x, f.scale, f.hinting)
if err != nil {
return image.Rectangle{}, nil, image.Point{}, 0, false
}
segments, err := f.f.LoadGlyph(&f.buf, x, f.scale, nil)
if err != nil {
return image.Rectangle{}, nil, image.Point{}, 0, false
}
// Numerical notation used below:
// - 2 is an integer, "two"
// - 2:16 is a 26.6 fixed point number, "two and a quarter"
// - 2.5 is a float32 number, "two and a half"
// Using 26.6 fixed point numbers means that there are 64 sub-pixel units
// in 1 integer pixel unit.
// Translate the sub-pixel bounding box from glyph space (where the glyph
// origin is at (0:00, 0:00)) to dst space (where the glyph origin is at
// the dot). dst space is the coordinate space that contains both the dot
// (a sub-pixel position) and dr (an integer-pixel rectangle).
dBounds := segments.Bounds().Add(dot)
// Quantize the sub-pixel bounds (dBounds) to integer-pixel bounds (dr).
dr.Min.X = dBounds.Min.X.Floor()
dr.Min.Y = dBounds.Min.Y.Floor()
dr.Max.X = dBounds.Max.X.Ceil()
dr.Max.Y = dBounds.Max.Y.Ceil()
width := dr.Dx()
height := dr.Dy()
if width < 0 || height < 0 {
return image.Rectangle{}, nil, image.Point{}, 0, false
}
// Calculate the sub-pixel bias to convert from glyph space to rasterizer
// space. In glyph space, the segments may be to the left or right and
// above or below the glyph origin. In rasterizer space, the segments
// should only be right and below (or equal to) the top-left corner (0.0,
// 0.0). They should also be left and above (or equal to) the bottom-right
// corner (width, height), as the rasterizer should enclose the glyph
// bounding box.
//
// For example, suppose that dot.X was at the sub-pixel position 25:48,
// three quarters of the way into the 26th pixel, and that bounds.Min.X was
// 1:20. We then have dBounds.Min.X = 1:20 + 25:48 = 27:04, dr.Min.X = 27
// and biasX = 25:48 - 27:00 = -1:16. A vertical stroke at 1:20 in glyph
// space becomes (1:20 + -1:16) = 0:04 in rasterizer space. 0:04 as a
// fixed.Int26_6 value is float32(4)/64.0 = 0.0625 as a float32 value.
biasX := dot.X - fixed.Int26_6(dr.Min.X<<6)
biasY := dot.Y - fixed.Int26_6(dr.Min.Y<<6)
// Configure the mask image, re-allocating its buffer if necessary.
nPixels := width * height
if cap(f.mask.Pix) < nPixels {
f.mask.Pix = make([]uint8, 2*nPixels)
}
f.mask.Pix = f.mask.Pix[:nPixels]
f.mask.Stride = width
f.mask.Rect.Min.X = 0
f.mask.Rect.Min.Y = 0
f.mask.Rect.Max.X = width
f.mask.Rect.Max.Y = height
// Rasterize the biased segments, converting from fixed.Int26_6 to float32.
f.rast.Reset(width, height)
f.rast.DrawOp = draw.Src
for _, seg := range segments {
switch seg.Op {
case sfnt.SegmentOpMoveTo:
f.rast.MoveTo(
float32(seg.Args[0].X+biasX)/64,
float32(seg.Args[0].Y+biasY)/64,
)
case sfnt.SegmentOpLineTo:
f.rast.LineTo(
float32(seg.Args[0].X+biasX)/64,
float32(seg.Args[0].Y+biasY)/64,
)
case sfnt.SegmentOpQuadTo:
f.rast.QuadTo(
float32(seg.Args[0].X+biasX)/64,
float32(seg.Args[0].Y+biasY)/64,
float32(seg.Args[1].X+biasX)/64,
float32(seg.Args[1].Y+biasY)/64,
)
case sfnt.SegmentOpCubeTo:
f.rast.CubeTo(
float32(seg.Args[0].X+biasX)/64,
float32(seg.Args[0].Y+biasY)/64,
float32(seg.Args[1].X+biasX)/64,
float32(seg.Args[1].Y+biasY)/64,
float32(seg.Args[2].X+biasX)/64,
float32(seg.Args[2].Y+biasY)/64,
)
}
}
f.rast.Draw(&f.mask, f.mask.Bounds(), image.Opaque, image.Point{})
return dr, &f.mask, f.mask.Rect.Min, advance, true
}
// GlyphBounds satisfies the font.Face interface.
func (f *Face) GlyphBounds(r rune) (bounds fixed.Rectangle26_6, advance fixed.Int26_6, ok bool) {
bounds, advance, err := f.f.GlyphBounds(&f.buf, f.index(r), f.scale, f.hinting)
return bounds, advance, err == nil
}
// GlyphAdvance satisfies the font.Face interface.
func (f *Face) GlyphAdvance(r rune) (advance fixed.Int26_6, ok bool) {
advance, err := f.f.GlyphAdvance(&f.buf, f.index(r), f.scale, f.hinting)
return advance, err == nil
}
func (f *Face) index(r rune) sfnt.GlyphIndex {
x, _ := f.f.GlyphIndex(&f.buf, r)
return x
}

309
vendor/golang.org/x/image/font/sfnt/cmap.go generated vendored Normal file
View File

@ -0,0 +1,309 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package sfnt
import (
"golang.org/x/text/encoding/charmap"
)
// Platform IDs and Platform Specific IDs as per
// https://www.microsoft.com/typography/otspec/name.htm
const (
pidUnicode = 0
pidMacintosh = 1
pidWindows = 3
psidUnicode2BMPOnly = 3
psidUnicode2FullRepertoire = 4
// Note that FontForge may generate a bogus Platform Specific ID (value 10)
// for the Unicode Platform ID (value 0). See
// https://github.com/fontforge/fontforge/issues/2728
psidMacintoshRoman = 0
psidWindowsSymbol = 0
psidWindowsUCS2 = 1
psidWindowsUCS4 = 10
)
// platformEncodingWidth returns the number of bytes per character assumed by
// the given Platform ID and Platform Specific ID.
//
// Very old fonts, from before Unicode was widely adopted, assume only 1 byte
// per character: a character map.
//
// Old fonts, from when Unicode meant the Basic Multilingual Plane (BMP),
// assume that 2 bytes per character is sufficient.
//
// Recent fonts naturally support the full range of Unicode code points, which
// can take up to 4 bytes per character. Such fonts might still choose one of
// the legacy encodings if e.g. their repertoire is limited to the BMP, for
// greater compatibility with older software, or because the resultant file
// size can be smaller.
func platformEncodingWidth(pid, psid uint16) int {
switch pid {
case pidUnicode:
switch psid {
case psidUnicode2BMPOnly:
return 2
case psidUnicode2FullRepertoire:
return 4
}
case pidMacintosh:
switch psid {
case psidMacintoshRoman:
return 1
}
case pidWindows:
switch psid {
case psidWindowsSymbol:
return 2
case psidWindowsUCS2:
return 2
case psidWindowsUCS4:
return 4
}
}
return 0
}
// The various cmap formats are described at
// https://www.microsoft.com/typography/otspec/cmap.htm
var supportedCmapFormat = func(format, pid, psid uint16) bool {
switch format {
case 0:
return pid == pidMacintosh && psid == psidMacintoshRoman
case 4:
return true
case 6:
return true
case 12:
return true
}
return false
}
func (f *Font) makeCachedGlyphIndex(buf []byte, offset, length uint32, format uint16) ([]byte, glyphIndexFunc, error) {
switch format {
case 0:
return f.makeCachedGlyphIndexFormat0(buf, offset, length)
case 4:
return f.makeCachedGlyphIndexFormat4(buf, offset, length)
case 6:
return f.makeCachedGlyphIndexFormat6(buf, offset, length)
case 12:
return f.makeCachedGlyphIndexFormat12(buf, offset, length)
}
panic("unreachable")
}
func (f *Font) makeCachedGlyphIndexFormat0(buf []byte, offset, length uint32) ([]byte, glyphIndexFunc, error) {
if length != 6+256 || offset+length > f.cmap.length {
return nil, nil, errInvalidCmapTable
}
var err error
buf, err = f.src.view(buf, int(f.cmap.offset+offset), int(length))
if err != nil {
return nil, nil, err
}
var table [256]byte
copy(table[:], buf[6:])
return buf, func(f *Font, b *Buffer, r rune) (GlyphIndex, error) {
x, ok := charmap.Macintosh.EncodeRune(r)
if !ok {
// The source rune r is not representable in the Macintosh-Roman encoding.
return 0, nil
}
return GlyphIndex(table[x]), nil
}, nil
}
func (f *Font) makeCachedGlyphIndexFormat4(buf []byte, offset, length uint32) ([]byte, glyphIndexFunc, error) {
const headerSize = 14
if offset+headerSize > f.cmap.length {
return nil, nil, errInvalidCmapTable
}
var err error
buf, err = f.src.view(buf, int(f.cmap.offset+offset), headerSize)
if err != nil {
return nil, nil, err
}
offset += headerSize
segCount := u16(buf[6:])
if segCount&1 != 0 {
return nil, nil, errInvalidCmapTable
}
segCount /= 2
if segCount > maxCmapSegments {
return nil, nil, errUnsupportedNumberOfCmapSegments
}
eLength := 8*uint32(segCount) + 2
if offset+eLength > f.cmap.length {
return nil, nil, errInvalidCmapTable
}
buf, err = f.src.view(buf, int(f.cmap.offset+offset), int(eLength))
if err != nil {
return nil, nil, err
}
offset += eLength
entries := make([]cmapEntry16, segCount)
for i := range entries {
entries[i] = cmapEntry16{
end: u16(buf[0*len(entries)+0+2*i:]),
start: u16(buf[2*len(entries)+2+2*i:]),
delta: u16(buf[4*len(entries)+2+2*i:]),
offset: u16(buf[6*len(entries)+2+2*i:]),
}
}
indexesBase := f.cmap.offset + offset
indexesLength := f.cmap.length - offset
return buf, func(f *Font, b *Buffer, r rune) (GlyphIndex, error) {
if uint32(r) > 0xffff {
return 0, nil
}
c := uint16(r)
for i, j := 0, len(entries); i < j; {
h := i + (j-i)/2
entry := &entries[h]
if c < entry.start {
j = h
} else if entry.end < c {
i = h + 1
} else if entry.offset == 0 {
return GlyphIndex(c + entry.delta), nil
} else {
offset := uint32(entry.offset) + 2*uint32(h-len(entries)+int(c-entry.start))
if offset > indexesLength || offset+2 > indexesLength {
return 0, errInvalidCmapTable
}
x, err := b.view(&f.src, int(indexesBase+offset), 2)
if err != nil {
return 0, err
}
return GlyphIndex(u16(x)), nil
}
}
return 0, nil
}, nil
}
func (f *Font) makeCachedGlyphIndexFormat6(buf []byte, offset, length uint32) ([]byte, glyphIndexFunc, error) {
const headerSize = 10
if offset+headerSize > f.cmap.length {
return nil, nil, errInvalidCmapTable
}
var err error
buf, err = f.src.view(buf, int(f.cmap.offset+offset), headerSize)
if err != nil {
return nil, nil, err
}
offset += headerSize
firstCode := u16(buf[6:])
entryCount := u16(buf[8:])
eLength := 2 * uint32(entryCount)
if offset+eLength > f.cmap.length {
return nil, nil, errInvalidCmapTable
}
if entryCount != 0 {
buf, err = f.src.view(buf, int(f.cmap.offset+offset), int(eLength))
if err != nil {
return nil, nil, err
}
offset += eLength
}
entries := make([]uint16, entryCount)
for i := range entries {
entries[i] = u16(buf[2*i:])
}
return buf, func(f *Font, b *Buffer, r rune) (GlyphIndex, error) {
if uint16(r) < firstCode {
return 0, nil
}
c := int(uint16(r) - firstCode)
if c >= len(entries) {
return 0, nil
}
return GlyphIndex(entries[c]), nil
}, nil
}
func (f *Font) makeCachedGlyphIndexFormat12(buf []byte, offset, _ uint32) ([]byte, glyphIndexFunc, error) {
const headerSize = 16
if offset+headerSize > f.cmap.length {
return nil, nil, errInvalidCmapTable
}
var err error
buf, err = f.src.view(buf, int(f.cmap.offset+offset), headerSize)
if err != nil {
return nil, nil, err
}
length := u32(buf[4:])
if f.cmap.length < offset || length > f.cmap.length-offset {
return nil, nil, errInvalidCmapTable
}
offset += headerSize
numGroups := u32(buf[12:])
if numGroups > maxCmapSegments {
return nil, nil, errUnsupportedNumberOfCmapSegments
}
eLength := 12 * numGroups
if headerSize+eLength != length {
return nil, nil, errInvalidCmapTable
}
buf, err = f.src.view(buf, int(f.cmap.offset+offset), int(eLength))
if err != nil {
return nil, nil, err
}
offset += eLength
entries := make([]cmapEntry32, numGroups)
for i := range entries {
entries[i] = cmapEntry32{
start: u32(buf[0+12*i:]),
end: u32(buf[4+12*i:]),
delta: u32(buf[8+12*i:]),
}
}
return buf, func(f *Font, b *Buffer, r rune) (GlyphIndex, error) {
c := uint32(r)
for i, j := 0, len(entries); i < j; {
h := i + (j-i)/2
entry := &entries[h]
if c < entry.start {
j = h
} else if entry.end < c {
i = h + 1
} else {
return GlyphIndex(c - entry.start + entry.delta), nil
}
}
return 0, nil
}, nil
}
type cmapEntry16 struct {
end, start, delta, offset uint16
}
type cmapEntry32 struct {
start, end, delta uint32
}

68
vendor/golang.org/x/image/font/sfnt/data.go generated vendored Normal file
View File

@ -0,0 +1,68 @@
// generated by go run gen.go; DO NOT EDIT
package sfnt
const numBuiltInPostNames = 258
const builtInPostNamesData = "" +
".notdef.nullnonmarkingreturnspaceexclamquotedblnumbersigndollarp" +
"ercentampersandquotesingleparenleftparenrightasteriskpluscommahy" +
"phenperiodslashzeroonetwothreefourfivesixseveneightninecolonsemi" +
"colonlessequalgreaterquestionatABCDEFGHIJKLMNOPQRSTUVWXYZbracket" +
"leftbackslashbracketrightasciicircumunderscoregraveabcdefghijklm" +
"nopqrstuvwxyzbraceleftbarbracerightasciitildeAdieresisAringCcedi" +
"llaEacuteNtildeOdieresisUdieresisaacuteagraveacircumflexadieresi" +
"satildearingccedillaeacuteegraveecircumflexedieresisiacuteigrave" +
"icircumflexidieresisntildeoacuteograveocircumflexodieresisotilde" +
"uacuteugraveucircumflexudieresisdaggerdegreecentsterlingsectionb" +
"ulletparagraphgermandblsregisteredcopyrighttrademarkacutedieresi" +
"snotequalAEOslashinfinityplusminuslessequalgreaterequalyenmupart" +
"ialdiffsummationproductpiintegralordfeminineordmasculineOmegaaeo" +
"slashquestiondownexclamdownlogicalnotradicalflorinapproxequalDel" +
"taguillemotleftguillemotrightellipsisnonbreakingspaceAgraveAtild" +
"eOtildeOEoeendashemdashquotedblleftquotedblrightquoteleftquoteri" +
"ghtdividelozengeydieresisYdieresisfractioncurrencyguilsinglleftg" +
"uilsinglrightfifldaggerdblperiodcenteredquotesinglbasequotedblba" +
"seperthousandAcircumflexEcircumflexAacuteEdieresisEgraveIacuteIc" +
"ircumflexIdieresisIgraveOacuteOcircumflexappleOgraveUacuteUcircu" +
"mflexUgravedotlessicircumflextildemacronbrevedotaccentringcedill" +
"ahungarumlautogonekcaronLslashlslashScaronscaronZcaronzcaronbrok" +
"enbarEthethYacuteyacuteThornthornminusmultiplyonesuperiortwosupe" +
"riorthreesuperioronehalfonequarterthreequartersfrancGbrevegbreve" +
"IdotaccentScedillascedillaCacutecacuteCcaronccarondcroat"
var builtInPostNamesOffsets = [...]uint16{
0x0000, 0x0007, 0x000c, 0x001c, 0x0021, 0x0027, 0x002f, 0x0039,
0x003f, 0x0046, 0x004f, 0x005a, 0x0063, 0x006d, 0x0075, 0x0079,
0x007e, 0x0084, 0x008a, 0x008f, 0x0093, 0x0096, 0x0099, 0x009e,
0x00a2, 0x00a6, 0x00a9, 0x00ae, 0x00b3, 0x00b7, 0x00bc, 0x00c5,
0x00c9, 0x00ce, 0x00d5, 0x00dd, 0x00df, 0x00e0, 0x00e1, 0x00e2,
0x00e3, 0x00e4, 0x00e5, 0x00e6, 0x00e7, 0x00e8, 0x00e9, 0x00ea,
0x00eb, 0x00ec, 0x00ed, 0x00ee, 0x00ef, 0x00f0, 0x00f1, 0x00f2,
0x00f3, 0x00f4, 0x00f5, 0x00f6, 0x00f7, 0x00f8, 0x00f9, 0x0104,
0x010d, 0x0119, 0x0124, 0x012e, 0x0133, 0x0134, 0x0135, 0x0136,
0x0137, 0x0138, 0x0139, 0x013a, 0x013b, 0x013c, 0x013d, 0x013e,
0x013f, 0x0140, 0x0141, 0x0142, 0x0143, 0x0144, 0x0145, 0x0146,
0x0147, 0x0148, 0x0149, 0x014a, 0x014b, 0x014c, 0x014d, 0x0156,
0x0159, 0x0163, 0x016d, 0x0176, 0x017b, 0x0183, 0x0189, 0x018f,
0x0198, 0x01a1, 0x01a7, 0x01ad, 0x01b8, 0x01c1, 0x01c7, 0x01cc,
0x01d4, 0x01da, 0x01e0, 0x01eb, 0x01f4, 0x01fa, 0x0200, 0x020b,
0x0214, 0x021a, 0x0220, 0x0226, 0x0231, 0x023a, 0x0240, 0x0246,
0x024c, 0x0257, 0x0260, 0x0266, 0x026c, 0x0270, 0x0278, 0x027f,
0x0285, 0x028e, 0x0298, 0x02a2, 0x02ab, 0x02b4, 0x02b9, 0x02c1,
0x02c9, 0x02cb, 0x02d1, 0x02d9, 0x02e2, 0x02eb, 0x02f7, 0x02fa,
0x02fc, 0x0307, 0x0310, 0x0317, 0x0319, 0x0321, 0x032c, 0x0338,
0x033d, 0x033f, 0x0345, 0x0351, 0x035b, 0x0365, 0x036c, 0x0372,
0x037d, 0x0382, 0x038f, 0x039d, 0x03a5, 0x03b5, 0x03bb, 0x03c1,
0x03c7, 0x03c9, 0x03cb, 0x03d1, 0x03d7, 0x03e3, 0x03f0, 0x03f9,
0x0403, 0x0409, 0x0410, 0x0419, 0x0422, 0x042a, 0x0432, 0x043f,
0x044d, 0x044f, 0x0451, 0x045a, 0x0468, 0x0476, 0x0482, 0x048d,
0x0498, 0x04a3, 0x04a9, 0x04b2, 0x04b8, 0x04be, 0x04c9, 0x04d2,
0x04d8, 0x04de, 0x04e9, 0x04ee, 0x04f4, 0x04fa, 0x0505, 0x050b,
0x0513, 0x051d, 0x0522, 0x0528, 0x052d, 0x0536, 0x053a, 0x0541,
0x054d, 0x0553, 0x0558, 0x055e, 0x0564, 0x056a, 0x0570, 0x0576,
0x057c, 0x0585, 0x0588, 0x058b, 0x0591, 0x0597, 0x059c, 0x05a1,
0x05a6, 0x05ae, 0x05b9, 0x05c4, 0x05d1, 0x05d8, 0x05e2, 0x05ef,
0x05f4, 0x05fa, 0x0600, 0x060a, 0x0612, 0x061a, 0x0620, 0x0626,
0x062c, 0x0632, 0x0638,
}

550
vendor/golang.org/x/image/font/sfnt/gpos.go generated vendored Normal file
View File

@ -0,0 +1,550 @@
// Copyright 2019 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package sfnt
import (
"sort"
)
const (
hexScriptLatn = uint32(0x6c61746e) // latn
hexScriptDFLT = uint32(0x44464c54) // DFLT
hexFeatureKern = uint32(0x6b65726e) // kern
)
//kernFunc returns the unscaled kerning value for kerning pair a+b.
// Returns ErrNotFound if no kerning is specified for this pair.
type kernFunc func(a, b GlyphIndex) (int16, error)
func (f *Font) parseGPOSKern(buf []byte) ([]byte, []kernFunc, error) {
// https://docs.microsoft.com/en-us/typography/opentype/spec/gpos
if f.gpos.length == 0 {
return buf, nil, nil
}
const headerSize = 10 // GPOS header v1.1 is 14 bytes, but we don't support FeatureVariations
if f.gpos.length < headerSize {
return buf, nil, errInvalidGPOSTable
}
buf, err := f.src.view(buf, int(f.gpos.offset), headerSize)
if err != nil {
return buf, nil, err
}
// check for version 1.0/1.1
if u16(buf) != 1 || u16(buf[2:]) > 1 {
return buf, nil, errUnsupportedGPOSTable
}
scriptListOffset := u16(buf[4:])
featureListOffset := u16(buf[6:])
lookupListOffset := u16(buf[8:])
// get all feature indices for latn script
buf, featureIdxs, err := f.parseGPOSScriptFeatures(buf, int(f.gpos.offset)+int(scriptListOffset), hexScriptLatn)
if err != nil {
return buf, nil, err
}
if len(featureIdxs) == 0 {
// get all feature indices for DFLT script
buf, featureIdxs, err = f.parseGPOSScriptFeatures(buf, int(f.gpos.offset)+int(scriptListOffset), hexScriptDFLT)
if err != nil {
return buf, nil, err
}
if len(featureIdxs) == 0 {
return buf, nil, nil
}
}
// get all lookup indices for kern features
buf, lookupIdx, err := f.parseGPOSFeaturesLookup(buf, int(f.gpos.offset)+int(featureListOffset), featureIdxs, hexFeatureKern)
if err != nil {
return buf, nil, err
}
// LookupTableList: lookupCount,[]lookups
buf, numLookupTables, err := f.src.varLenView(buf, int(f.gpos.offset)+int(lookupListOffset), 2, 0, 2)
if err != nil {
return buf, nil, err
}
var kernFuncs []kernFunc
lookupTables:
for _, n := range lookupIdx {
if n > numLookupTables {
return buf, nil, errInvalidGPOSTable
}
tableOffset := int(f.gpos.offset) + int(lookupListOffset) + int(u16(buf[2+n*2:]))
// LookupTable: lookupType, lookupFlag, subTableCount, []subtableOffsets, markFilteringSet
buf, numSubTables, err := f.src.varLenView(buf, tableOffset, 8, 4, 2)
if err != nil {
return buf, nil, err
}
flags := u16(buf[2:])
subTableOffsets := make([]int, numSubTables)
for i := 0; i < int(numSubTables); i++ {
subTableOffsets[i] = int(tableOffset) + int(u16(buf[6+i*2:]))
}
switch lookupType := u16(buf); lookupType {
case 2: // PairPos table
case 9:
// Extension Positioning table defines an additional u32 offset
// to allow subtables to exceed the 16-bit limit.
for i := range subTableOffsets {
buf, err = f.src.view(buf, subTableOffsets[i], 8)
if err != nil {
return buf, nil, err
}
if format := u16(buf); format != 1 {
return buf, nil, errUnsupportedExtensionPosFormat
}
if lookupType := u16(buf[2:]); lookupType != 2 {
continue lookupTables
}
subTableOffsets[i] += int(u32(buf[4:]))
}
default: // other types are not supported
continue
}
if flags&0x0010 > 0 {
// useMarkFilteringSet enabled, skip as it is not supported
continue
}
for _, subTableOffset := range subTableOffsets {
buf, err = f.src.view(buf, int(subTableOffset), 4)
if err != nil {
return buf, nil, err
}
format := u16(buf)
var lookupIndex indexLookupFunc
buf, lookupIndex, err = f.makeCachedCoverageLookup(buf, subTableOffset+int(u16(buf[2:])))
if err != nil {
return buf, nil, err
}
switch format {
case 1: // Adjustments for Glyph Pairs
buf, kern, err := f.parsePairPosFormat1(buf, subTableOffset, lookupIndex)
if err != nil {
return buf, nil, err
}
if kern != nil {
kernFuncs = append(kernFuncs, kern)
}
case 2: // Class Pair Adjustment
buf, kern, err := f.parsePairPosFormat2(buf, subTableOffset, lookupIndex)
if err != nil {
return buf, nil, err
}
if kern != nil {
kernFuncs = append(kernFuncs, kern)
}
}
}
}
return buf, kernFuncs, nil
}
func (f *Font) parsePairPosFormat1(buf []byte, offset int, lookupIndex indexLookupFunc) ([]byte, kernFunc, error) {
// PairPos Format 1: posFormat, coverageOffset, valueFormat1,
// valueFormat2, pairSetCount, []pairSetOffsets
var err error
var nPairs int
buf, nPairs, err = f.src.varLenView(buf, offset, 10, 8, 2)
if err != nil {
return buf, nil, err
}
// check valueFormat1 and valueFormat2 flags
if u16(buf[4:]) != 0x04 || u16(buf[6:]) != 0x00 {
// we only support kerning with X_ADVANCE for first glyph
return buf, nil, nil
}
// PairPos table contains an array of offsets to PairSet
// tables, which contains an array of PairValueRecords.
// Calculate length of complete PairPos table by jumping to
// last PairSet.
// We need to iterate all offsets to find the last pair as
// offsets are not sorted and can be repeated.
var lastPairSetOffset int
for n := 0; n < nPairs; n++ {
pairOffset := int(u16(buf[10+n*2:]))
if pairOffset > lastPairSetOffset {
lastPairSetOffset = pairOffset
}
}
buf, err = f.src.view(buf, offset+lastPairSetOffset, 2)
if err != nil {
return buf, nil, err
}
pairValueCount := int(u16(buf))
// Each PairSet contains the secondGlyph (u16) and one or more value records (all u16).
// We only support lookup tables with one value record (X_ADVANCE, see valueFormat1/2 above).
lastPairSetLength := 2 + pairValueCount*4
length := lastPairSetOffset + lastPairSetLength
buf, err = f.src.view(buf, offset, length)
if err != nil {
return buf, nil, err
}
kern := makeCachedPairPosGlyph(lookupIndex, nPairs, buf)
return buf, kern, nil
}
func (f *Font) parsePairPosFormat2(buf []byte, offset int, lookupIndex indexLookupFunc) ([]byte, kernFunc, error) {
// PairPos Format 2:
// posFormat, coverageOffset, valueFormat1, valueFormat2,
// classDef1Offset, classDef2Offset, class1Count, class2Count,
// []class1Records
var err error
buf, err = f.src.view(buf, offset, 16)
if err != nil {
return buf, nil, err
}
// check valueFormat1 and valueFormat2 flags
if u16(buf[4:]) != 0x04 || u16(buf[6:]) != 0x00 {
// we only support kerning with X_ADVANCE for first glyph
return buf, nil, nil
}
numClass1 := int(u16(buf[12:]))
numClass2 := int(u16(buf[14:]))
cdef1Offset := offset + int(u16(buf[8:]))
cdef2Offset := offset + int(u16(buf[10:]))
var cdef1, cdef2 classLookupFunc
buf, cdef1, err = f.makeCachedClassLookup(buf, cdef1Offset)
if err != nil {
return buf, nil, err
}
buf, cdef2, err = f.makeCachedClassLookup(buf, cdef2Offset)
if err != nil {
return buf, nil, err
}
buf, err = f.src.view(buf, offset+16, numClass1*numClass2*2)
if err != nil {
return buf, nil, err
}
kern := makeCachedPairPosClass(
lookupIndex,
numClass1,
numClass2,
cdef1,
cdef2,
buf,
)
return buf, kern, nil
}
// parseGPOSScriptFeatures returns all indices of features in FeatureTable that
// are valid for the given script.
// Returns features from DefaultLangSys, different languages are not supported.
// However, all observed fonts either do not use different languages or use the
// same features as DefaultLangSys.
func (f *Font) parseGPOSScriptFeatures(buf []byte, offset int, script uint32) ([]byte, []int, error) {
// ScriptList table: scriptCount, []scriptRecords{scriptTag, scriptOffset}
buf, numScriptTables, err := f.src.varLenView(buf, offset, 2, 0, 6)
if err != nil {
return buf, nil, err
}
// Search ScriptTables for script
var scriptTableOffset uint16
for i := 0; i < numScriptTables; i++ {
scriptTag := u32(buf[2+i*6:])
if scriptTag == script {
scriptTableOffset = u16(buf[2+i*6+4:])
break
}
}
if scriptTableOffset == 0 {
return buf, nil, nil
}
// Script table: defaultLangSys, langSysCount, []langSysRecords{langSysTag, langSysOffset}
buf, err = f.src.view(buf, offset+int(scriptTableOffset), 2)
if err != nil {
return buf, nil, err
}
defaultLangSysOffset := u16(buf)
if defaultLangSysOffset == 0 {
return buf, nil, nil
}
// LangSys table: lookupOrder (reserved), requiredFeatureIndex, featureIndexCount, []featureIndices
buf, numFeatures, err := f.src.varLenView(buf, offset+int(scriptTableOffset)+int(defaultLangSysOffset), 6, 4, 2)
if err != nil {
return buf, nil, err
}
featureIdxs := make([]int, numFeatures)
for i := range featureIdxs {
featureIdxs[i] = int(u16(buf[6+i*2:]))
}
return buf, featureIdxs, nil
}
func (f *Font) parseGPOSFeaturesLookup(buf []byte, offset int, featureIdxs []int, feature uint32) ([]byte, []int, error) {
// FeatureList table: featureCount, []featureRecords{featureTag, featureOffset}
buf, numFeatureTables, err := f.src.varLenView(buf, offset, 2, 0, 6)
if err != nil {
return buf, nil, err
}
lookupIdx := make([]int, 0, 4)
for _, fidx := range featureIdxs {
if fidx > numFeatureTables {
return buf, nil, errInvalidGPOSTable
}
featureTag := u32(buf[2+fidx*6:])
if featureTag != feature {
continue
}
featureOffset := u16(buf[2+fidx*6+4:])
buf, numLookups, err := f.src.varLenView(nil, offset+int(featureOffset), 4, 2, 2)
if err != nil {
return buf, nil, err
}
for i := 0; i < numLookups; i++ {
lookupIdx = append(lookupIdx, int(u16(buf[4+i*2:])))
}
}
return buf, lookupIdx, nil
}
func makeCachedPairPosGlyph(cov indexLookupFunc, num int, buf []byte) kernFunc {
glyphs := make([]byte, len(buf))
copy(glyphs, buf)
return func(a, b GlyphIndex) (int16, error) {
idx, found := cov(a)
if !found {
return 0, ErrNotFound
}
if idx >= num {
return 0, ErrNotFound
}
offset := int(u16(glyphs[10+idx*2:]))
if offset+1 >= len(glyphs) {
return 0, errInvalidGPOSTable
}
count := int(u16(glyphs[offset:]))
for i := 0; i < count; i++ {
secondGlyphIndex := GlyphIndex(int(u16(glyphs[offset+2+i*4:])))
if secondGlyphIndex == b {
return int16(u16(glyphs[offset+2+i*4+2:])), nil
}
if secondGlyphIndex > b {
return 0, ErrNotFound
}
}
return 0, ErrNotFound
}
}
func makeCachedPairPosClass(cov indexLookupFunc, num1, num2 int, cdef1, cdef2 classLookupFunc, buf []byte) kernFunc {
glyphs := make([]byte, len(buf))
copy(glyphs, buf)
return func(a, b GlyphIndex) (int16, error) {
// check coverage to avoid selection of default class 0
_, found := cov(a)
if !found {
return 0, ErrNotFound
}
idxa := cdef1(a)
idxb := cdef2(b)
return int16(u16(glyphs[(idxb+idxa*num2)*2:])), nil
}
}
// indexLookupFunc returns the index into a PairPos table for the provided glyph.
// Returns false if the glyph is not covered by this lookup.
type indexLookupFunc func(GlyphIndex) (int, bool)
func (f *Font) makeCachedCoverageLookup(buf []byte, offset int) ([]byte, indexLookupFunc, error) {
var err error
buf, err = f.src.view(buf, offset, 2)
if err != nil {
return buf, nil, err
}
switch u16(buf) {
case 1:
// Coverage Format 1: coverageFormat, glyphCount, []glyphArray
buf, _, err = f.src.varLenView(buf, offset, 4, 2, 2)
if err != nil {
return buf, nil, err
}
return buf, makeCachedCoverageList(buf[2:]), nil
case 2:
// Coverage Format 2: coverageFormat, rangeCount, []rangeRecords{startGlyphID, endGlyphID, startCoverageIndex}
buf, _, err = f.src.varLenView(buf, offset, 4, 2, 6)
if err != nil {
return buf, nil, err
}
return buf, makeCachedCoverageRange(buf[2:]), nil
default:
return buf, nil, errUnsupportedCoverageFormat
}
}
func makeCachedCoverageList(buf []byte) indexLookupFunc {
num := int(u16(buf))
list := make([]byte, len(buf)-2)
copy(list, buf[2:])
return func(gi GlyphIndex) (int, bool) {
idx := sort.Search(num, func(i int) bool {
return gi <= GlyphIndex(u16(list[i*2:]))
})
if idx < num && GlyphIndex(u16(list[idx*2:])) == gi {
return idx, true
}
return 0, false
}
}
func makeCachedCoverageRange(buf []byte) indexLookupFunc {
num := int(u16(buf))
ranges := make([]byte, len(buf)-2)
copy(ranges, buf[2:])
return func(gi GlyphIndex) (int, bool) {
if num == 0 {
return 0, false
}
// ranges is an array of startGlyphID, endGlyphID and startCoverageIndex
// Ranges are non-overlapping.
// The following GlyphIDs/index pairs are stored as follows:
// pairs: 130=0, 131=1, 132=2, 133=3, 134=4, 135=5, 137=6
// ranges: 130, 135, 0 137, 137, 6
// startCoverageIndex is used to calculate the index without counting
// the length of the preceeding ranges
idx := sort.Search(num, func(i int) bool {
return gi <= GlyphIndex(u16(ranges[i*6:]))
})
// idx either points to a matching start, or to the next range (or idx==num)
// e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range
// check if gi is the start of a range, but only if sort.Search returned a valid result
if idx < num {
if start := u16(ranges[idx*6:]); gi == GlyphIndex(start) {
return int(u16(ranges[idx*6+4:])), true
}
}
// check if gi is in previous range
if idx > 0 {
idx--
start, end := u16(ranges[idx*6:]), u16(ranges[idx*6+2:])
if gi >= GlyphIndex(start) && gi <= GlyphIndex(end) {
return int(u16(ranges[idx*6+4:]) + uint16(gi) - start), true
}
}
return 0, false
}
}
// classLookupFunc returns the class ID for the provided glyph. Returns 0
// (default class) for glyphs not covered by this lookup.
type classLookupFunc func(GlyphIndex) int
func (f *Font) makeCachedClassLookup(buf []byte, offset int) ([]byte, classLookupFunc, error) {
var err error
buf, err = f.src.view(buf, offset, 2)
if err != nil {
return buf, nil, err
}
switch u16(buf) {
case 1:
// ClassDefFormat 1: classFormat, startGlyphID, glyphCount, []classValueArray
buf, _, err = f.src.varLenView(buf, offset, 6, 4, 2)
if err != nil {
return buf, nil, err
}
return buf, makeCachedClassLookupFormat1(buf), nil
case 2:
// ClassDefFormat 2: classFormat, classRangeCount, []classRangeRecords
buf, _, err = f.src.varLenView(buf, offset, 4, 2, 6)
if err != nil {
return buf, nil, err
}
return buf, makeCachedClassLookupFormat2(buf), nil
default:
return buf, nil, errUnsupportedClassDefFormat
}
}
func makeCachedClassLookupFormat1(buf []byte) classLookupFunc {
startGI := u16(buf[2:])
num := u16(buf[4:])
classIDs := make([]byte, len(buf)-4)
copy(classIDs, buf[6:])
return func(gi GlyphIndex) int {
// classIDs is an array of target class IDs. gi is the index into that array (minus startGI).
if gi < GlyphIndex(startGI) || gi >= GlyphIndex(startGI+num) {
// default to class 0
return 0
}
return int(u16(classIDs[(int(gi)-int(startGI))*2:]))
}
}
func makeCachedClassLookupFormat2(buf []byte) classLookupFunc {
num := int(u16(buf[2:]))
classRanges := make([]byte, len(buf)-2)
copy(classRanges, buf[4:])
return func(gi GlyphIndex) int {
if num == 0 {
return 0 // default to class 0
}
// classRange is an array of startGlyphID, endGlyphID and target class ID.
// Ranges are non-overlapping.
// E.g. 130, 135, 1 137, 137, 5 etc
idx := sort.Search(num, func(i int) bool {
return gi <= GlyphIndex(u16(classRanges[i*6:]))
})
// idx either points to a matching start, or to the next range (or idx==num)
// e.g. with the range example from above: 130 points to 130-135 range, 133 points to 137-137 range
// check if gi is the start of a range, but only if sort.Search returned a valid result
if idx < num {
if start := u16(classRanges[idx*6:]); gi == GlyphIndex(start) {
return int(u16(classRanges[idx*6+4:]))
}
}
// check if gi is in previous range
if idx > 0 {
idx--
start, end := u16(classRanges[idx*6:]), u16(classRanges[idx*6+2:])
if gi >= GlyphIndex(start) && gi <= GlyphIndex(end) {
return int(u16(classRanges[idx*6+4:]))
}
}
// default to class 0
return 0
}
}

1414
vendor/golang.org/x/image/font/sfnt/postscript.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

2000
vendor/golang.org/x/image/font/sfnt/sfnt.go generated vendored Normal file

File diff suppressed because it is too large Load Diff

572
vendor/golang.org/x/image/font/sfnt/truetype.go generated vendored Normal file
View File

@ -0,0 +1,572 @@
// Copyright 2017 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package sfnt
import (
"golang.org/x/image/math/fixed"
)
// Flags for simple (non-compound) glyphs.
//
// See https://www.microsoft.com/typography/OTSPEC/glyf.htm
const (
flagOnCurve = 1 << 0 // 0x0001
flagXShortVector = 1 << 1 // 0x0002
flagYShortVector = 1 << 2 // 0x0004
flagRepeat = 1 << 3 // 0x0008
// The same flag bits are overloaded to have two meanings, dependent on the
// value of the flag{X,Y}ShortVector bits.
flagPositiveXShortVector = 1 << 4 // 0x0010
flagThisXIsSame = 1 << 4 // 0x0010
flagPositiveYShortVector = 1 << 5 // 0x0020
flagThisYIsSame = 1 << 5 // 0x0020
)
// Flags for compound glyphs.
//
// See https://www.microsoft.com/typography/OTSPEC/glyf.htm
const (
flagArg1And2AreWords = 1 << 0 // 0x0001
flagArgsAreXYValues = 1 << 1 // 0x0002
flagRoundXYToGrid = 1 << 2 // 0x0004
flagWeHaveAScale = 1 << 3 // 0x0008
flagReserved4 = 1 << 4 // 0x0010
flagMoreComponents = 1 << 5 // 0x0020
flagWeHaveAnXAndYScale = 1 << 6 // 0x0040
flagWeHaveATwoByTwo = 1 << 7 // 0x0080
flagWeHaveInstructions = 1 << 8 // 0x0100
flagUseMyMetrics = 1 << 9 // 0x0200
flagOverlapCompound = 1 << 10 // 0x0400
flagScaledComponentOffset = 1 << 11 // 0x0800
flagUnscaledComponentOffset = 1 << 12 // 0x1000
)
func midPoint(p, q fixed.Point26_6) fixed.Point26_6 {
return fixed.Point26_6{
X: (p.X + q.X) / 2,
Y: (p.Y + q.Y) / 2,
}
}
func parseLoca(src *source, loca table, glyfOffset uint32, indexToLocFormat bool, numGlyphs int32) (locations []uint32, err error) {
if indexToLocFormat {
if loca.length != 4*uint32(numGlyphs+1) {
return nil, errInvalidLocaTable
}
} else {
if loca.length != 2*uint32(numGlyphs+1) {
return nil, errInvalidLocaTable
}
}
locations = make([]uint32, numGlyphs+1)
buf, err := src.view(nil, int(loca.offset), int(loca.length))
if err != nil {
return nil, err
}
if indexToLocFormat {
for i := range locations {
locations[i] = 1*uint32(u32(buf[4*i:])) + glyfOffset
}
} else {
for i := range locations {
locations[i] = 2*uint32(u16(buf[2*i:])) + glyfOffset
}
}
return locations, err
}
// https://www.microsoft.com/typography/OTSPEC/glyf.htm says that "Each
// glyph begins with the following [10 byte] header".
const glyfHeaderLen = 10
func loadGlyf(f *Font, b *Buffer, x GlyphIndex, stackBottom, recursionDepth uint32) error {
data, _, _, err := f.viewGlyphData(b, x)
if err != nil {
return err
}
if len(data) == 0 {
return nil
}
if len(data) < glyfHeaderLen {
return errInvalidGlyphData
}
index := glyfHeaderLen
numContours, numPoints := int16(u16(data)), 0
switch {
case numContours == -1:
// We have a compound glyph. No-op.
case numContours == 0:
return nil
case numContours > 0:
// We have a simple (non-compound) glyph.
index += 2 * int(numContours)
if index > len(data) {
return errInvalidGlyphData
}
// The +1 for numPoints is because the value in the file format is
// inclusive, but Go's slice[:index] semantics are exclusive.
numPoints = 1 + int(u16(data[index-2:]))
default:
return errInvalidGlyphData
}
if numContours < 0 {
return loadCompoundGlyf(f, b, data[glyfHeaderLen:], stackBottom, recursionDepth)
}
// Skip the hinting instructions.
index += 2
if index > len(data) {
return errInvalidGlyphData
}
hintsLength := int(u16(data[index-2:]))
index += hintsLength
if index > len(data) {
return errInvalidGlyphData
}
// For simple (non-compound) glyphs, the remainder of the glyf data
// consists of (flags, x, y) points: the Bézier curve segments. These are
// stored in columns (all the flags first, then all the x coordinates, then
// all the y coordinates), not rows, as it compresses better.
//
// Decoding those points in row order involves two passes. The first pass
// determines the indexes (relative to the data slice) of where the flags,
// the x coordinates and the y coordinates each start.
flagIndex := int32(index)
xIndex, yIndex, ok := findXYIndexes(data, index, numPoints)
if !ok {
return errInvalidGlyphData
}
// The second pass decodes each (flags, x, y) tuple in row order.
g := glyfIter{
data: data,
flagIndex: flagIndex,
xIndex: xIndex,
yIndex: yIndex,
endIndex: glyfHeaderLen,
// The -1 is because the contour-end index in the file format is
// inclusive, but Go's slice[:index] semantics are exclusive.
prevEnd: -1,
numContours: int32(numContours),
}
for g.nextContour() {
for g.nextSegment() {
b.segments = append(b.segments, g.seg)
}
}
return g.err
}
func findXYIndexes(data []byte, index, numPoints int) (xIndex, yIndex int32, ok bool) {
xDataLen := 0
yDataLen := 0
for i := 0; ; {
if i > numPoints {
return 0, 0, false
}
if i == numPoints {
break
}
repeatCount := 1
if index >= len(data) {
return 0, 0, false
}
flag := data[index]
index++
if flag&flagRepeat != 0 {
if index >= len(data) {
return 0, 0, false
}
repeatCount += int(data[index])
index++
}
xSize := 0
if flag&flagXShortVector != 0 {
xSize = 1
} else if flag&flagThisXIsSame == 0 {
xSize = 2
}
xDataLen += xSize * repeatCount
ySize := 0
if flag&flagYShortVector != 0 {
ySize = 1
} else if flag&flagThisYIsSame == 0 {
ySize = 2
}
yDataLen += ySize * repeatCount
i += repeatCount
}
if index+xDataLen+yDataLen > len(data) {
return 0, 0, false
}
return int32(index), int32(index + xDataLen), true
}
func loadCompoundGlyf(f *Font, b *Buffer, data []byte, stackBottom, recursionDepth uint32) error {
if recursionDepth++; recursionDepth == maxCompoundRecursionDepth {
return errUnsupportedCompoundGlyph
}
// Read and process the compound glyph's components. They are two separate
// for loops, since reading parses the elements of the data slice, and
// processing can overwrite the backing array.
stackTop := stackBottom
for {
if stackTop >= maxCompoundStackSize {
return errUnsupportedCompoundGlyph
}
elem := &b.compoundStack[stackTop]
stackTop++
if len(data) < 4 {
return errInvalidGlyphData
}
flags := u16(data)
elem.glyphIndex = GlyphIndex(u16(data[2:]))
if flags&flagArg1And2AreWords == 0 {
if len(data) < 6 {
return errInvalidGlyphData
}
elem.dx = int16(int8(data[4]))
elem.dy = int16(int8(data[5]))
data = data[6:]
} else {
if len(data) < 8 {
return errInvalidGlyphData
}
elem.dx = int16(u16(data[4:]))
elem.dy = int16(u16(data[6:]))
data = data[8:]
}
if flags&flagArgsAreXYValues == 0 {
return errUnsupportedCompoundGlyph
}
elem.hasTransform = flags&(flagWeHaveAScale|flagWeHaveAnXAndYScale|flagWeHaveATwoByTwo) != 0
if elem.hasTransform {
switch {
case flags&flagWeHaveAScale != 0:
if len(data) < 2 {
return errInvalidGlyphData
}
elem.transformXX = int16(u16(data))
elem.transformXY = 0
elem.transformYX = 0
elem.transformYY = elem.transformXX
data = data[2:]
case flags&flagWeHaveAnXAndYScale != 0:
if len(data) < 4 {
return errInvalidGlyphData
}
elem.transformXX = int16(u16(data[0:]))
elem.transformXY = 0
elem.transformYX = 0
elem.transformYY = int16(u16(data[2:]))
data = data[4:]
case flags&flagWeHaveATwoByTwo != 0:
if len(data) < 8 {
return errInvalidGlyphData
}
elem.transformXX = int16(u16(data[0:]))
elem.transformXY = int16(u16(data[2:]))
elem.transformYX = int16(u16(data[4:]))
elem.transformYY = int16(u16(data[6:]))
data = data[8:]
}
}
if flags&flagMoreComponents == 0 {
break
}
}
// To support hinting, we'd have to save the remaining bytes in data here
// and interpret them after the for loop below, since that for loop's
// loadGlyf calls can overwrite the backing array.
for i := stackBottom; i < stackTop; i++ {
elem := &b.compoundStack[i]
base := len(b.segments)
if err := loadGlyf(f, b, elem.glyphIndex, stackTop, recursionDepth); err != nil {
return err
}
dx, dy := fixed.Int26_6(elem.dx), fixed.Int26_6(elem.dy)
segments := b.segments[base:]
if elem.hasTransform {
txx := elem.transformXX
txy := elem.transformXY
tyx := elem.transformYX
tyy := elem.transformYY
for j := range segments {
transformArgs(&segments[j].Args, txx, txy, tyx, tyy, dx, dy)
}
} else {
for j := range segments {
translateArgs(&segments[j].Args, dx, dy)
}
}
}
return nil
}
type glyfIter struct {
data []byte
err error
// Various indices into the data slice. See the "Decoding those points in
// row order" comment above.
flagIndex int32
xIndex int32
yIndex int32
// endIndex points to the uint16 that is the inclusive point index of the
// current contour's end. prevEnd is the previous contour's end.
endIndex int32
prevEnd int32
// c and p count the current contour and point, up to numContours and
// numPoints.
c, numContours int32
p, nPoints int32
// The next two groups of fields track points and segments. Points are what
// the underlying file format provides. Bézier curve segments are what the
// rasterizer consumes.
//
// Points are either on-curve or off-curve. Two consecutive on-curve points
// define a linear curve segment between them. N off-curve points between
// on-curve points define N quadratic curve segments. The TrueType glyf
// format does not use cubic curves. If N is greater than 1, some of these
// segment end points are implicit, the midpoint of two off-curve points.
// Given the points A, B1, B2, ..., BN, C, where A and C are on-curve and
// all the Bs are off-curve, the segments are:
//
// - A, B1, midpoint(B1, B2)
// - midpoint(B1, B2), B2, midpoint(B2, B3)
// - midpoint(B2, B3), B3, midpoint(B3, B4)
// - ...
// - midpoint(BN-1, BN), BN, C
//
// Note that the sequence of Bs may wrap around from the last point in the
// glyf data to the first. A and C may also be the same point (the only
// explicit on-curve point), or there may be no explicit on-curve points at
// all (but still implicit ones between explicit off-curve points).
// Points.
x, y int16
on bool
flag uint8
repeats uint8
// Segments.
closing bool
closed bool
firstOnCurveValid bool
firstOffCurveValid bool
lastOffCurveValid bool
firstOnCurve fixed.Point26_6
firstOffCurve fixed.Point26_6
lastOffCurve fixed.Point26_6
seg Segment
}
func (g *glyfIter) nextContour() (ok bool) {
if g.c == g.numContours {
return false
}
g.c++
end := int32(u16(g.data[g.endIndex:]))
g.endIndex += 2
if end <= g.prevEnd {
g.err = errInvalidGlyphData
return false
}
g.nPoints = end - g.prevEnd
g.p = 0
g.prevEnd = end
g.closing = false
g.closed = false
g.firstOnCurveValid = false
g.firstOffCurveValid = false
g.lastOffCurveValid = false
return true
}
func (g *glyfIter) close() {
switch {
case !g.firstOffCurveValid && !g.lastOffCurveValid:
g.closed = true
g.seg = Segment{
Op: SegmentOpLineTo,
Args: [3]fixed.Point26_6{g.firstOnCurve},
}
case !g.firstOffCurveValid && g.lastOffCurveValid:
g.closed = true
g.seg = Segment{
Op: SegmentOpQuadTo,
Args: [3]fixed.Point26_6{g.lastOffCurve, g.firstOnCurve},
}
case g.firstOffCurveValid && !g.lastOffCurveValid:
g.closed = true
g.seg = Segment{
Op: SegmentOpQuadTo,
Args: [3]fixed.Point26_6{g.firstOffCurve, g.firstOnCurve},
}
case g.firstOffCurveValid && g.lastOffCurveValid:
g.lastOffCurveValid = false
g.seg = Segment{
Op: SegmentOpQuadTo,
Args: [3]fixed.Point26_6{
g.lastOffCurve,
midPoint(g.lastOffCurve, g.firstOffCurve),
},
}
}
}
func (g *glyfIter) nextSegment() (ok bool) {
for !g.closed {
if g.closing || !g.nextPoint() {
g.closing = true
g.close()
return true
}
// Convert the tuple (g.x, g.y) to a fixed.Point26_6, since the latter
// is what's held in a Segment. The input (g.x, g.y) is a pair of int16
// values, measured in font units, since that is what the underlying
// format provides. The output is a pair of fixed.Int26_6 values. A
// fixed.Int26_6 usually represents a 26.6 fixed number of pixels, but
// this here is just a straight numerical conversion, with no scaling
// factor. A later step scales the Segment.Args values by such a factor
// to convert e.g. 1792 font units to 10.5 pixels at 2048 font units
// per em and 12 ppem (pixels per em).
p := fixed.Point26_6{
X: fixed.Int26_6(g.x),
Y: fixed.Int26_6(g.y),
}
if !g.firstOnCurveValid {
if g.on {
g.firstOnCurve = p
g.firstOnCurveValid = true
g.seg = Segment{
Op: SegmentOpMoveTo,
Args: [3]fixed.Point26_6{p},
}
return true
} else if !g.firstOffCurveValid {
g.firstOffCurve = p
g.firstOffCurveValid = true
continue
} else {
g.firstOnCurve = midPoint(g.firstOffCurve, p)
g.firstOnCurveValid = true
g.lastOffCurve = p
g.lastOffCurveValid = true
g.seg = Segment{
Op: SegmentOpMoveTo,
Args: [3]fixed.Point26_6{g.firstOnCurve},
}
return true
}
} else if !g.lastOffCurveValid {
if !g.on {
g.lastOffCurve = p
g.lastOffCurveValid = true
continue
} else {
g.seg = Segment{
Op: SegmentOpLineTo,
Args: [3]fixed.Point26_6{p},
}
return true
}
} else {
if !g.on {
g.seg = Segment{
Op: SegmentOpQuadTo,
Args: [3]fixed.Point26_6{
g.lastOffCurve,
midPoint(g.lastOffCurve, p),
},
}
g.lastOffCurve = p
g.lastOffCurveValid = true
return true
} else {
g.seg = Segment{
Op: SegmentOpQuadTo,
Args: [3]fixed.Point26_6{g.lastOffCurve, p},
}
g.lastOffCurveValid = false
return true
}
}
}
return false
}
func (g *glyfIter) nextPoint() (ok bool) {
if g.p == g.nPoints {
return false
}
g.p++
if g.repeats > 0 {
g.repeats--
} else {
g.flag = g.data[g.flagIndex]
g.flagIndex++
if g.flag&flagRepeat != 0 {
g.repeats = g.data[g.flagIndex]
g.flagIndex++
}
}
if g.flag&flagXShortVector != 0 {
if g.flag&flagPositiveXShortVector != 0 {
g.x += int16(g.data[g.xIndex])
} else {
g.x -= int16(g.data[g.xIndex])
}
g.xIndex += 1
} else if g.flag&flagThisXIsSame == 0 {
g.x += int16(u16(g.data[g.xIndex:]))
g.xIndex += 2
}
if g.flag&flagYShortVector != 0 {
if g.flag&flagPositiveYShortVector != 0 {
g.y += int16(g.data[g.yIndex])
} else {
g.y -= int16(g.data[g.yIndex])
}
g.yIndex += 1
} else if g.flag&flagThisYIsSame == 0 {
g.y += int16(u16(g.data[g.yIndex:]))
g.yIndex += 2
}
g.on = g.flag&flagOnCurve != 0
return true
}

30
vendor/golang.org/x/image/vector/acc_amd64.go generated vendored Normal file
View File

@ -0,0 +1,30 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !appengine && gc && !noasm
// +build !appengine,gc,!noasm
package vector
func haveSSE4_1() bool
var haveAccumulateSIMD = haveSSE4_1()
//go:noescape
func fixedAccumulateOpOverSIMD(dst []uint8, src []uint32)
//go:noescape
func fixedAccumulateOpSrcSIMD(dst []uint8, src []uint32)
//go:noescape
func fixedAccumulateMaskSIMD(buf []uint32)
//go:noescape
func floatingAccumulateOpOverSIMD(dst []uint8, src []float32)
//go:noescape
func floatingAccumulateOpSrcSIMD(dst []uint8, src []float32)
//go:noescape
func floatingAccumulateMaskSIMD(dst []uint32, src []float32)

1028
vendor/golang.org/x/image/vector/acc_amd64.s generated vendored Normal file

File diff suppressed because it is too large Load Diff

17
vendor/golang.org/x/image/vector/acc_other.go generated vendored Normal file
View File

@ -0,0 +1,17 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:build !amd64 || appengine || !gc || noasm
// +build !amd64 appengine !gc noasm
package vector
const haveAccumulateSIMD = false
func fixedAccumulateOpOverSIMD(dst []uint8, src []uint32) {}
func fixedAccumulateOpSrcSIMD(dst []uint8, src []uint32) {}
func fixedAccumulateMaskSIMD(buf []uint32) {}
func floatingAccumulateOpOverSIMD(dst []uint8, src []float32) {}
func floatingAccumulateOpSrcSIMD(dst []uint8, src []float32) {}
func floatingAccumulateMaskSIMD(dst []uint32, src []float32) {}

170
vendor/golang.org/x/image/vector/gen_acc_amd64.s.tmpl generated vendored Normal file
View File

@ -0,0 +1,170 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// +build !appengine
// +build gc
// +build !noasm
#include "textflag.h"
// fl is short for floating point math. fx is short for fixed point math.
DATA flAlmost65536<>+0x00(SB)/8, $0x477fffff477fffff
DATA flAlmost65536<>+0x08(SB)/8, $0x477fffff477fffff
DATA flOne<>+0x00(SB)/8, $0x3f8000003f800000
DATA flOne<>+0x08(SB)/8, $0x3f8000003f800000
DATA flSignMask<>+0x00(SB)/8, $0x7fffffff7fffffff
DATA flSignMask<>+0x08(SB)/8, $0x7fffffff7fffffff
// scatterAndMulBy0x101 is a PSHUFB mask that brings the low four bytes of an
// XMM register to the low byte of that register's four uint32 values. It
// duplicates those bytes, effectively multiplying each uint32 by 0x101.
//
// It transforms a little-endian 16-byte XMM value from
// ijkl????????????
// to
// ii00jj00kk00ll00
DATA scatterAndMulBy0x101<>+0x00(SB)/8, $0x8080010180800000
DATA scatterAndMulBy0x101<>+0x08(SB)/8, $0x8080030380800202
// gather is a PSHUFB mask that brings the second-lowest byte of the XMM
// register's four uint32 values to the low four bytes of that register.
//
// It transforms a little-endian 16-byte XMM value from
// ?i???j???k???l??
// to
// ijkl000000000000
DATA gather<>+0x00(SB)/8, $0x808080800d090501
DATA gather<>+0x08(SB)/8, $0x8080808080808080
DATA fxAlmost65536<>+0x00(SB)/8, $0x0000ffff0000ffff
DATA fxAlmost65536<>+0x08(SB)/8, $0x0000ffff0000ffff
DATA inverseFFFF<>+0x00(SB)/8, $0x8000800180008001
DATA inverseFFFF<>+0x08(SB)/8, $0x8000800180008001
GLOBL flAlmost65536<>(SB), (NOPTR+RODATA), $16
GLOBL flOne<>(SB), (NOPTR+RODATA), $16
GLOBL flSignMask<>(SB), (NOPTR+RODATA), $16
GLOBL scatterAndMulBy0x101<>(SB), (NOPTR+RODATA), $16
GLOBL gather<>(SB), (NOPTR+RODATA), $16
GLOBL fxAlmost65536<>(SB), (NOPTR+RODATA), $16
GLOBL inverseFFFF<>(SB), (NOPTR+RODATA), $16
// func haveSSE4_1() bool
TEXT ·haveSSE4_1(SB), NOSPLIT, $0
MOVQ $1, AX
CPUID
SHRQ $19, CX
ANDQ $1, CX
MOVB CX, ret+0(FP)
RET
// ----------------------------------------------------------------------------
// func {{.LongName}}SIMD({{.Args}})
//
// XMM registers. Variable names are per
// https://github.com/google/font-rs/blob/master/src/accumulate.c
//
// xmm0 scratch
// xmm1 x
// xmm2 y, z
// xmm3 {{.XMM3}}
// xmm4 {{.XMM4}}
// xmm5 {{.XMM5}}
// xmm6 {{.XMM6}}
// xmm7 offset
// xmm8 {{.XMM8}}
// xmm9 {{.XMM9}}
// xmm10 {{.XMM10}}
TEXT ·{{.LongName}}SIMD(SB), NOSPLIT, ${{.FrameSize}}-{{.ArgsSize}}
{{.LoadArgs}}
// R10 = len(src) &^ 3
// R11 = len(src)
MOVQ R10, R11
ANDQ $-4, R10
{{.Setup}}
{{.LoadXMMRegs}}
// offset := XMM(0x00000000 repeated four times) // Cumulative sum.
XORPS X7, X7
// i := 0
MOVQ $0, R9
{{.ShortName}}Loop4:
// for i < (len(src) &^ 3)
CMPQ R9, R10
JAE {{.ShortName}}Loop1
// x = XMM(s0, s1, s2, s3)
//
// Where s0 is src[i+0], s1 is src[i+1], etc.
MOVOU (SI), X1
// scratch = XMM(0, s0, s1, s2)
// x += scratch // yields x == XMM(s0, s0+s1, s1+s2, s2+s3)
MOVOU X1, X0
PSLLO $4, X0
{{.Add}} X0, X1
// scratch = XMM(0, 0, 0, 0)
// scratch = XMM(scratch@0, scratch@0, x@0, x@1) // yields scratch == XMM(0, 0, s0, s0+s1)
// x += scratch // yields x == XMM(s0, s0+s1, s0+s1+s2, s0+s1+s2+s3)
XORPS X0, X0
SHUFPS $0x40, X1, X0
{{.Add}} X0, X1
// x += offset
{{.Add}} X7, X1
{{.ClampAndScale}}
{{.ConvertToInt32}}
{{.Store4}}
// offset = XMM(x@3, x@3, x@3, x@3)
MOVOU X1, X7
SHUFPS $0xff, X1, X7
// i += 4
// dst = dst[4:]
// src = src[4:]
ADDQ $4, R9
ADDQ ${{.DstElemSize4}}, DI
ADDQ $16, SI
JMP {{.ShortName}}Loop4
{{.ShortName}}Loop1:
// for i < len(src)
CMPQ R9, R11
JAE {{.ShortName}}End
// x = src[i] + offset
MOVL (SI), X1
{{.Add}} X7, X1
{{.ClampAndScale}}
{{.ConvertToInt32}}
{{.Store1}}
// offset = x
MOVOU X1, X7
// i += 1
// dst = dst[1:]
// src = src[1:]
ADDQ $1, R9
ADDQ ${{.DstElemSize1}}, DI
ADDQ $4, SI
JMP {{.ShortName}}Loop1
{{.ShortName}}End:
RET

327
vendor/golang.org/x/image/vector/raster_fixed.go generated vendored Normal file
View File

@ -0,0 +1,327 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vector
// This file contains a fixed point math implementation of the vector
// graphics rasterizer.
const (
// ϕ is the number of binary digits after the fixed point.
//
// For example, if ϕ == 10 (and int1ϕ is based on the int32 type) then we
// are using 22.10 fixed point math.
//
// When changing this number, also change the assembly code (search for ϕ
// in the .s files).
ϕ = 9
fxOne int1ϕ = 1 << ϕ
fxOneAndAHalf int1ϕ = 1<<ϕ + 1<<(ϕ-1)
fxOneMinusIota int1ϕ = 1<<ϕ - 1 // Used for rounding up.
)
// int1ϕ is a signed fixed-point number with 1*ϕ binary digits after the fixed
// point.
type int1ϕ int32
// int2ϕ is a signed fixed-point number with 2*ϕ binary digits after the fixed
// point.
//
// The Rasterizer's bufU32 field, nominally of type []uint32 (since that slice
// is also used by other code), can be thought of as a []int2ϕ during the
// fixedLineTo method. Lines of code that are actually like:
// buf[i] += uint32(etc) // buf has type []uint32.
// can be thought of as
// buf[i] += int2ϕ(etc) // buf has type []int2ϕ.
type int2ϕ int32
func fixedMax(x, y int1ϕ) int1ϕ {
if x > y {
return x
}
return y
}
func fixedMin(x, y int1ϕ) int1ϕ {
if x < y {
return x
}
return y
}
func fixedFloor(x int1ϕ) int32 { return int32(x >> ϕ) }
func fixedCeil(x int1ϕ) int32 { return int32((x + fxOneMinusIota) >> ϕ) }
func (z *Rasterizer) fixedLineTo(bx, by float32) {
ax, ay := z.penX, z.penY
z.penX, z.penY = bx, by
dir := int1ϕ(1)
if ay > by {
dir, ax, ay, bx, by = -1, bx, by, ax, ay
}
// Horizontal line segments yield no change in coverage. Almost horizontal
// segments would yield some change, in ideal math, but the computation
// further below, involving 1 / (by - ay), is unstable in fixed point math,
// so we treat the segment as if it was perfectly horizontal.
if by-ay <= 0.000001 {
return
}
dxdy := (bx - ax) / (by - ay)
ayϕ := int1ϕ(ay * float32(fxOne))
byϕ := int1ϕ(by * float32(fxOne))
x := int1ϕ(ax * float32(fxOne))
y := fixedFloor(ayϕ)
yMax := fixedCeil(byϕ)
if yMax > int32(z.size.Y) {
yMax = int32(z.size.Y)
}
width := int32(z.size.X)
for ; y < yMax; y++ {
dy := fixedMin(int1ϕ(y+1)<<ϕ, byϕ) - fixedMax(int1ϕ(y)<<ϕ, ayϕ)
xNext := x + int1ϕ(float32(dy)*dxdy)
if y < 0 {
x = xNext
continue
}
buf := z.bufU32[y*width:]
d := dy * dir // d ranges up to ±1<<(1*ϕ).
x0, x1 := x, xNext
if x > xNext {
x0, x1 = x1, x0
}
x0i := fixedFloor(x0)
x0Floor := int1ϕ(x0i) << ϕ
x1i := fixedCeil(x1)
x1Ceil := int1ϕ(x1i) << ϕ
if x1i <= x0i+1 {
xmf := (x+xNext)>>1 - x0Floor
if i := clamp(x0i+0, width); i < uint(len(buf)) {
buf[i] += uint32(d * (fxOne - xmf))
}
if i := clamp(x0i+1, width); i < uint(len(buf)) {
buf[i] += uint32(d * xmf)
}
} else {
oneOverS := x1 - x0
twoOverS := 2 * oneOverS
x0f := x0 - x0Floor
oneMinusX0f := fxOne - x0f
oneMinusX0fSquared := oneMinusX0f * oneMinusX0f
x1f := x1 - x1Ceil + fxOne
x1fSquared := x1f * x1f
// These next two variables are unused, as rounding errors are
// minimized when we delay the division by oneOverS for as long as
// possible. These lines of code (and the "In ideal math" comments
// below) are commented out instead of deleted in order to aid the
// comparison with the floating point version of the rasterizer.
//
// a0 := ((oneMinusX0f * oneMinusX0f) >> 1) / oneOverS
// am := ((x1f * x1f) >> 1) / oneOverS
if i := clamp(x0i, width); i < uint(len(buf)) {
// In ideal math: buf[i] += uint32(d * a0)
D := oneMinusX0fSquared // D ranges up to ±1<<(2*ϕ).
D *= d // D ranges up to ±1<<(3*ϕ).
D /= twoOverS
buf[i] += uint32(D)
}
if x1i == x0i+2 {
if i := clamp(x0i+1, width); i < uint(len(buf)) {
// In ideal math: buf[i] += uint32(d * (fxOne - a0 - am))
//
// (x1i == x0i+2) and (twoOverS == 2 * (x1 - x0)) implies
// that twoOverS ranges up to +1<<(1*ϕ+2).
D := twoOverS<<ϕ - oneMinusX0fSquared - x1fSquared // D ranges up to ±1<<(2*ϕ+2).
D *= d // D ranges up to ±1<<(3*ϕ+2).
D /= twoOverS
buf[i] += uint32(D)
}
} else {
// This is commented out for the same reason as a0 and am.
//
// a1 := ((fxOneAndAHalf - x0f) << ϕ) / oneOverS
if i := clamp(x0i+1, width); i < uint(len(buf)) {
// In ideal math:
// buf[i] += uint32(d * (a1 - a0))
// or equivalently (but better in non-ideal, integer math,
// with respect to rounding errors),
// buf[i] += uint32(A * d / twoOverS)
// where
// A = (a1 - a0) * twoOverS
// = a1*twoOverS - a0*twoOverS
// Noting that twoOverS/oneOverS equals 2, substituting for
// a0 and then a1, given above, yields:
// A = a1*twoOverS - oneMinusX0fSquared
// = (fxOneAndAHalf-x0f)<<(ϕ+1) - oneMinusX0fSquared
// = fxOneAndAHalf<<(ϕ+1) - x0f<<(ϕ+1) - oneMinusX0fSquared
//
// This is a positive number minus two non-negative
// numbers. For an upper bound on A, the positive number is
// P = fxOneAndAHalf<<(ϕ+1)
// < (2*fxOne)<<(ϕ+1)
// = fxOne<<(ϕ+2)
// = 1<<(2*ϕ+2)
//
// For a lower bound on A, the two non-negative numbers are
// N = x0f<<(ϕ+1) + oneMinusX0fSquared
// ≤ x0f<<(ϕ+1) + fxOne*fxOne
// = x0f<<(ϕ+1) + 1<<(2*ϕ)
// < x0f<<(ϕ+1) + 1<<(2*ϕ+1)
// ≤ fxOne<<(ϕ+1) + 1<<(2*ϕ+1)
// = 1<<(2*ϕ+1) + 1<<(2*ϕ+1)
// = 1<<(2*ϕ+2)
//
// Thus, A ranges up to ±1<<(2*ϕ+2). It is possible to
// derive a tighter bound, but this bound is sufficient to
// reason about overflow.
D := (fxOneAndAHalf-x0f)<<(ϕ+1) - oneMinusX0fSquared // D ranges up to ±1<<(2*ϕ+2).
D *= d // D ranges up to ±1<<(3*ϕ+2).
D /= twoOverS
buf[i] += uint32(D)
}
dTimesS := uint32((d << (2 * ϕ)) / oneOverS)
for xi := x0i + 2; xi < x1i-1; xi++ {
if i := clamp(xi, width); i < uint(len(buf)) {
buf[i] += dTimesS
}
}
// This is commented out for the same reason as a0 and am.
//
// a2 := a1 + (int1ϕ(x1i-x0i-3)<<(2*ϕ))/oneOverS
if i := clamp(x1i-1, width); i < uint(len(buf)) {
// In ideal math:
// buf[i] += uint32(d * (fxOne - a2 - am))
// or equivalently (but better in non-ideal, integer math,
// with respect to rounding errors),
// buf[i] += uint32(A * d / twoOverS)
// where
// A = (fxOne - a2 - am) * twoOverS
// = twoOverS<<ϕ - a2*twoOverS - am*twoOverS
// Noting that twoOverS/oneOverS equals 2, substituting for
// am and then a2, given above, yields:
// A = twoOverS<<ϕ - a2*twoOverS - x1f*x1f
// = twoOverS<<ϕ - a1*twoOverS - (int1ϕ(x1i-x0i-3)<<(2*ϕ))*2 - x1f*x1f
// = twoOverS<<ϕ - a1*twoOverS - int1ϕ(x1i-x0i-3)<<(2*ϕ+1) - x1f*x1f
// Substituting for a1, given above, yields:
// A = twoOverS<<ϕ - ((fxOneAndAHalf-x0f)<<ϕ)*2 - int1ϕ(x1i-x0i-3)<<(2*ϕ+1) - x1f*x1f
// = twoOverS<<ϕ - (fxOneAndAHalf-x0f)<<(ϕ+1) - int1ϕ(x1i-x0i-3)<<(2*ϕ+1) - x1f*x1f
// = B<<ϕ - x1f*x1f
// where
// B = twoOverS - (fxOneAndAHalf-x0f)<<1 - int1ϕ(x1i-x0i-3)<<(ϕ+1)
// = (x1-x0)<<1 - (fxOneAndAHalf-x0f)<<1 - int1ϕ(x1i-x0i-3)<<(ϕ+1)
//
// Re-arranging the defintions given above:
// x0Floor := int1ϕ(x0i) << ϕ
// x0f := x0 - x0Floor
// x1Ceil := int1ϕ(x1i) << ϕ
// x1f := x1 - x1Ceil + fxOne
// combined with fxOne = 1<<ϕ yields:
// x0 = x0f + int1ϕ(x0i)<<ϕ
// x1 = x1f + int1ϕ(x1i-1)<<ϕ
// so that expanding (x1-x0) yields:
// B = (x1f-x0f + int1ϕ(x1i-x0i-1)<<ϕ)<<1 - (fxOneAndAHalf-x0f)<<1 - int1ϕ(x1i-x0i-3)<<(ϕ+1)
// = (x1f-x0f)<<1 + int1ϕ(x1i-x0i-1)<<(ϕ+1) - (fxOneAndAHalf-x0f)<<1 - int1ϕ(x1i-x0i-3)<<(ϕ+1)
// A large part of the second and fourth terms cancel:
// B = (x1f-x0f)<<1 - (fxOneAndAHalf-x0f)<<1 - int1ϕ(-2)<<(ϕ+1)
// = (x1f-x0f)<<1 - (fxOneAndAHalf-x0f)<<1 + 1<<(ϕ+2)
// = (x1f - fxOneAndAHalf)<<1 + 1<<(ϕ+2)
// The first term, (x1f - fxOneAndAHalf)<<1, is a negative
// number, bounded below by -fxOneAndAHalf<<1, which is
// greater than -fxOne<<2, or -1<<(ϕ+2). Thus, B ranges up
// to ±1<<(ϕ+2). One final simplification:
// B = x1f<<1 + (1<<(ϕ+2) - fxOneAndAHalf<<1)
const C = 1<<(ϕ+2) - fxOneAndAHalf<<1
D := x1f<<1 + C // D ranges up to ±1<<(1*ϕ+2).
D <<= ϕ // D ranges up to ±1<<(2*ϕ+2).
D -= x1fSquared // D ranges up to ±1<<(2*ϕ+3).
D *= d // D ranges up to ±1<<(3*ϕ+3).
D /= twoOverS
buf[i] += uint32(D)
}
}
if i := clamp(x1i, width); i < uint(len(buf)) {
// In ideal math: buf[i] += uint32(d * am)
D := x1fSquared // D ranges up to ±1<<(2*ϕ).
D *= d // D ranges up to ±1<<(3*ϕ).
D /= twoOverS
buf[i] += uint32(D)
}
}
x = xNext
}
}
func fixedAccumulateOpOver(dst []uint8, src []uint32) {
// Sanity check that len(dst) >= len(src).
if len(dst) < len(src) {
return
}
acc := int2ϕ(0)
for i, v := range src {
acc += int2ϕ(v)
a := acc
if a < 0 {
a = -a
}
a >>= 2*ϕ - 16
if a > 0xffff {
a = 0xffff
}
// This algorithm comes from the standard library's image/draw package.
dstA := uint32(dst[i]) * 0x101
maskA := uint32(a)
outA := dstA*(0xffff-maskA)/0xffff + maskA
dst[i] = uint8(outA >> 8)
}
}
func fixedAccumulateOpSrc(dst []uint8, src []uint32) {
// Sanity check that len(dst) >= len(src).
if len(dst) < len(src) {
return
}
acc := int2ϕ(0)
for i, v := range src {
acc += int2ϕ(v)
a := acc
if a < 0 {
a = -a
}
a >>= 2*ϕ - 8
if a > 0xff {
a = 0xff
}
dst[i] = uint8(a)
}
}
func fixedAccumulateMask(buf []uint32) {
acc := int2ϕ(0)
for i, v := range buf {
acc += int2ϕ(v)
a := acc
if a < 0 {
a = -a
}
a >>= 2*ϕ - 16
if a > 0xffff {
a = 0xffff
}
buf[i] = uint32(a)
}
}

220
vendor/golang.org/x/image/vector/raster_floating.go generated vendored Normal file
View File

@ -0,0 +1,220 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vector
// This file contains a floating point math implementation of the vector
// graphics rasterizer.
import (
"math"
)
func floatingMax(x, y float32) float32 {
if x > y {
return x
}
return y
}
func floatingMin(x, y float32) float32 {
if x < y {
return x
}
return y
}
func floatingFloor(x float32) int32 { return int32(math.Floor(float64(x))) }
func floatingCeil(x float32) int32 { return int32(math.Ceil(float64(x))) }
func (z *Rasterizer) floatingLineTo(bx, by float32) {
ax, ay := z.penX, z.penY
z.penX, z.penY = bx, by
dir := float32(1)
if ay > by {
dir, ax, ay, bx, by = -1, bx, by, ax, ay
}
// Horizontal line segments yield no change in coverage. Almost horizontal
// segments would yield some change, in ideal math, but the computation
// further below, involving 1 / (by - ay), is unstable in floating point
// math, so we treat the segment as if it was perfectly horizontal.
if by-ay <= 0.000001 {
return
}
dxdy := (bx - ax) / (by - ay)
x := ax
y := floatingFloor(ay)
yMax := floatingCeil(by)
if yMax > int32(z.size.Y) {
yMax = int32(z.size.Y)
}
width := int32(z.size.X)
for ; y < yMax; y++ {
dy := floatingMin(float32(y+1), by) - floatingMax(float32(y), ay)
// The "float32" in expressions like "float32(foo*bar)" here and below
// look redundant, since foo and bar already have type float32, but are
// explicit in order to disable the compiler's Fused Multiply Add (FMA)
// instruction selection, which can improve performance but can result
// in different rounding errors in floating point computations.
//
// This package aims to have bit-exact identical results across all
// GOARCHes, and across pure Go code and assembly, so it disables FMA.
//
// See the discussion at
// https://groups.google.com/d/topic/golang-dev/Sti0bl2xUXQ/discussion
xNext := x + float32(dy*dxdy)
if y < 0 {
x = xNext
continue
}
buf := z.bufF32[y*width:]
d := float32(dy * dir)
x0, x1 := x, xNext
if x > xNext {
x0, x1 = x1, x0
}
x0i := floatingFloor(x0)
x0Floor := float32(x0i)
x1i := floatingCeil(x1)
x1Ceil := float32(x1i)
if x1i <= x0i+1 {
xmf := float32(0.5*(x+xNext)) - x0Floor
if i := clamp(x0i+0, width); i < uint(len(buf)) {
buf[i] += d - float32(d*xmf)
}
if i := clamp(x0i+1, width); i < uint(len(buf)) {
buf[i] += float32(d * xmf)
}
} else {
s := 1 / (x1 - x0)
x0f := x0 - x0Floor
oneMinusX0f := 1 - x0f
a0 := float32(0.5 * s * oneMinusX0f * oneMinusX0f)
x1f := x1 - x1Ceil + 1
am := float32(0.5 * s * x1f * x1f)
if i := clamp(x0i, width); i < uint(len(buf)) {
buf[i] += float32(d * a0)
}
if x1i == x0i+2 {
if i := clamp(x0i+1, width); i < uint(len(buf)) {
buf[i] += float32(d * (1 - a0 - am))
}
} else {
a1 := float32(s * (1.5 - x0f))
if i := clamp(x0i+1, width); i < uint(len(buf)) {
buf[i] += float32(d * (a1 - a0))
}
dTimesS := float32(d * s)
for xi := x0i + 2; xi < x1i-1; xi++ {
if i := clamp(xi, width); i < uint(len(buf)) {
buf[i] += dTimesS
}
}
a2 := a1 + float32(s*float32(x1i-x0i-3))
if i := clamp(x1i-1, width); i < uint(len(buf)) {
buf[i] += float32(d * (1 - a2 - am))
}
}
if i := clamp(x1i, width); i < uint(len(buf)) {
buf[i] += float32(d * am)
}
}
x = xNext
}
}
const (
// almost256 scales a floating point value in the range [0, 1] to a uint8
// value in the range [0x00, 0xff].
//
// 255 is too small. Floating point math accumulates rounding errors, so a
// fully covered src value that would in ideal math be float32(1) might be
// float32(1-ε), and uint8(255 * (1-ε)) would be 0xfe instead of 0xff. The
// uint8 conversion rounds to zero, not to nearest.
//
// 256 is too big. If we multiplied by 256, below, then a fully covered src
// value of float32(1) would translate to uint8(256 * 1), which can be 0x00
// instead of the maximal value 0xff.
//
// math.Float32bits(almost256) is 0x437fffff.
almost256 = 255.99998
// almost65536 scales a floating point value in the range [0, 1] to a
// uint16 value in the range [0x0000, 0xffff].
//
// math.Float32bits(almost65536) is 0x477fffff.
almost65536 = almost256 * 256
)
func floatingAccumulateOpOver(dst []uint8, src []float32) {
// Sanity check that len(dst) >= len(src).
if len(dst) < len(src) {
return
}
acc := float32(0)
for i, v := range src {
acc += v
a := acc
if a < 0 {
a = -a
}
if a > 1 {
a = 1
}
// This algorithm comes from the standard library's image/draw package.
dstA := uint32(dst[i]) * 0x101
maskA := uint32(almost65536 * a)
outA := dstA*(0xffff-maskA)/0xffff + maskA
dst[i] = uint8(outA >> 8)
}
}
func floatingAccumulateOpSrc(dst []uint8, src []float32) {
// Sanity check that len(dst) >= len(src).
if len(dst) < len(src) {
return
}
acc := float32(0)
for i, v := range src {
acc += v
a := acc
if a < 0 {
a = -a
}
if a > 1 {
a = 1
}
dst[i] = uint8(almost256 * a)
}
}
func floatingAccumulateMask(dst []uint32, src []float32) {
// Sanity check that len(dst) >= len(src).
if len(dst) < len(src) {
return
}
acc := float32(0)
for i, v := range src {
acc += v
a := acc
if a < 0 {
a = -a
}
if a > 1 {
a = 1
}
dst[i] = uint32(almost65536 * a)
}
}

472
vendor/golang.org/x/image/vector/vector.go generated vendored Normal file
View File

@ -0,0 +1,472 @@
// Copyright 2016 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
//go:generate go run gen.go
//go:generate asmfmt -w acc_amd64.s
// asmfmt is https://github.com/klauspost/asmfmt
// Package vector provides a rasterizer for 2-D vector graphics.
package vector // import "golang.org/x/image/vector"
// The rasterizer's design follows
// https://medium.com/@raphlinus/inside-the-fastest-font-renderer-in-the-world-75ae5270c445
//
// Proof of concept code is in
// https://github.com/google/font-go
//
// See also:
// http://nothings.org/gamedev/rasterize/
// http://projects.tuxee.net/cl-vectors/section-the-cl-aa-algorithm
// https://people.gnome.org/~mathieu/libart/internals.html#INTERNALS-SCANLINE
import (
"image"
"image/color"
"image/draw"
"math"
)
// floatingPointMathThreshold is the width or height above which the rasterizer
// chooses to used floating point math instead of fixed point math.
//
// Both implementations of line segmentation rasterization (see raster_fixed.go
// and raster_floating.go) implement the same algorithm (in ideal, infinite
// precision math) but they perform differently in practice. The fixed point
// math version is roughtly 1.25x faster (on GOARCH=amd64) on the benchmarks,
// but at sufficiently large scales, the computations will overflow and hence
// show rendering artifacts. The floating point math version has more
// consistent quality over larger scales, but it is significantly slower.
//
// This constant determines when to use the faster implementation and when to
// use the better quality implementation.
//
// The rationale for this particular value is that TestRasterizePolygon in
// vector_test.go checks the rendering quality of polygon edges at various
// angles, inscribed in a circle of diameter 512. It may be that a higher value
// would still produce acceptable quality, but 512 seems to work.
const floatingPointMathThreshold = 512
func lerp(t, px, py, qx, qy float32) (x, y float32) {
return px + t*(qx-px), py + t*(qy-py)
}
func clamp(i, width int32) uint {
if i < 0 {
return 0
}
if i < width {
return uint(i)
}
return uint(width)
}
// NewRasterizer returns a new Rasterizer whose rendered mask image is bounded
// by the given width and height.
func NewRasterizer(w, h int) *Rasterizer {
z := &Rasterizer{}
z.Reset(w, h)
return z
}
// Raster is a 2-D vector graphics rasterizer.
//
// The zero value is usable, in that it is a Rasterizer whose rendered mask
// image has zero width and zero height. Call Reset to change its bounds.
type Rasterizer struct {
// bufXxx are buffers of float32 or uint32 values, holding either the
// individual or cumulative area values.
//
// We don't actually need both values at any given time, and to conserve
// memory, the integration of the individual to the cumulative could modify
// the buffer in place. In other words, we could use a single buffer, say
// of type []uint32, and add some math.Float32bits and math.Float32frombits
// calls to satisfy the compiler's type checking. As of Go 1.7, though,
// there is a performance penalty between:
// bufF32[i] += x
// and
// bufU32[i] = math.Float32bits(x + math.Float32frombits(bufU32[i]))
//
// See golang.org/issue/17220 for some discussion.
bufF32 []float32
bufU32 []uint32
useFloatingPointMath bool
size image.Point
firstX float32
firstY float32
penX float32
penY float32
// DrawOp is the operator used for the Draw method.
//
// The zero value is draw.Over.
DrawOp draw.Op
// TODO: an exported field equivalent to the mask point in the
// draw.DrawMask function in the stdlib image/draw package?
}
// Reset resets a Rasterizer as if it was just returned by NewRasterizer.
//
// This includes setting z.DrawOp to draw.Over.
func (z *Rasterizer) Reset(w, h int) {
z.size = image.Point{w, h}
z.firstX = 0
z.firstY = 0
z.penX = 0
z.penY = 0
z.DrawOp = draw.Over
z.setUseFloatingPointMath(w > floatingPointMathThreshold || h > floatingPointMathThreshold)
}
func (z *Rasterizer) setUseFloatingPointMath(b bool) {
z.useFloatingPointMath = b
// Make z.bufF32 or z.bufU32 large enough to hold width * height samples.
if z.useFloatingPointMath {
if n := z.size.X * z.size.Y; n > cap(z.bufF32) {
z.bufF32 = make([]float32, n)
} else {
z.bufF32 = z.bufF32[:n]
for i := range z.bufF32 {
z.bufF32[i] = 0
}
}
} else {
if n := z.size.X * z.size.Y; n > cap(z.bufU32) {
z.bufU32 = make([]uint32, n)
} else {
z.bufU32 = z.bufU32[:n]
for i := range z.bufU32 {
z.bufU32[i] = 0
}
}
}
}
// Size returns the width and height passed to NewRasterizer or Reset.
func (z *Rasterizer) Size() image.Point {
return z.size
}
// Bounds returns the rectangle from (0, 0) to the width and height passed to
// NewRasterizer or Reset.
func (z *Rasterizer) Bounds() image.Rectangle {
return image.Rectangle{Max: z.size}
}
// Pen returns the location of the path-drawing pen: the last argument to the
// most recent XxxTo call.
func (z *Rasterizer) Pen() (x, y float32) {
return z.penX, z.penY
}
// ClosePath closes the current path.
func (z *Rasterizer) ClosePath() {
z.LineTo(z.firstX, z.firstY)
}
// MoveTo starts a new path and moves the pen to (ax, ay).
//
// The coordinates are allowed to be out of the Rasterizer's bounds.
func (z *Rasterizer) MoveTo(ax, ay float32) {
z.firstX = ax
z.firstY = ay
z.penX = ax
z.penY = ay
}
// LineTo adds a line segment, from the pen to (bx, by), and moves the pen to
// (bx, by).
//
// The coordinates are allowed to be out of the Rasterizer's bounds.
func (z *Rasterizer) LineTo(bx, by float32) {
if z.useFloatingPointMath {
z.floatingLineTo(bx, by)
} else {
z.fixedLineTo(bx, by)
}
}
// QuadTo adds a quadratic Bézier segment, from the pen via (bx, by) to (cx,
// cy), and moves the pen to (cx, cy).
//
// The coordinates are allowed to be out of the Rasterizer's bounds.
func (z *Rasterizer) QuadTo(bx, by, cx, cy float32) {
ax, ay := z.penX, z.penY
devsq := devSquared(ax, ay, bx, by, cx, cy)
if devsq >= 0.333 {
const tol = 3
n := 1 + int(math.Sqrt(math.Sqrt(tol*float64(devsq))))
t, nInv := float32(0), 1/float32(n)
for i := 0; i < n-1; i++ {
t += nInv
abx, aby := lerp(t, ax, ay, bx, by)
bcx, bcy := lerp(t, bx, by, cx, cy)
z.LineTo(lerp(t, abx, aby, bcx, bcy))
}
}
z.LineTo(cx, cy)
}
// CubeTo adds a cubic Bézier segment, from the pen via (bx, by) and (cx, cy)
// to (dx, dy), and moves the pen to (dx, dy).
//
// The coordinates are allowed to be out of the Rasterizer's bounds.
func (z *Rasterizer) CubeTo(bx, by, cx, cy, dx, dy float32) {
ax, ay := z.penX, z.penY
devsq := devSquared(ax, ay, bx, by, dx, dy)
if devsqAlt := devSquared(ax, ay, cx, cy, dx, dy); devsq < devsqAlt {
devsq = devsqAlt
}
if devsq >= 0.333 {
const tol = 3
n := 1 + int(math.Sqrt(math.Sqrt(tol*float64(devsq))))
t, nInv := float32(0), 1/float32(n)
for i := 0; i < n-1; i++ {
t += nInv
abx, aby := lerp(t, ax, ay, bx, by)
bcx, bcy := lerp(t, bx, by, cx, cy)
cdx, cdy := lerp(t, cx, cy, dx, dy)
abcx, abcy := lerp(t, abx, aby, bcx, bcy)
bcdx, bcdy := lerp(t, bcx, bcy, cdx, cdy)
z.LineTo(lerp(t, abcx, abcy, bcdx, bcdy))
}
}
z.LineTo(dx, dy)
}
// devSquared returns a measure of how curvy the sequence (ax, ay) to (bx, by)
// to (cx, cy) is. It determines how many line segments will approximate a
// Bézier curve segment.
//
// http://lists.nongnu.org/archive/html/freetype-devel/2016-08/msg00080.html
// gives the rationale for this evenly spaced heuristic instead of a recursive
// de Casteljau approach:
//
// The reason for the subdivision by n is that I expect the "flatness"
// computation to be semi-expensive (it's done once rather than on each
// potential subdivision) and also because you'll often get fewer subdivisions.
// Taking a circular arc as a simplifying assumption (ie a spherical cow),
// where I get n, a recursive approach would get 2^⌈lg n⌉, which, if I haven't
// made any horrible mistakes, is expected to be 33% more in the limit.
func devSquared(ax, ay, bx, by, cx, cy float32) float32 {
devx := ax - 2*bx + cx
devy := ay - 2*by + cy
return devx*devx + devy*devy
}
// Draw implements the Drawer interface from the standard library's image/draw
// package.
//
// The vector paths previously added via the XxxTo calls become the mask for
// drawing src onto dst.
func (z *Rasterizer) Draw(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) {
// TODO: adjust r and sp (and mp?) if src.Bounds() doesn't contain
// r.Add(sp.Sub(r.Min)).
if src, ok := src.(*image.Uniform); ok {
srcR, srcG, srcB, srcA := src.RGBA()
switch dst := dst.(type) {
case *image.Alpha:
// Fast path for glyph rendering.
if srcA == 0xffff {
if z.DrawOp == draw.Over {
z.rasterizeDstAlphaSrcOpaqueOpOver(dst, r)
} else {
z.rasterizeDstAlphaSrcOpaqueOpSrc(dst, r)
}
return
}
case *image.RGBA:
if z.DrawOp == draw.Over {
z.rasterizeDstRGBASrcUniformOpOver(dst, r, srcR, srcG, srcB, srcA)
} else {
z.rasterizeDstRGBASrcUniformOpSrc(dst, r, srcR, srcG, srcB, srcA)
}
return
}
}
if z.DrawOp == draw.Over {
z.rasterizeOpOver(dst, r, src, sp)
} else {
z.rasterizeOpSrc(dst, r, src, sp)
}
}
func (z *Rasterizer) accumulateMask() {
if z.useFloatingPointMath {
if n := z.size.X * z.size.Y; n > cap(z.bufU32) {
z.bufU32 = make([]uint32, n)
} else {
z.bufU32 = z.bufU32[:n]
}
if haveAccumulateSIMD {
floatingAccumulateMaskSIMD(z.bufU32, z.bufF32)
} else {
floatingAccumulateMask(z.bufU32, z.bufF32)
}
} else {
if haveAccumulateSIMD {
fixedAccumulateMaskSIMD(z.bufU32)
} else {
fixedAccumulateMask(z.bufU32)
}
}
}
func (z *Rasterizer) rasterizeDstAlphaSrcOpaqueOpOver(dst *image.Alpha, r image.Rectangle) {
// TODO: non-zero vs even-odd winding?
if r == dst.Bounds() && r == z.Bounds() {
// We bypass the z.accumulateMask step and convert straight from
// z.bufF32 or z.bufU32 to dst.Pix.
if z.useFloatingPointMath {
if haveAccumulateSIMD {
floatingAccumulateOpOverSIMD(dst.Pix, z.bufF32)
} else {
floatingAccumulateOpOver(dst.Pix, z.bufF32)
}
} else {
if haveAccumulateSIMD {
fixedAccumulateOpOverSIMD(dst.Pix, z.bufU32)
} else {
fixedAccumulateOpOver(dst.Pix, z.bufU32)
}
}
return
}
z.accumulateMask()
pix := dst.Pix[dst.PixOffset(r.Min.X, r.Min.Y):]
for y, y1 := 0, r.Max.Y-r.Min.Y; y < y1; y++ {
for x, x1 := 0, r.Max.X-r.Min.X; x < x1; x++ {
ma := z.bufU32[y*z.size.X+x]
i := y*dst.Stride + x
// This formula is like rasterizeOpOver's, simplified for the
// concrete dst type and opaque src assumption.
a := 0xffff - ma
pix[i] = uint8((uint32(pix[i])*0x101*a/0xffff + ma) >> 8)
}
}
}
func (z *Rasterizer) rasterizeDstAlphaSrcOpaqueOpSrc(dst *image.Alpha, r image.Rectangle) {
// TODO: non-zero vs even-odd winding?
if r == dst.Bounds() && r == z.Bounds() {
// We bypass the z.accumulateMask step and convert straight from
// z.bufF32 or z.bufU32 to dst.Pix.
if z.useFloatingPointMath {
if haveAccumulateSIMD {
floatingAccumulateOpSrcSIMD(dst.Pix, z.bufF32)
} else {
floatingAccumulateOpSrc(dst.Pix, z.bufF32)
}
} else {
if haveAccumulateSIMD {
fixedAccumulateOpSrcSIMD(dst.Pix, z.bufU32)
} else {
fixedAccumulateOpSrc(dst.Pix, z.bufU32)
}
}
return
}
z.accumulateMask()
pix := dst.Pix[dst.PixOffset(r.Min.X, r.Min.Y):]
for y, y1 := 0, r.Max.Y-r.Min.Y; y < y1; y++ {
for x, x1 := 0, r.Max.X-r.Min.X; x < x1; x++ {
ma := z.bufU32[y*z.size.X+x]
// This formula is like rasterizeOpSrc's, simplified for the
// concrete dst type and opaque src assumption.
pix[y*dst.Stride+x] = uint8(ma >> 8)
}
}
}
func (z *Rasterizer) rasterizeDstRGBASrcUniformOpOver(dst *image.RGBA, r image.Rectangle, sr, sg, sb, sa uint32) {
z.accumulateMask()
pix := dst.Pix[dst.PixOffset(r.Min.X, r.Min.Y):]
for y, y1 := 0, r.Max.Y-r.Min.Y; y < y1; y++ {
for x, x1 := 0, r.Max.X-r.Min.X; x < x1; x++ {
ma := z.bufU32[y*z.size.X+x]
// This formula is like rasterizeOpOver's, simplified for the
// concrete dst type and uniform src assumption.
a := 0xffff - (sa * ma / 0xffff)
i := y*dst.Stride + 4*x
pix[i+0] = uint8(((uint32(pix[i+0])*0x101*a + sr*ma) / 0xffff) >> 8)
pix[i+1] = uint8(((uint32(pix[i+1])*0x101*a + sg*ma) / 0xffff) >> 8)
pix[i+2] = uint8(((uint32(pix[i+2])*0x101*a + sb*ma) / 0xffff) >> 8)
pix[i+3] = uint8(((uint32(pix[i+3])*0x101*a + sa*ma) / 0xffff) >> 8)
}
}
}
func (z *Rasterizer) rasterizeDstRGBASrcUniformOpSrc(dst *image.RGBA, r image.Rectangle, sr, sg, sb, sa uint32) {
z.accumulateMask()
pix := dst.Pix[dst.PixOffset(r.Min.X, r.Min.Y):]
for y, y1 := 0, r.Max.Y-r.Min.Y; y < y1; y++ {
for x, x1 := 0, r.Max.X-r.Min.X; x < x1; x++ {
ma := z.bufU32[y*z.size.X+x]
// This formula is like rasterizeOpSrc's, simplified for the
// concrete dst type and uniform src assumption.
i := y*dst.Stride + 4*x
pix[i+0] = uint8((sr * ma / 0xffff) >> 8)
pix[i+1] = uint8((sg * ma / 0xffff) >> 8)
pix[i+2] = uint8((sb * ma / 0xffff) >> 8)
pix[i+3] = uint8((sa * ma / 0xffff) >> 8)
}
}
}
func (z *Rasterizer) rasterizeOpOver(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) {
z.accumulateMask()
out := color.RGBA64{}
outc := color.Color(&out)
for y, y1 := 0, r.Max.Y-r.Min.Y; y < y1; y++ {
for x, x1 := 0, r.Max.X-r.Min.X; x < x1; x++ {
sr, sg, sb, sa := src.At(sp.X+x, sp.Y+y).RGBA()
ma := z.bufU32[y*z.size.X+x]
// This algorithm comes from the standard library's image/draw
// package.
dr, dg, db, da := dst.At(r.Min.X+x, r.Min.Y+y).RGBA()
a := 0xffff - (sa * ma / 0xffff)
out.R = uint16((dr*a + sr*ma) / 0xffff)
out.G = uint16((dg*a + sg*ma) / 0xffff)
out.B = uint16((db*a + sb*ma) / 0xffff)
out.A = uint16((da*a + sa*ma) / 0xffff)
dst.Set(r.Min.X+x, r.Min.Y+y, outc)
}
}
}
func (z *Rasterizer) rasterizeOpSrc(dst draw.Image, r image.Rectangle, src image.Image, sp image.Point) {
z.accumulateMask()
out := color.RGBA64{}
outc := color.Color(&out)
for y, y1 := 0, r.Max.Y-r.Min.Y; y < y1; y++ {
for x, x1 := 0, r.Max.X-r.Min.X; x < x1; x++ {
sr, sg, sb, sa := src.At(sp.X+x, sp.Y+y).RGBA()
ma := z.bufU32[y*z.size.X+x]
// This algorithm comes from the standard library's image/draw
// package.
out.R = uint16(sr * ma / 0xffff)
out.G = uint16(sg * ma / 0xffff)
out.B = uint16(sb * ma / 0xffff)
out.A = uint16(sa * ma / 0xffff)
dst.Set(r.Min.X+x, r.Min.Y+y, outc)
}
}
}

3
vendor/modules.txt vendored
View File

@ -1174,9 +1174,12 @@ golang.org/x/image/bmp
golang.org/x/image/draw golang.org/x/image/draw
golang.org/x/image/font golang.org/x/image/font
golang.org/x/image/font/basicfont golang.org/x/image/font/basicfont
golang.org/x/image/font/opentype
golang.org/x/image/font/sfnt
golang.org/x/image/math/f64 golang.org/x/image/math/f64
golang.org/x/image/math/fixed golang.org/x/image/math/fixed
golang.org/x/image/riff golang.org/x/image/riff
golang.org/x/image/vector
golang.org/x/image/vp8 golang.org/x/image/vp8
golang.org/x/image/vp8l golang.org/x/image/vp8l
golang.org/x/image/webp golang.org/x/image/webp