The Canvas.TextWidth method replaced by Canvas.TextMetrics

This commit is contained in:
anoshenko 2022-10-30 12:35:22 +03:00
parent 8216ce192a
commit 76413c931a
6 changed files with 186 additions and 156 deletions

View File

@ -1,3 +1,7 @@
# v.10.0
* The Canvas.TextWidth method replaced by Canvas.TextMetrics
# v0.9.0
* Requires go 1.18 or higher

View File

@ -1702,3 +1702,44 @@ function imageError(element, event) {
var message = "imageViewError{session=" + sessionID + ",id=" + element.id + "}";
sendMessage(message);
}
function canvasTextMetrics(answerID, elementId, font, text) {
var w = 0;
var ascent = 0;
var descent = 0;
var left = 0;
var right = 0;
const canvas = document.getElementById(elementId);
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.save()
const dpr = window.devicePixelRatio || 1;
ctx.scale(dpr, dpr);
ctx.font = font;
ctx.textBaseline = 'alphabetic';
ctx.textAlign = 'start';
var metrics = ctx.measureText(text)
w = metrics.width;
ascent = metrics.actualBoundingBoxAscent;
descent = metrics.actualBoundingBoxDescent;
left = metrics.actualBoundingBoxLeft;
right = metrics.actualBoundingBoxRight;
ctx.restore();
}
}
sendMessage('answer{answerID=' + answerID + ', width=' + w + ', ascent=' + ascent +
', descent=' + descent + ', left=' + left + ', right=' + right + '}');
}
function getPropertyValue(answerID, elementId, name) {
const element = document.getElementById(elementId);
if (element && element[name]) {
sendMessage('answer{answerID=' + answerID + ', value="' + element[name] + '"}')
return
}
sendMessage('answer{answerID=' + answerID + ', value=""}')
}

134
canvas.go
View File

