feat: render initials avatar using media server (#3513)
This commit is contained in:
parent
57b4036da3
commit
6cf0162877
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -137,6 +137,33 @@ func ImageToBytes(imagePath string) ([]byte, error) {
|
|||
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 {
|
||||
bounds := img.Bounds()
|
||||
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
|
||||
}
|
||||
|
||||
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 {
|
||||
bounds := circle.Bounds()
|
||||
centerX := (paddedImg.Bounds().Min.X + paddedImg.Bounds().Max.X) / 2
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -4,10 +4,14 @@ import (
|
|||
"bytes"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
|
@ -33,9 +37,10 @@ const (
|
|||
LinkPreviewThumbnailPath = "/link-preview/thumbnail"
|
||||
|
||||
// Handler routes for pairing
|
||||
accountImagesPath = "/accountImages"
|
||||
contactImagesPath = "/contactImages"
|
||||
generateQRCode = "/GenerateQRCode"
|
||||
accountImagesPath = "/accountImages"
|
||||
accountInitialsPath = "/accountInitials"
|
||||
contactImagesPath = "/contactImages"
|
||||
generateQRCode = "/GenerateQRCode"
|
||||
)
|
||||
|
||||
type HandlerPatternMap map[string]http.HandlerFunc
|
||||
|
@ -52,84 +57,514 @@ func handleRequestDownloaderMissing(logger *zap.Logger) http.HandlerFunc {
|
|||
}
|
||||
}
|
||||
|
||||
type ImageParams struct {
|
||||
KeyUID string
|
||||
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
|
||||
|
||||
AuthorID string
|
||||
URL string
|
||||
MessageID string
|
||||
AttachmentID string
|
||||
|
||||
Hash string
|
||||
Download bool
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
if parsed.ImageName == "" {
|
||||
logger.Error("handleAccountImagesImpl: no imageName")
|
||||
return
|
||||
}
|
||||
|
||||
identityImage, err := multiaccountsDB.GetIdentityImage(parsed.KeyUID, parsed.ImageName)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
accColorHash := account.ColorHash
|
||||
|
||||
if accColorHash == nil {
|
||||
if parsed.PublicKey == "" {
|
||||
logger.Error("handleAccountImagesImpl: no public key for color hash", zap.String("keyUid", parsed.KeyUID))
|
||||
return
|
||||
}
|
||||
|
||||
accColorHash, err = colorhash.GenerateFor(parsed.PublicKey)
|
||||
if err != nil {
|
||||
logger.Error("handleAccountImagesImpl: could not generate color hash", zap.String("keyUid", parsed.KeyUID), zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
payload, err = ring.DrawRing(&ring.DrawRingParam{
|
||||
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)
|
||||
|
||||
keyUids, ok := params["keyUid"]
|
||||
if !ok || len(keyUids) == 0 {
|
||||
logger.Error("no keyUid")
|
||||
return
|
||||
}
|
||||
imageNames, ok := params["imageName"]
|
||||
if !ok || len(imageNames) == 0 {
|
||||
logger.Error("no imageName")
|
||||
return
|
||||
}
|
||||
|
||||
identityImage, err := multiaccountsDB.GetIdentityImage(keyUids[0], imageNames[0])
|
||||
if err != nil {
|
||||
logger.Error("handleAccountImages: failed to load image.", zap.String("keyUid", keyUids[0]), zap.String("imageName", imageNames[0]), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
var payload = identityImage.Payload
|
||||
|
||||
if ringEnabled(params) {
|
||||
account, err := multiaccountsDB.GetAccount(keyUids[0])
|
||||
|
||||
if err != nil {
|
||||
logger.Error("handleAccountImages: failed to GetAccount .", zap.String("keyUid", keyUids[0]), zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
accColorHash := account.ColorHash
|
||||
|
||||
if accColorHash == nil {
|
||||
pks, ok := params["publicKey"]
|
||||
if !ok || len(pks) == 0 {
|
||||
logger.Error("no publicKey")
|
||||
return
|
||||
}
|
||||
|
||||
accColorHash, err = colorhash.GenerateFor(pks[0])
|
||||
if err != nil {
|
||||
logger.Error("could not generate color hash")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var theme = getTheme(params, logger)
|
||||
|
||||
payload, err = ring.DrawRing(&ring.DrawRingParam{
|
||||
Theme: theme, ColorHash: accColorHash, ImageBytes: identityImage.Payload, Height: identityImage.Height, Width: identityImage.Width,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
logger.Error("failed to draw ring for account identity", zap.Error(err))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if len(payload) == 0 {
|
||||
logger.Error("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("failed to write image", zap.Error(err))
|
||||
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 {
|
||||
logger.Error("failed to draw ring for account identity", 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("handleAccountInitialsImpl: 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))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if db == nil {
|
||||
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) {
|
||||
params := r.URL.Query()
|
||||
pks, ok := params["publicKey"]
|
||||
if !ok || len(pks) == 0 {
|
||||
parsed := ParseImageParams(logger, params)
|
||||
|
||||
if parsed.PublicKey == "" {
|
||||
logger.Error("no publicKey")
|
||||
return
|
||||
}
|
||||
imageNames, ok := params["imageName"]
|
||||
if !ok || len(imageNames) == 0 {
|
||||
|
||||
if parsed.ImageName == "" {
|
||||
logger.Error("no imageName")
|
||||
return
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
|
||||
if ringEnabled(params) {
|
||||
colorHash, err := colorhash.GenerateFor(pks[0])
|
||||
img, _, err := image.Decode(bytes.NewReader(payload))
|
||||
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 {
|
||||
logger.Error("could not generate color hash")
|
||||
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{
|
||||
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 {
|
||||
|
@ -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 {
|
||||
logger.Error("empty image")
|
||||
return
|
||||
|
@ -220,27 +675,27 @@ func getTheme(params url.Values, logger *zap.Logger) ring.Theme {
|
|||
func handleIdenticon(logger *zap.Logger) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
params := r.URL.Query()
|
||||
pks, ok := params["publicKey"]
|
||||
if !ok || len(pks) == 0 {
|
||||
parsed := ParseImageParams(logger, params)
|
||||
|
||||
if parsed.PublicKey == "" {
|
||||
logger.Error("no publicKey")
|
||||
return
|
||||
}
|
||||
pk := pks[0]
|
||||
image, err := identicon.Generate(pk)
|
||||
|
||||
identiconImage, err := identicon.Generate(parsed.PublicKey)
|
||||
if err != nil {
|
||||
logger.Error("could not generate identicon")
|
||||
}
|
||||
|
||||
if image != nil && ringEnabled(params) {
|
||||
colorHash, err := colorhash.GenerateFor(pk)
|
||||
if identiconImage != nil && parsed.Ring {
|
||||
colorHash, err := colorhash.GenerateFor(parsed.PublicKey)
|
||||
if err != nil {
|
||||
logger.Error("could not generate color hash")
|
||||
return
|
||||
}
|
||||
|
||||
theme := getTheme(params, logger)
|
||||
image, err = ring.DrawRing(&ring.DrawRingParam{
|
||||
Theme: theme, ColorHash: colorHash, ImageBytes: image, Height: identicon.Height, Width: identicon.Width,
|
||||
identiconImage, err = ring.DrawRing(&ring.DrawRingParam{
|
||||
Theme: parsed.Theme, ColorHash: colorHash, ImageBytes: identiconImage, Height: identicon.Height, Width: identicon.Width,
|
||||
})
|
||||
if err != nil {
|
||||
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("Expires", time.Now().AddDate(60, 0, 0).Format(http.TimeFormat))
|
||||
|
||||
_, err = w.Write(image)
|
||||
_, err = w.Write(identiconImage)
|
||||
if err != nil {
|
||||
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) {
|
||||
authorIDs, ok := r.URL.Query()["authorId"]
|
||||
if !ok || len(authorIDs) == 0 {
|
||||
params := r.URL.Query()
|
||||
parsed := ParseImageParams(logger, params)
|
||||
|
||||
if parsed.AuthorID == "" {
|
||||
logger.Error("no authorIDs")
|
||||
return
|
||||
}
|
||||
authorID := authorIDs[0]
|
||||
|
||||
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 {
|
||||
logger.Error("failed to find image", zap.Error(err))
|
||||
return
|
||||
|
@ -302,20 +758,20 @@ func handleDiscordAttachment(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
|
|||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
messageIDs, ok := r.URL.Query()["messageId"]
|
||||
if !ok || len(messageIDs) == 0 {
|
||||
params := r.URL.Query()
|
||||
parsed := ParseImageParams(logger, params)
|
||||
|
||||
if parsed.MessageID == "" {
|
||||
logger.Error("no messageID")
|
||||
return
|
||||
}
|
||||
attachmentIDs, ok := r.URL.Query()["attachmentId"]
|
||||
if !ok || len(attachmentIDs) == 0 {
|
||||
if parsed.AttachmentID == "" {
|
||||
logger.Error("no attachmentID")
|
||||
return
|
||||
}
|
||||
messageID := messageIDs[0]
|
||||
attachmentID := attachmentIDs[0]
|
||||
|
||||
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 {
|
||||
logger.Error("failed to find image", zap.Error(err))
|
||||
return
|
||||
|
@ -345,14 +801,16 @@ func handleImage(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
|
|||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
messageIDs, ok := r.URL.Query()["messageId"]
|
||||
if !ok || len(messageIDs) == 0 {
|
||||
params := r.URL.Query()
|
||||
parsed := ParseImageParams(logger, params)
|
||||
|
||||
if parsed.MessageID == "" {
|
||||
logger.Error("no messageID")
|
||||
return
|
||||
}
|
||||
messageID := messageIDs[0]
|
||||
|
||||
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 {
|
||||
logger.Error("failed to find image", zap.Error(err))
|
||||
return
|
||||
|
@ -382,14 +840,16 @@ func handleAudio(db *sql.DB, logger *zap.Logger) http.HandlerFunc {
|
|||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
messageIDs, ok := r.URL.Query()["messageId"]
|
||||
if !ok || len(messageIDs) == 0 {
|
||||
params := r.URL.Query()
|
||||
parsed := ParseImageParams(logger, params)
|
||||
|
||||
if parsed.MessageID == "" {
|
||||
logger.Error("no messageID")
|
||||
return
|
||||
}
|
||||
messageID := messageIDs[0]
|
||||
|
||||
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 {
|
||||
logger.Error("failed to find image", zap.Error(err))
|
||||
return
|
||||
|
@ -415,15 +875,15 @@ func handleIPFS(downloader *ipfs.Downloader, logger *zap.Logger) http.HandlerFun
|
|||
}
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
hashes, ok := r.URL.Query()["hash"]
|
||||
if !ok || len(hashes) == 0 {
|
||||
params := r.URL.Query()
|
||||
parsed := ParseImageParams(logger, params)
|
||||
|
||||
if parsed.Hash == "" {
|
||||
logger.Error("no hash")
|
||||
return
|
||||
}
|
||||
|
||||
_, download := r.URL.Query()["download"]
|
||||
|
||||
content, err := downloader.Get(hashes[0], download)
|
||||
content, err := downloader.Get(parsed.Hash, parsed.Download)
|
||||
if err != nil {
|
||||
logger.Error("could not download hash", zap.Error(err))
|
||||
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 {
|
||||
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 !ok || len(paramID) == 0 {
|
||||
if parsed.MessageID == "" {
|
||||
http.Error(w, "missing query parameter 'message-id'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
paramURL, ok := queryParams["url"]
|
||||
if !ok || len(paramURL) == 0 {
|
||||
if parsed.URL == "" {
|
||||
http.Error(w, "missing query parameter 'url'", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
msgID := paramID[0]
|
||||
thumbnailURL := paramURL[0]
|
||||
|
||||
thumbnail, err := getThumbnailPayload(db, logger, msgID, thumbnailURL)
|
||||
thumbnail, err := getThumbnailPayload(db, logger, parsed.MessageID, parsed.URL)
|
||||
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)
|
||||
return
|
||||
}
|
||||
|
|
|
@ -38,6 +38,7 @@ func NewMediaServer(db *sql.DB, downloader *ipfs.Downloader, multiaccountsDB *mu
|
|||
}
|
||||
s.SetHandlers(HandlerPatternMap{
|
||||
accountImagesPath: handleAccountImages(s.multiaccountsDB, s.logger),
|
||||
accountInitialsPath: handleAccountInitials(s.multiaccountsDB, s.logger),
|
||||
audioPath: handleAudio(s.db, s.logger),
|
||||
contactImagesPath: handleContactImages(s.db, s.logger),
|
||||
discordAttachmentsPath: handleDiscordAttachment(s.db, s.logger),
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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,
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
|
@ -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
|
||||
}
|
|
@ -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)
|
File diff suppressed because it is too large
Load Diff
|
@ -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) {}
|
|
@ -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
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1174,9 +1174,12 @@ golang.org/x/image/bmp
|
|||
golang.org/x/image/draw
|
||||
golang.org/x/image/font
|
||||
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/fixed
|
||||
golang.org/x/image/riff
|
||||
golang.org/x/image/vector
|
||||
golang.org/x/image/vp8
|
||||
golang.org/x/image/vp8l
|
||||
golang.org/x/image/webp
|
||||
|
|
Loading…
Reference in New Issue