@ -76,6 +76,21 @@ type FontParams struct {
LineHeight SizeUnit
}
// TextMetrics is the result of the Canvas.TextMetrics function
type TextMetrics struct {
// Width is the calculated width of a segment of inline text in pixels
Width float64
// Ascent is the distance from the horizontal baseline to the top of the bounding rectangle used to render the text, in pixels.
Ascent float64
// Descent is the distance from the horizontal baseline to the bottom of the bounding rectangle used to render the text, in pixels.
Descent float64
// Left is the distance to the left side of the bounding rectangle of the given text, in pixels;
// positive numbers indicating a distance going left from the given alignment point.
Left float64
// Right is the distance to the right side of the bounding rectangle of the given text, CSS pixels.
Right float64
}
// Canvas is a drawing interface
type Canvas interface {
// View return the view for the drawing
@ -195,8 +210,8 @@ type Canvas interface {
// SetFontWithParams sets the current text style to use when drawing text
SetFontWithParams(name string, size SizeUnit, params FontParams)
// TextWidth calculates the width of the text drawn by a given font
TextWidth(text string, fontName string, fontSize SizeUnit) float64
// TextWidth calculates metrics of the text drawn by a given font
TextMetrics(text string, fontName string, fontSize SizeUnit, fontParams FontParams) TextMetrics
// SetTextBaseline sets the current text baseline used when drawing text. Valid values:
// AlphabeticBaseline (0), TopBaseline (1), MiddleBaseline (2), BottomBaseline (3),
@ -596,6 +611,67 @@ func (canvas *canvasData) SetFont(name string, size SizeUnit) {
canvas.writeFont(name, &canvas.script)
}
func (canvas *canvasData) fontWithParams(name string, size SizeUnit, params FontParams) string {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
if params.Italic {
buffer.WriteString("italic ")
}
if params.SmallCaps {
buffer.WriteString("small-caps ")
}
if params.Weight > 0 && params.Weight <= 9 {
switch params.Weight {
case 4:
buffer.WriteString("normal ")
case 7:
buffer.WriteString("bold ")
default:
buffer.WriteString(strconv.Itoa(params.Weight * 100))
buffer.WriteRune(' ')
}
}
buffer.WriteString(size.cssString("1rem", canvas.View().Session()))
switch params.LineHeight.Type {
case Auto:
case SizeInPercent:
if params.LineHeight.Value != 100 {
buffer.WriteString("/")
buffer.WriteString(strconv.FormatFloat(params.LineHeight.Value/100, 'g', -1, 64))
}
case SizeInFraction:
if params.LineHeight.Value != 1 {
buffer.WriteString("/")
buffer.WriteString(strconv.FormatFloat(params.LineHeight.Value, 'g', -1, 64))
}
default:
buffer.WriteString("/")
buffer.WriteString(params.LineHeight.cssString("", canvas.View().Session()))
}
names := strings.Split(name, ",")
lead := " "
for _, font := range names {
font = strings.Trim(font, " \n\"'")
buffer.WriteString(lead)
lead = ","
if strings.Contains(font, " ") {
buffer.WriteRune('"')
buffer.WriteString(font)
buffer.WriteRune('"')
} else {
buffer.WriteString(font)
}
}
return buffer.String()
}
func (canvas *canvasData) setFontWithParams(name string, size SizeUnit, params FontParams, script *strings.Builder) {
script.WriteString("\nctx.font = '")
if params.Italic {
@ -644,57 +720,9 @@ func (canvas *canvasData) SetFontWithParams(name string, size SizeUnit, params F
canvas.setFontWithParams(name, size, params, &canvas.script)
}
func (canvas *canvasData) TextWidth(text string, fontName string, fontSize SizeUnit) float64 {
buffer := allocStringBuilder()
defer freeStringBuilder(buffer)
canvas.setFontWithParams(fontName, fontSize, FontParams{}, buffer)
fontParams := buffer.String()
buffer.Reset()
canvas.writeStringArgs(text, buffer)
str := buffer.String()
script := fmt.Sprintf(`
var w = 0;
const canvas = document.getElementById('%s');
if (canvas) {
const ctx = canvas.getContext('2d');
if (ctx) {
ctx.save()
const dpr = window.devicePixelRatio || 1;
ctx.scale(dpr, dpr);
%s;
w = ctx.measureText('%s').width;
ctx.restore();
}
}
sendMessage('answer{width=' + w + ', answerID=' + answerID + '}');
`, canvas.View().htmlID(), fontParams, str)
result := canvas.View().Session().runGetterScript(script)
switch result.Tag() {
case "answer":
if value, ok := result.PropertyValue("width"); ok {
w, err := strconv.ParseFloat(value, 32)
if err == nil {
return w
}
ErrorLog(err.Error())
}
case "error":
if text, ok := result.PropertyValue("errorText"); ok {
ErrorLog(text)
} else {
ErrorLog("error")
}
default:
ErrorLog("Unknown answer: " + result.Tag())
}
return 0
func (canvas *canvasData) TextMetrics(text string, fontName string, fontSize SizeUnit, fontParams FontParams) TextMetrics {
view := canvas.View()
return view.Session().canvasTextMetrics(view.htmlID(), canvas.fontWithParams(fontName, fontSize, fontParams), text)
}
func (canvas *canvasData) SetTextBaseline(baseline int) {

View File

@ -667,48 +667,15 @@ func (player *mediaPlayerData) SetCurrentTime(seconds float64) {
}
func (player *mediaPlayerData) getFloatPlayerProperty(tag string) (float64, bool) {
script := allocStringBuilder()
defer freeStringBuilder(script)
script.WriteString(`const element = document.getElementById('`)
script.WriteString(player.htmlID())
script.WriteString(`');
if (element && element.`)
script.WriteString(tag)
script.WriteString(`) {
sendMessage('answer{answerID=' + answerID + ',`)
script.WriteString(tag)
script.WriteString(`=' + element.`)
script.WriteString(tag)
script.WriteString(` + '}');
} else {
sendMessage('answer{answerID=' + answerID + ',`)
script.WriteString(tag)
script.WriteString(`=0}');
}`)
result := player.Session().runGetterScript(script.String())
switch result.Tag() {
case "answer":
if value, ok := result.PropertyValue(tag); ok {
w, err := strconv.ParseFloat(value, 32)
if err == nil {
return w, true
}
ErrorLog(err.Error())
value := player.Session().htmlPropertyValue(player.htmlID(), tag)
if value != "" {
result, err := strconv.ParseFloat(value, 32)
if err == nil {
return result, true
}
case "error":
if text, ok := result.PropertyValue("errorText"); ok {
ErrorLog(text)
} else {
ErrorLog("error")
}
default:
ErrorLog("Unknown answer: " + result.Tag())
ErrorLog(err.Error())
}
return 0, false
}
@ -751,45 +718,14 @@ func (player *mediaPlayerData) Volume() float64 {
}
func (player *mediaPlayerData) getBoolPlayerProperty(tag string) (bool, bool) {
switch value := player.Session().htmlPropertyValue(player.htmlID(), tag); strings.ToLower(value) {
case "0", "false", "off":
return false, true
script := allocStringBuilder()
defer freeStringBuilder(script)
script.WriteString(`const element = document.getElementById('`)
script.WriteString(player.htmlID())
script.WriteString(`');
if (element && element.`)
script.WriteString(tag)
script.WriteString(`) {
sendMessage('answer{answerID=' + answerID + ',`)
script.WriteString(tag)
script.WriteString(`=1}')
} else {
sendMessage('answer{answerID=' + answerID + ',`)
script.WriteString(tag)
script.WriteString(`=0}')
}`)
result := player.Session().runGetterScript(script.String())
switch result.Tag() {
case "answer":
if value, ok := result.PropertyValue(tag); ok {
if value == "1" {
return true, true
}
return false, true
}
case "error":
if text, ok := result.PropertyValue("errorText"); ok {
ErrorLog(text)
} else {
ErrorLog("error")
}
default:
ErrorLog("Unknown answer: " + result.Tag())
case "1", "true", "on":
return false, true
}
return false, false
}

View File

@ -11,7 +11,8 @@ type webBrige interface {
runFunc(funcName string, args ...any) bool
readMessage() (string, bool)
writeMessage(text string) bool
runGetterScript(script string) DataObject
canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string
answerReceived(answer DataObject)
close()
remoteAddr() string
@ -99,7 +100,8 @@ type Session interface {
writeInitScript(writer *strings.Builder)
runFunc(funcName string, args ...any)
runScript(script string)
runGetterScript(script string) DataObject //, answer chan DataObject)
canvasTextMetrics(htmlID, font, text string) TextMetrics
htmlPropertyValue(htmlID, name string) string
handleAnswer(data DataObject)
handleRootSize(data DataObject)
handleResize(data DataObject)
@ -339,15 +341,22 @@ func (session *sessionData) runScript(script string) {
}
}
func (session *sessionData) runGetterScript(script string) DataObject { //}, answer chan DataObject) {
func (session *sessionData) canvasTextMetrics(htmlID, font, text string) TextMetrics {
if session.brige != nil {
return session.brige.runGetterScript(script)
return session.brige.canvasTextMetrics(htmlID, font, text)
}
ErrorLog("No connection")
result := NewDataObject("error")
result.SetPropertyValue("text", "No connection")
return result
return TextMetrics{Width: 0}
}
func (session *sessionData) htmlPropertyValue(htmlID, name string) string {
if session.brige != nil {
return session.brige.htmlPropertyValue(htmlID, name)
}
ErrorLog("No connection")
return ""
}
func (session *sessionData) handleAnswer(data DataObject) {

View File

@ -151,6 +151,10 @@ func (brige *wsBrige) writeMessage(script string) bool {
DebugLog("Run script:")
DebugLog(script)
}
if brige.conn == nil {
ErrorLog("No connection")
return false
}
if err := brige.conn.WriteMessage(websocket.TextMessage, []byte(script)); err != nil {
ErrorLog(err.Error())
return false
@ -158,7 +162,9 @@ func (brige *wsBrige) writeMessage(script string) bool {
return true
}
func (brige *wsBrige) runGetterScript(script string) DataObject {
func (brige *wsBrige) canvasTextMetrics(htmlID, font, text string) TextMetrics {
result := TextMetrics{}
brige.answerMutex.Lock()
answerID := brige.answerID
brige.answerID++
@ -166,30 +172,36 @@ func (brige *wsBrige) runGetterScript(script string) DataObject {
answer := make(chan DataObject)
brige.answer[answerID] = answer
errorText := ""
if brige.conn != nil {
script = "var answerID = " + strconv.Itoa(answerID) + ";\n" + script
if ProtocolInDebugLog {
DebugLog("\n" + script)
}
err := brige.conn.WriteMessage(websocket.TextMessage, []byte(script))
if err == nil {
return <-answer
}
errorText = err.Error()
} else {
if ProtocolInDebugLog {
DebugLog("\n" + script)
}
errorText = "No connection"
if brige.runFunc("canvasTextMetrics", answerID, htmlID, font, text) {
data := <-answer
result.Width = dataFloatProperty(data, "width")
}
result := NewDataObject("error")
result.SetPropertyValue("text", errorText)
delete(brige.answer, answerID)
return result
}
func (brige *wsBrige) htmlPropertyValue(htmlID, name string) string {
brige.answerMutex.Lock()
answerID := brige.answerID
brige.answerID++
brige.answerMutex.Unlock()
answer := make(chan DataObject)
brige.answer[answerID] = answer
if brige.runFunc("getPropertyValue", answerID, htmlID, name) {
data := <-answer
if value, ok := data.PropertyValue("value"); ok {
return value
}
}
delete(brige.answer, answerID)
return ""
}
func (brige *wsBrige) answerReceived(answer DataObject) {
if text, ok := answer.PropertyValue("answerID"); ok {
if id, err := strconv.Atoi(text); err == nil {