")
+ tx.Styler(func(s *styles.Style) {
+ s.Max.X.In(8) // big enough to not constrain PDF render
+ s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
+ })
}
// citeWikilink processes citation links, which start with @
diff --git a/htmlcore/handler.go b/htmlcore/handler.go
index 8fc92e6f1c..4c56d6e7a9 100644
--- a/htmlcore/handler.go
+++ b/htmlcore/handler.go
@@ -19,7 +19,6 @@ import (
"cogentcore.org/core/core"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/states"
- "cogentcore.org/core/styles/units"
"cogentcore.org/core/text/lines"
"cogentcore.org/core/text/rich"
"cogentcore.org/core/text/text"
@@ -213,6 +212,7 @@ func handleElement(ctx *Context) {
ctx.Node = ctx.Node.FirstChild.NextSibling
}
text, sublist := handleTextExclude(ctx, "ol", "ul") // exclude other lists
+ newWidget = text
start := ""
if pw, ok := text.Parent.(core.Widget); ok {
pwt := pw.AsTree()
@@ -243,6 +243,7 @@ func handleElement(ctx *Context) {
}
txt, psub := handleTextExclude(ctx, "ol", "ul")
txt.SetText(txt.Text)
+ ctx.handleWidget(cnode, txt)
if psub != nil {
if psub != sublist {
readHTMLNode(ctx, ctx.Parent(), psub)
@@ -397,17 +398,6 @@ func handleElement(ctx *Context) {
}
}
-func (ctx *Context) textStyler(s *styles.Style) {
- s.Margin.SetVertical(units.Em(core.ConstantSpacing(0.25)))
- s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
- // TODO: it would be ideal for htmlcore to automatically save a scale factor
- // in general and for each domain, that is applied only to page content
- // scale := float32(1.2)
- // s.Font.Size.Value *= scale
- // s.Text.LineHeight.Value *= scale
- // s.Text.LetterSpacing.Value *= scale
-}
-
// handleText creates a new [core.Text] from the given information, setting the text and
// the text click function so that URLs are opened according to [Context.OpenURL].
func handleText(ctx *Context, tag string) *core.Text {
@@ -426,7 +416,6 @@ func handleTextExclude(ctx *Context, excludeSubs ...string) (*core.Text, *html.N
return core.NewText(), excl
}
tx := New[core.Text](ctx).SetText(et)
- tx.Styler(ctx.textStyler)
tx.HandleTextClick(func(tl *rich.Hyperlink) {
ctx.OpenURL(tl.URL)
})
@@ -442,7 +431,6 @@ func handleTextTag(ctx *Context) *core.Text {
start, end := nodeString(ctx.Node)
str := start + ExtractText(ctx) + end
tx := New[core.Text](ctx).SetText(str)
- tx.Styler(ctx.textStyler)
tx.HandleTextClick(func(tl *rich.Hyperlink) {
ctx.OpenURL(tl.URL)
})
From a031688a8ac777c309115de51b1fae69d4e55592 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 7 Oct 2025 21:52:48 +0200
Subject: [PATCH 32/99] pdf: move pre handler with BindTextEditor into content
-- did not belong in htmlcore, which is just the basic handler without any of
the fancy interactive stuff. moved all the pre elements into a frame, so pdf
render is blocked. neuron-channels page is _amazing_ with all the plots etc.
---
content/content.go | 1 +
content/handlers.go | 191 +++++++++++++-----
htmlcore/context.go | 1 +
htmlcore/handler.go | 92 ++-------
htmlcore/html.go | 2 +-
.../cogentcore_org-core-content.go | 6 +-
.../cogentcore_org-core-htmlcore.go | 2 +-
yaegicore/yaegicore.go | 5 +-
8 files changed, 177 insertions(+), 123 deletions(-)
diff --git a/content/content.go b/content/content.go
index 41ca11306f..edc4bc1eb4 100644
--- a/content/content.go
+++ b/content/content.go
@@ -141,6 +141,7 @@ func (ct *Content) Init() {
errors.Log(ct.embedPage(ctx))
return true
}
+ ct.Context.ElementHandlers["pre"] = ct.htmlPreHandler
ct.Context.AttributeHandlers["id"] = ct.htmlIDAttributeHandler
ct.Context.AddWidgetHandler(ct.widgetHandler)
diff --git a/content/handlers.go b/content/handlers.go
index 7688613d3e..0f94d54ad0 100644
--- a/content/handlers.go
+++ b/content/handlers.go
@@ -12,6 +12,7 @@ import (
"cogentcore.org/core/base/errors"
"cogentcore.org/core/base/labels"
"cogentcore.org/core/base/strcase"
+ "cogentcore.org/core/colors"
"cogentcore.org/core/content/bcontent"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
@@ -22,31 +23,31 @@ import (
"cogentcore.org/core/styles/states"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/text/csl"
+ "cogentcore.org/core/text/textcore"
"cogentcore.org/core/tree"
"github.com/gomarkdown/markdown/ast"
)
-// handles the id attribute in htmlcore -- deals with all non-image id cases
+// BindTextEditor is a function set to [cogentcore.org/core/yaegicore.BindTextEditor]
+// when importing yaegicore, which provides interactive editing functionality for Go
+// code blocks in text editors.
+var BindTextEditor func(ed *textcore.Editor, parent *core.Frame, language string)
+
+// * add after-first-line indent (what is official name?) to text style and implement it -- need for li etc
+// * paginate uses left margins
+// * not able to click off shading of figures etc when clicking link
+
+// handles the id attribute in htmlcore: needed for equation case
func (ct *Content) htmlIDAttributeHandler(ctx *htmlcore.Context, w io.Writer, node ast.Node, entering bool, tag, value string) bool {
if ct.currentPage == nil {
return false
}
lbl := ct.currentPage.SpecialLabel(value)
+ if lbl == "" {
+ return false
+ }
ch := node.GetChildren()
if len(ch) == 2 { // image or table
- if entering {
- return false
- }
- if _, ok := ch[1].(*ast.Image); ok {
- return false
- }
- cp := "\n
" + lbl + ":"
- title := htmlcore.MDGetAttr(node, "title")
- if title != "" {
- cp += " " + title
- }
- cp += "
\n"
- w.Write([]byte(cp))
return false
}
if entering {
@@ -63,16 +64,97 @@ func (ct *Content) htmlIDAttributeHandler(ctx *htmlcore.Context, w io.Writer, no
return false
}
+func (ct *Content) htmlPreHandler(ctx *htmlcore.Context) bool {
+ hasCode := ctx.Node.FirstChild != nil && ctx.Node.FirstChild.Data == "code"
+ if !hasCode {
+ return false
+ }
+ codeEl := ctx.Node.FirstChild
+ collapsed := htmlcore.GetAttr(codeEl, "collapsed")
+ lang := htmlcore.GetLanguage(htmlcore.GetAttr(codeEl, "class"))
+ id := htmlcore.GetAttr(codeEl, "id")
+ title := htmlcore.GetAttr(codeEl, "title")
+ var ed *textcore.Editor
+ parent := ctx.Parent().AsWidget()
+ fr := core.NewFrame(parent.This)
+ fr.Styler(func(s *styles.Style) {
+ s.Grow.Set(1, 0)
+ s.Direction = styles.Column
+ })
+ fr.SetProperty("paginate-block", true) // no split
+ if id != "" {
+ fr.SetProperty("id", id)
+ fr.SetName(id)
+ tree.MoveToParent(parent.Children[parent.NumChildren()-2], fr) // get title text
+ }
+ if collapsed != "" {
+ cl := core.NewCollapser(fr)
+ core.NewText(cl.Summary).SetText("Code").SetText(title)
+ ed = textcore.NewEditor(cl.Details)
+ if collapsed == "false" || collapsed == "-" {
+ cl.Open = true
+ }
+ } else {
+ ed = textcore.NewEditor(fr)
+ }
+ ctx.Node = codeEl
+ if lang != "" {
+ ed.Lines.SetFileExt(lang)
+ }
+ ed.Lines.SetString(htmlcore.ExtractText(ctx))
+ if BindTextEditor != nil && (lang == "Go" || lang == "Goal") {
+ ed.Lines.SpacesToTabs(0, ed.Lines.NumLines()) // Go uses tabs
+ parFrame := core.NewFrame(fr)
+ parFrame.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ s.Grow.Set(1, 0)
+ })
+ // we inherit our Grow.Y from our first child so that
+ // elements that want to grow can do so
+ parFrame.SetOnChildAdded(func(n tree.Node) {
+ if _, ok := n.(*core.Body); ok { // Body should not grow
+ return
+ }
+ wb := core.AsWidget(n)
+ if wb.IndexInParent() != 0 {
+ return
+ }
+ wb.FinalStyler(func(s *styles.Style) {
+ parFrame.Styles.Grow.Y = s.Grow.Y
+ })
+ })
+ BindTextEditor(ed, parFrame, lang)
+ } else {
+ ed.SetReadOnly(true)
+ ed.Lines.Settings.LineNumbers = false
+ ed.Styler(func(s *styles.Style) {
+ s.Border.Width.Zero()
+ s.MaxBorder.Width.Zero()
+ s.StateLayer = 0
+ s.Background = colors.Scheme.SurfaceContainer
+ })
+ }
+ return true
+}
+
// widgetHandler is htmlcore widget handler for adding our own actions etc.
func (ct *Content) widgetHandler(w core.Widget) {
+ tag := ""
+ id := ""
+ title := ""
+ wb := w.AsWidget()
+ if t, ok := wb.Properties["tag"]; ok {
+ tag = t.(string)
+ }
+ if t, ok := wb.Properties["id"]; ok {
+ id = t.(string)
+ }
+ if t, ok := wb.Properties["title"]; ok {
+ title = t.(string)
+ }
switch x := w.(type) {
case *core.Text:
- hdr := false
- if t, ok := x.Properties["tag"]; ok {
- if len(t.(string)) > 0 && (t.(string))[0] == 'h' {
- hdr = true
- }
- }
+ hdr := len(tag) > 0 && tag[0] == 'h'
x.Styler(func(s *styles.Style) {
s.Margin.SetVertical(units.Em(core.ConstantSpacing(0.25)))
s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
@@ -82,14 +164,14 @@ func (ct *Content) widgetHandler(w core.Widget) {
}
})
case *core.Image:
- ct.widgetHandlerFigure(w)
+ ct.widgetHandlerFigure(w, id)
x.OnDoubleClick(func(e events.Event) {
d := core.NewBody("Image")
core.NewImage(d).SetImage(x.Image)
d.RunWindowDialog(x)
})
case *core.SVG:
- ct.widgetHandlerFigure(w)
+ ct.widgetHandlerFigure(w, id)
x.OnDoubleClick(func(e events.Event) {
d := core.NewBody("SVG")
sv := core.NewSVG(d)
@@ -97,29 +179,57 @@ func (ct *Content) widgetHandler(w core.Widget) {
d.RunWindowDialog(x)
})
case *core.Frame:
- table := false
- if t, ok := x.Properties["tag"]; ok {
- table = (t.(string) == "table")
- }
- if table {
+ switch tag {
+ case "table":
x.Styler(func(s *styles.Style) {
s.Align.Self = styles.Center
})
+ if id == "" {
+ break
+ }
+ lbl := ct.currentPage.SpecialLabel(id)
+ cp := "" + lbl + ":"
+ if title != "" {
+ cp += " " + title
+ }
+ ct.moveToBlockFrame(w, id, cp, true)
}
}
}
-func (ct *Content) widgetHandlerFigure(w core.Widget) {
+// moveToBlockFrame moves given widget into a block frame with given text
+// widget either at the top or bottom of the new frame.
+func (ct *Content) moveToBlockFrame(w core.Widget, id, txt string, top bool) {
+ wb := w.AsWidget()
+ fr := core.NewFrame(wb.Parent)
+ fr.Styler(func(s *styles.Style) {
+ s.Grow.Set(1, 0)
+ s.Direction = styles.Column
+ })
+ fr.SetProperty("paginate-block", true) // no split
+ fr.SetProperty("id", id)
+ fr.SetName(id)
+ var tx *core.Text
+ if top {
+ tx = core.NewText(fr).SetText(txt)
+ }
+ tree.MoveToParent(w, fr)
+ if !top {
+ tx = core.NewText(fr).SetText(txt)
+ }
+ tx.Styler(func(s *styles.Style) {
+ s.Max.X.In(8)
+ s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
+ })
+}
+
+func (ct *Content) widgetHandlerFigure(w core.Widget, id string) {
wb := w.AsWidget()
fig := false
alt := ""
- id := ""
if p, ok := wb.Properties["alt"]; ok {
alt = p.(string)
}
- if p, ok := wb.Properties["id"]; ok {
- id = p.(string)
- }
if alt != "" && id != "" {
fig = true
}
@@ -133,22 +243,9 @@ func (ct *Content) widgetHandlerFigure(w core.Widget) {
if !fig {
return
}
- fr := core.NewFrame(wb.Parent)
- fr.Styler(func(s *styles.Style) {
- s.Grow.Set(1, 0)
- s.Direction = styles.Column
- })
- fr.SetProperty("paginate-block", true) // no split
- fr.SetProperty("id", id)
- tree.MoveToParent(w, fr)
- fr.SetName(id)
lbl := ct.currentPage.SpecialLabel(id)
- lbf := "" + lbl + ": "
- tx := core.NewText(fr).SetText(lbf + alt + "
")
- tx.Styler(func(s *styles.Style) {
- s.Max.X.In(8) // big enough to not constrain PDF render
- s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
- })
+ lbf := "" + lbl + ": " + alt + "
"
+ ct.moveToBlockFrame(w, id, lbf, false)
}
// citeWikilink processes citation links, which start with @
diff --git a/htmlcore/context.go b/htmlcore/context.go
index 8b73b81abb..f1ed184cbf 100644
--- a/htmlcore/context.go
+++ b/htmlcore/context.go
@@ -146,6 +146,7 @@ func (c *Context) config(w core.Widget) {
switch attr.Key {
case "id":
wb.SetName(attr.Val)
+ wb.SetProperty("id", attr.Val)
case "style":
c.setStyleAttr(c.Node, attr.Val)
default:
diff --git a/htmlcore/handler.go b/htmlcore/handler.go
index 4c56d6e7a9..bb819f9f56 100644
--- a/htmlcore/handler.go
+++ b/htmlcore/handler.go
@@ -53,6 +53,12 @@ func handleElement(ctx *Context) {
return
}
+ pid := ""
+ pstyle := ""
+ if ctx.BlockParent != nil { // these attributes get put on a block parent element
+ pstyle = GetAttr(ctx.Node.Parent, "style")
+ pid = GetAttr(ctx.Node.Parent, "id")
+ }
var newWidget core.Widget
switch tag {
@@ -130,31 +136,12 @@ func handleElement(ctx *Context) {
hasCode := ctx.Node.FirstChild != nil && ctx.Node.FirstChild.Data == "code"
if hasCode {
codeEl := ctx.Node.FirstChild
- collapsed := GetAttr(codeEl, "collapsed")
- lang := getLanguage(GetAttr(codeEl, "class"))
+ lang := GetLanguage(GetAttr(codeEl, "class"))
id := GetAttr(codeEl, "id")
var ed *textcore.Editor
- var parent tree.Node
- if collapsed != "" {
- cl := New[core.Collapser](ctx)
- summary := core.NewText(cl.Summary).SetText("Code")
- if title := GetAttr(codeEl, "title"); title != "" {
- summary.SetText(title)
- }
- ed = textcore.NewEditor(cl.Details)
- if id != "" {
- cl.Summary.Name = id
- }
- parent = cl.Parent
- if collapsed == "false" || collapsed == "-" {
- cl.Open = true
- }
- } else {
- ed = New[textcore.Editor](ctx)
- if id != "" {
- ed.SetName(id)
- }
- parent = ed.Parent
+ ed = New[textcore.Editor](ctx)
+ if id != "" {
+ ed.SetName(id)
}
newWidget = ed
ctx.Node = codeEl
@@ -162,38 +149,14 @@ func handleElement(ctx *Context) {
ed.Lines.SetFileExt(lang)
}
ed.Lines.SetString(ExtractText(ctx))
- if BindTextEditor != nil && (lang == "Go" || lang == "Goal") {
- ed.Lines.SpacesToTabs(0, ed.Lines.NumLines()) // Go uses tabs
- parFrame := core.NewFrame(parent)
- parFrame.Styler(func(s *styles.Style) {
- s.Direction = styles.Column
- s.Grow.Set(1, 0)
- })
- // we inherit our Grow.Y from our first child so that
- // elements that want to grow can do so
- parFrame.SetOnChildAdded(func(n tree.Node) {
- if _, ok := n.(*core.Body); ok { // Body should not grow
- return
- }
- wb := core.AsWidget(n)
- if wb.IndexInParent() != 0 {
- return
- }
- wb.FinalStyler(func(s *styles.Style) {
- parFrame.Styles.Grow.Y = s.Grow.Y
- })
- })
- BindTextEditor(ed, parFrame, lang)
- } else {
- ed.SetReadOnly(true)
- ed.Lines.Settings.LineNumbers = false
- ed.Styler(func(s *styles.Style) {
- s.Border.Width.Zero()
- s.MaxBorder.Width.Zero()
- s.StateLayer = 0
- s.Background = colors.Scheme.SurfaceContainer
- })
- }
+ ed.SetReadOnly(true)
+ ed.Lines.Settings.LineNumbers = false
+ ed.Styler(func(s *styles.Style) {
+ s.Border.Width.Zero()
+ s.MaxBorder.Width.Zero()
+ s.StateLayer = 0
+ s.Background = colors.Scheme.SurfaceContainer
+ })
} else {
newWidget = handleText(ctx, tag)
newWidget.AsWidget().Styler(func(s *styles.Style) {
@@ -259,14 +222,8 @@ func handleElement(ctx *Context) {
n := ctx.Node
src := GetAttr(n, "src")
alt := GetAttr(n, "alt")
- style := ""
- pid := ""
- if ctx.BlockParent != nil { // these attributes get put on a block parent element
- style = GetAttr(n.Parent, "style")
- pid = GetAttr(n.Parent, "id")
- }
- if style != "" {
- ctx.setStyleAttr(n, style)
+ if pstyle != "" {
+ ctx.setStyleAttr(n, pstyle)
}
// Can be either image or svg.
var img *core.Image
@@ -473,9 +430,9 @@ func HasAttr(n *html.Node, attr string) bool {
})
}
-// getLanguage returns the 'x' in a `language-x` class from the given
+// GetLanguage returns the 'x' in a `language-x` class from the given
// string of class(es).
-func getLanguage(class string) string {
+func GetLanguage(class string) string {
fields := strings.Fields(class)
for _, field := range fields {
if strings.HasPrefix(field, "language-") {
@@ -505,8 +462,3 @@ func Get(ctx *Context, url string) (*http.Response, error) {
}
return resp, nil
}
-
-// BindTextEditor is a function set to [cogentcore.org/core/yaegicore.BindTextEditor]
-// when importing yaegicore, which provides interactive editing functionality for Go
-// code blocks in text editors.
-var BindTextEditor func(ed *textcore.Editor, parent *core.Frame, language string)
diff --git a/htmlcore/html.go b/htmlcore/html.go
index 08d70405f9..6ec8681a25 100644
--- a/htmlcore/html.go
+++ b/htmlcore/html.go
@@ -46,7 +46,7 @@ func readHTMLNode(ctx *Context, parent core.Widget, n *html.Node) error {
case html.TextNode:
str := strings.TrimSpace(n.Data)
if str != "" {
- New[core.Text](ctx).SetText(str)
+ ctx.handleWidget(n, New[core.Text](ctx).SetText(str))
}
case html.ElementNode:
ctx.Node = n
diff --git a/yaegicore/coresymbols/cogentcore_org-core-content.go b/yaegicore/coresymbols/cogentcore_org-core-content.go
index ccd1ffadae..197406c9c1 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-content.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-content.go
@@ -10,10 +10,14 @@ import (
func init() {
Symbols["cogentcore.org/core/content/content"] = map[string]reflect.Value{
// function, constant and variable definitions
+ "BindTextEditor": reflect.ValueOf(&content.BindTextEditor).Elem(),
"NewContent": reflect.ValueOf(content.NewContent),
"NewPageInitFunc": reflect.ValueOf(&content.NewPageInitFunc).Elem(),
+ "OfflineURL": reflect.ValueOf(&content.OfflineURL).Elem(),
+ "Settings": reflect.ValueOf(&content.Settings).Elem(),
// type definitions
- "Content": reflect.ValueOf((*content.Content)(nil)),
+ "Content": reflect.ValueOf((*content.Content)(nil)),
+ "SettingsData": reflect.ValueOf((*content.SettingsData)(nil)),
}
}
diff --git a/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go b/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
index e4cbe9d2c8..7fd2b2a36f 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
@@ -10,11 +10,11 @@ import (
func init() {
Symbols["cogentcore.org/core/htmlcore/htmlcore"] = map[string]reflect.Value{
// function, constant and variable definitions
- "BindTextEditor": reflect.ValueOf(&htmlcore.BindTextEditor).Elem(),
"ExtractText": reflect.ValueOf(htmlcore.ExtractText),
"ExtractTextExclude": reflect.ValueOf(htmlcore.ExtractTextExclude),
"Get": reflect.ValueOf(htmlcore.Get),
"GetAttr": reflect.ValueOf(htmlcore.GetAttr),
+ "GetLanguage": reflect.ValueOf(htmlcore.GetLanguage),
"GetURLFromFS": reflect.ValueOf(htmlcore.GetURLFromFS),
"GoDocWikilink": reflect.ValueOf(htmlcore.GoDocWikilink),
"HasAttr": reflect.ValueOf(htmlcore.HasAttr),
diff --git a/yaegicore/yaegicore.go b/yaegicore/yaegicore.go
index 05f57b8188..974a930e42 100644
--- a/yaegicore/yaegicore.go
+++ b/yaegicore/yaegicore.go
@@ -17,7 +17,6 @@ import (
"cogentcore.org/core/content"
"cogentcore.org/core/core"
"cogentcore.org/core/events"
- "cogentcore.org/core/htmlcore"
"cogentcore.org/core/text/textcore"
"cogentcore.org/core/yaegicore/basesymbols"
"cogentcore.org/core/yaegicore/coresymbols"
@@ -51,7 +50,7 @@ type Interpreter interface {
}
func init() {
- htmlcore.BindTextEditor = BindTextEditor
+ content.BindTextEditor = BindTextEditor
content.NewPageInitFunc = ResetGoalInterpreter
coresymbols.Symbols["."] = map[string]reflect.Value{} // make "." available for use
basesymbols.Symbols["."] = map[string]reflect.Value{} // make "." available for use
@@ -103,7 +102,7 @@ func getInterpreter(language string) (in Interpreter, new bool, err error) {
// BindTextEditor binds the given text editor to a yaegi interpreter
// such that the contents of the text editor are interpreted as code
// of the given language, which is run in the context of the given parent widget.
-// It is used as the default value of [htmlcore.BindTextEditor].
+// It is used as the default value of [content.BindTextEditor].
func BindTextEditor(ed *textcore.Editor, parent *core.Frame, language string) {
oc := func() {
in, new, err := getInterpreter(language)
From 10c5e18164e3db7ee50b39924ae715723f47e9ea Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 7 Oct 2025 22:39:36 +0200
Subject: [PATCH 33/99] pdf: gap and left padding layout preserved in paginate
---
content/buttons.go | 2 +-
content/content.go | 1 +
content/examples/basic/content/button.md | 21 +++++++++++
htmlcore/handler.go | 2 +-
text/paginate/extract.go | 4 +-
text/paginate/layout.go | 47 +++++++++++++++++++-----
text/paginate/page.go | 15 ++++----
7 files changed, 71 insertions(+), 21 deletions(-)
diff --git a/content/buttons.go b/content/buttons.go
index 420ae90234..d751d6649f 100644
--- a/content/buttons.go
+++ b/content/buttons.go
@@ -77,7 +77,7 @@ func (ct *Content) MakeToolbar(p *tree.Plan) {
tree.Add(p, func(w *core.Button) {
w.SetText("PDF").SetIcon(icons.PictureAsPdf)
w.OnClick(func(e events.Event) {
- ct.PagePDF("")
+ ct.PagePDF("pdfs")
})
})
}
diff --git a/content/content.go b/content/content.go
index edc4bc1eb4..88404eabc0 100644
--- a/content/content.go
+++ b/content/content.go
@@ -489,6 +489,7 @@ func (ct *Content) PagePDF(path string) error {
fname := ct.currentPage.Name + ".pdf"
if path != "" {
+ os.MkdirAll(path, 0777)
fname = filepath.Join(path, fname)
}
f, err := os.Create(fname)
diff --git a/content/examples/basic/content/button.md b/content/examples/basic/content/button.md
index c8bed34f37..90c21f54d0 100644
--- a/content/examples/basic/content/button.md
+++ b/content/examples/basic/content/button.md
@@ -99,3 +99,24 @@ Action and menu buttons are the most minimal buttons, and they are typically onl
```Go
core.NewButton(b).SetType(core.ButtonAction).SetText("Action")
```
+
+* List item 1
+ * Sub list item 1
+ * Sub list item 2
+* List item 2
+ * Sub list item 1
+ * Sub list item 2
+* List item 3
+ * Sub list item 1
+ * Sub list item 2
+
+1. List item 1
+ 1. Sub list item 1
+ 2. Sub list item 2
+2. List item 2
+ 1. Sub list item 1
+ 2. Sub list item 2
+3. List item 3
+ 1. Sub list item 1
+ 2. Sub list item 2
+
diff --git a/htmlcore/handler.go b/htmlcore/handler.go
index bb819f9f56..fdeebf8536 100644
--- a/htmlcore/handler.go
+++ b/htmlcore/handler.go
@@ -101,7 +101,7 @@ func handleElement(ctx *Context) {
if tag == "ol" {
w.SetProperty("listCount", 0)
}
- w.Styler(func(s *styles.Style) {
+ w.FinalStyler(func(s *styles.Style) {
s.Grow.Set(1, 0)
s.Padding.Left.Ch(core.ConstantSpacing(float32(4 * ld)))
})
diff --git a/text/paginate/extract.go b/text/paginate/extract.go
index 88ca3d5481..a4381aa9d7 100644
--- a/text/paginate/extract.go
+++ b/text/paginate/extract.go
@@ -68,7 +68,7 @@ func (p *pager) extract() []*item {
continue
}
gap := cpw.Styles.Gap.Dots().Floor()
- // todo: left margin
+ left := cpw.Styles.Padding.Left.Dots
if cp.i == 0 {
gap.Y = 0
}
@@ -81,7 +81,7 @@ func (p *pager) extract() []*item {
}
}
}
- its = append(its, &item{w: cw.This.(core.Widget), gap: gap})
+ its = append(its, &item{w: cw.This.(core.Widget), gap: gap, left: left})
next()
if atEnd {
break
diff --git a/text/paginate/layout.go b/text/paginate/layout.go
index 4a288851fe..98f5f070b1 100644
--- a/text/paginate/layout.go
+++ b/text/paginate/layout.go
@@ -9,6 +9,7 @@ import (
"cogentcore.org/core/core"
"cogentcore.org/core/math32"
+ "cogentcore.org/core/styles"
"cogentcore.org/core/tree"
)
@@ -64,21 +65,47 @@ func (p *pager) pagify(its []*item) [][]*item {
return pgs
}
-// layout reorders items within the pages and generates final output.
-func (p *pager) layout(its []*item) {
- pgs := p.pagify(its)
+func (p *pager) outputPages(pgs [][]*item) {
for _, pg := range pgs {
- // todo: rearrange elements to put text at bottom and non-text at top
-
- gap := math32.Vector2{}
+ lastGap := math32.Vector2{}
+ lastLeft := float32(0)
if len(pg) > 0 {
- gap = pg[0].gap // todo: better
+ lastGap = pg[0].gap
}
- // now transfer over to frame
- page, body := p.newPage(gap)
+ page, body := p.newPage(lastGap)
+ cpar := body
for _, it := range pg {
- tree.MoveToParent(it.w, body)
+ gap := it.gap
+ left := it.left
+ if gap != lastGap || left != lastLeft {
+ cpar = p.newOutFrame(body, gap, left)
+ lastGap = gap
+ lastLeft = left
+ }
+ tree.MoveToParent(it.w, cpar)
}
p.outs = append(p.outs, page)
}
}
+
+func (p *pager) newOutFrame(par *core.Frame, gap math32.Vector2, left float32) *core.Frame {
+ fr := core.NewFrame(par)
+ fr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ s.ZeroSpace()
+ s.Min.X.Dot(p.opts.BodyDots.X)
+ s.Max.X.Dot(p.opts.BodyDots.X)
+ s.Gap.X.Dot(gap.X)
+ s.Gap.Y.Dot(gap.Y)
+ s.Padding.Left.Dot(core.ConstantSpacing(left))
+ })
+ return fr
+}
+
+// layout reorders items within the pages and generates final output.
+func (p *pager) layout(its []*item) {
+ pgs := p.pagify(its)
+ // note: could rearrange elements to put text at bottom and non-text at top?
+ // but this is probably not necessary?
+ p.outputPages(pgs)
+}
diff --git a/text/paginate/page.go b/text/paginate/page.go
index afafa6dce2..650047b983 100644
--- a/text/paginate/page.go
+++ b/text/paginate/page.go
@@ -12,14 +12,15 @@ import (
"cogentcore.org/core/styles"
)
+func styMinMax(s *styles.Style, x, y float32) {
+ s.ZeroSpace()
+ s.Min.X.Dot(x)
+ s.Min.Y.Dot(y)
+ s.Max.X.Dot(x)
+ s.Max.Y.Dot(y)
+}
+
func (p *pager) newPage(gap math32.Vector2) (page, body *core.Frame) {
- styMinMax := func(s *styles.Style, x, y float32) {
- s.ZeroSpace()
- s.Min.X.Dot(x)
- s.Min.Y.Dot(y)
- s.Max.X.Dot(x)
- s.Max.Y.Dot(y)
- }
curPage := len(p.outs) + 1
pn := fmt.Sprintf("page-%d", curPage)
From 229ec7f08df9f4f02dde6bc41ada40257cd00d22 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 7 Oct 2025 23:24:36 +0200
Subject: [PATCH 34/99] pdf: MouseUp always clears Active state regardless --
can be set for non-Activatable items
---
content/handlers.go | 4 ----
core/widgetevents.go | 5 ++---
styles/style_props.go | 4 ++++
styles/text.go | 15 +++++++++++++++
text/text/props.go | 2 ++
text/text/style.go | 6 ++++++
6 files changed, 29 insertions(+), 7 deletions(-)
diff --git a/content/handlers.go b/content/handlers.go
index 0f94d54ad0..8e70584524 100644
--- a/content/handlers.go
+++ b/content/handlers.go
@@ -33,10 +33,6 @@ import (
// code blocks in text editors.
var BindTextEditor func(ed *textcore.Editor, parent *core.Frame, language string)
-// * add after-first-line indent (what is official name?) to text style and implement it -- need for li etc
-// * paginate uses left margins
-// * not able to click off shading of figures etc when clicking link
-
// handles the id attribute in htmlcore: needed for equation case
func (ct *Content) htmlIDAttributeHandler(ctx *htmlcore.Context, w io.Writer, node ast.Node, entering bool, tag, value string) bool {
if ct.currentPage == nil {
diff --git a/core/widgetevents.go b/core/widgetevents.go
index 45d52ce995..78c92c266b 100644
--- a/core/widgetevents.go
+++ b/core/widgetevents.go
@@ -335,9 +335,8 @@ func (wb *WidgetBase) handleWidgetStateFromMouse() {
}
})
wb.On(events.MouseUp, func(e events.Event) {
- if wb.AbilityIs(abilities.Activatable) {
- wb.SetState(false, states.Active)
- }
+ // note: not contingent on Activatable: always clear on up
+ wb.SetState(false, states.Active)
})
wb.On(events.LongPressStart, func(e events.Event) {
if wb.AbilityIs(abilities.LongPressable) {
diff --git a/styles/style_props.go b/styles/style_props.go
index d288ae6da1..fdf0ff65ad 100644
--- a/styles/style_props.go
+++ b/styles/style_props.go
@@ -516,6 +516,10 @@ var styleFuncs = map[string]styleprops.Func{
func(obj *Text) enums.EnumSetter { return &obj.WhiteSpace }),
"direction": styleprops.Enum(rich.LTR,
func(obj *Text) enums.EnumSetter { return &obj.Direction }),
+ "text-indent": styleprops.Units(units.Value{},
+ func(obj *Text) *units.Value { return &obj.Indent }),
+ "text-hanging": styleprops.Units(units.Value{},
+ func(obj *Text) *units.Value { return &obj.Hanging }),
"tab-size": styleprops.Int(int(4),
func(obj *Text) *int { return &obj.TabSize }),
"select-color": func(obj any, key string, val any, parent any, cc colors.Context) {
diff --git a/styles/text.go b/styles/text.go
index 96331e462d..e5ac793188 100644
--- a/styles/text.go
+++ b/styles/text.go
@@ -45,6 +45,13 @@ type Text struct { //types:add
// unicode text is typically written in a different direction.
Direction rich.Directions
+ // Indent specifies how much to indent the first line in a paragraph (inherited).
+ Indent units.Value
+
+ // Hanging specifies how much to indent all but the first line
+ // in a paragraph (inherited).
+ Hanging units.Value
+
// TabSize specifies the tab size, in number of characters (inherited).
TabSize int
@@ -67,6 +74,8 @@ func (ts *Text) Defaults() {
// ToDots runs ToDots on unit values, to compile down to raw pixels
func (ts *Text) ToDots(uc *units.Context) {
+ ts.Indent.ToDots(uc)
+ ts.Hanging.ToDots(uc)
}
// InheritFields from parent
@@ -76,6 +85,8 @@ func (ts *Text) InheritFields(parent *Text) {
ts.LineHeight = parent.LineHeight
// ts.WhiteSpace = par.WhiteSpace // note: we can't inherit this b/c label base default then gets overwritten
ts.Direction = parent.Direction
+ ts.Indent = parent.Indent
+ ts.Hanging = parent.Hanging
ts.TabSize = parent.TabSize
ts.SelectColor = parent.SelectColor
ts.HighlightColor = parent.HighlightColor
@@ -88,6 +99,8 @@ func (ts *Text) SetText(tsty *text.Style) {
tsty.LineHeight = ts.LineHeight
tsty.WhiteSpace = ts.WhiteSpace
tsty.Direction = ts.Direction
+ tsty.Indent = ts.Indent
+ tsty.Hanging = ts.Hanging
tsty.TabSize = ts.TabSize
tsty.SelectColor = ts.SelectColor
tsty.HighlightColor = ts.HighlightColor
@@ -100,6 +113,8 @@ func (ts *Text) SetFromText(tsty *text.Style) {
ts.LineHeight = tsty.LineHeight
ts.WhiteSpace = tsty.WhiteSpace
ts.Direction = tsty.Direction
+ ts.Indent = tsty.Indent
+ ts.Hanging = tsty.Hanging
ts.TabSize = tsty.TabSize
ts.SelectColor = tsty.SelectColor
ts.HighlightColor = tsty.HighlightColor
diff --git a/text/text/props.go b/text/text/props.go
index cfe3672226..8eca9cb161 100644
--- a/text/text/props.go
+++ b/text/text/props.go
@@ -61,6 +61,8 @@ var styleFuncs = map[string]styleprops.Func{
func(obj *Style) enums.EnumSetter { return &obj.Direction }),
"text-indent": styleprops.Units(units.Value{},
func(obj *Style) *units.Value { return &obj.Indent }),
+ "text-hanging": styleprops.Units(units.Value{},
+ func(obj *Style) *units.Value { return &obj.Hanging }),
"tab-size": styleprops.Int(int(4),
func(obj *Style) *int { return &obj.TabSize }),
"select-color": func(obj any, key string, val any, parent any, cc colors.Context) {
diff --git a/text/text/style.go b/text/text/style.go
index b72845749b..b1087346c0 100644
--- a/text/text/style.go
+++ b/text/text/style.go
@@ -69,6 +69,10 @@ type Style struct { //types:add
// Indent specifies how much to indent the first line in a paragraph (inherited).
Indent units.Value
+ // Hanging specifies how much to indent all but the first line
+ // in a paragraph (inherited).
+ Hanging units.Value
+
// TabSize specifies the tab size, in number of characters (inherited).
TabSize int
@@ -110,6 +114,7 @@ func (ts *Style) ToDots(uc *units.Context) {
ts.FontSize.ToDots(uc)
ts.FontSize.Dots = math32.Round(ts.FontSize.Dots)
ts.Indent.ToDots(uc)
+ ts.Hanging.ToDots(uc)
}
// InheritFields from parent
@@ -121,6 +126,7 @@ func (ts *Style) InheritFields(parent *Style) {
// ts.WhiteSpace = par.WhiteSpace // todo: we can't inherit this b/c label base default then gets overwritten
ts.Direction = parent.Direction
ts.Indent = parent.Indent
+ ts.Hanging = parent.Hanging
ts.TabSize = parent.TabSize
ts.SelectColor = parent.SelectColor
ts.HighlightColor = parent.HighlightColor
From d14f36297c2a1a1804dc90a9302ce9dc4f075561 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 8 Oct 2025 01:18:25 +0200
Subject: [PATCH 35/99] pdf: fix the completely bizarre first-text-line-offset
issue with PDFs: needs an extra 3 pixels. The ascent thing must be wrong
somehow, somewhere.. will fix properly later.
---
core/text.go | 7 +++++++
paint/pdf/text.go | 4 ++--
text/paginate/layout.go | 4 +++-
text/paginate/pdf.go | 15 ++++++++++++++-
4 files changed, 26 insertions(+), 4 deletions(-)
diff --git a/core/text.go b/core/text.go
index 8de8b424c0..ff51b3ad30 100644
--- a/core/text.go
+++ b/core/text.go
@@ -119,6 +119,13 @@ const (
func (tx *Text) WidgetValue() any { return &tx.Text }
+// PaintText returns the shaped representation of the text,
+// which is needed for some specific special-case rendering
+// situations.
+func (tx *Text) PaintText() *shaped.Lines {
+ return tx.paintText
+}
+
func (tx *Text) Init() {
tx.WidgetBase.Init()
tx.AddContextMenu(tx.contextMenu)
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
index c2c9ebb85e..c8491674f0 100644
--- a/paint/pdf/text.go
+++ b/paint/pdf/text.go
@@ -31,14 +31,14 @@ func (r *PDF) Text(style *styles.Paint, m math32.Matrix2, pos math32.Vector2, ln
runes := lns.Source.Join()
for li := range lns.Lines {
ln := &lns.Lines[li]
- r.textLine(style, m, ln, lns, runes, clr, off)
+ r.textLine(style, m, li, ln, lns, runes, clr, off)
}
r.links(lns, m, pos)
r.w.PopStack()
}
// TextLine rasterizes the given shaped.Line.
-func (r *PDF) textLine(style *styles.Paint, m math32.Matrix2, ln *shaped.Line, lns *shaped.Lines, runes []rune, clr image.Image, off math32.Vector2) {
+func (r *PDF) textLine(style *styles.Paint, m math32.Matrix2, li int, ln *shaped.Line, lns *shaped.Lines, runes []rune, clr image.Image, off math32.Vector2) {
start := off.Add(ln.Offset)
off = start
for ri := range ln.Runs {
diff --git a/text/paginate/layout.go b/text/paginate/layout.go
index 98f5f070b1..381da52344 100644
--- a/text/paginate/layout.go
+++ b/text/paginate/layout.go
@@ -39,9 +39,11 @@ func (p *pager) pagify(its []*item) [][]*item {
over := ht+sz > maxY
if !over && nobrk {
if i < n-1 {
- nsz := size(its[i+1])
+ nsz := size(its[i+1]) + 100 // extra space to be sure
if ht+sz+nsz > maxY {
over = true // break now
+ // } else {
+ // fmt.Println("not !over, nobrk:", ht, sz, nsz, maxY, ht+sz+nsz, cw, its[i+1])
}
}
}
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index 7eec91fc19..ee74c58992 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -44,7 +44,20 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
tree.MoveToParent(p, sc)
p.SetScene(sc)
sc.StyleTree()
- sc.LayoutRenderScene()
+ sc.LayoutScene()
+
+ sc.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
+ if tx, ok := cwb.This.(*core.Text); ok {
+ lns := tx.PaintText()
+ if len(lns.Lines) > 0 {
+ ln := &lns.Lines[0]
+ ln.Offset.Y += 3 // todo: seriously, this fixes an otherwise inexplicable offset
+ }
+ }
+ return true
+ })
+
+ sc.RenderWidget()
rend := sc.Painter.RenderDone()
pdr.RenderPage(rend)
From 9a085e7271882b45c6b4dce99c26af78e4c07ab5 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 8 Oct 2025 02:07:32 +0200
Subject: [PATCH 36/99] pdf: render uniform boxes, fix path cases -- just need
gradient now; getting weird sizing issue in BG -- can't quite figure it out.
---
paint/pdf/pdf.go | 50 ++++++++++++--------------
paint/renderers/pdfrender/pdfrender.go | 7 +---
svg/svg_test.go | 2 +-
text/paginate/layout.go | 11 +++---
text/paginate/pdf.go | 4 ++-
5 files changed, 31 insertions(+), 43 deletions(-)
diff --git a/paint/pdf/pdf.go b/paint/pdf/pdf.go
index 6e49389ccc..640766873c 100644
--- a/paint/pdf/pdf.go
+++ b/paint/pdf/pdf.go
@@ -176,7 +176,8 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
closed = true
}
- if style.HasStroke() && strokeUnsupported { // todo
+ if style.HasStroke() && strokeUnsupported {
+ // todo: handle with optional inclusion of stroke function as _ import
/* // style.HasStroke() && strokeUnsupported
if style.HasFill() {
r.w.SetFill(style.Fill)
@@ -199,7 +200,7 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
r.w.Write([]byte(path.Transform(m).ToPDF()))
r.w.Write([]byte(" f"))
*/
- return
+ // return
}
if style.HasFill() && !style.HasStroke() {
r.w.SetFill(&style.Fill)
@@ -238,34 +239,27 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
if style.Fill.Rule == ppath.EvenOdd {
r.w.Write([]byte("*"))
}
- }
- /*
- else {
- r.w.SetFill(style.Fill)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- r.w.Write([]byte(" f"))
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
+ } else {
+ r.w.SetFill(&style.Fill)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ r.w.Write([]byte(" f"))
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
- r.w.SetStroke(style.Stroke)
- r.w.SetLineWidth(style.StrokeWidth)
- r.w.SetLineCap(style.StrokeCapper)
- r.w.SetLineJoin(style.StrokeJoiner)
- r.w.SetDashes(style.DashOffset, style.Dashes)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- if closed {
- r.w.Write([]byte(" s"))
- } else {
- r.w.Write([]byte(" S"))
- }
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
+ r.w.SetStroke(&style.Stroke)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ if closed {
+ r.w.Write([]byte(" s"))
+ } else {
+ r.w.Write([]byte(" S"))
}
- */
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+ }
}
}
diff --git a/paint/renderers/pdfrender/pdfrender.go b/paint/renderers/pdfrender/pdfrender.go
index b4d0d84d86..5a191f80ae 100644
--- a/paint/renderers/pdfrender/pdfrender.go
+++ b/paint/renderers/pdfrender/pdfrender.go
@@ -160,12 +160,7 @@ func (rs *Renderer) RenderImage(pr *pimage.Params) {
// Fast path for [image.Uniform]
if u, ok := usrc.(*image.Uniform); nilSrc || ok && umask == nil {
- _ = u
- // todo: draw a box
- // r := fpdf.NewRect(cg)
- // r.Pos = math32.FromPoint(pr.Rect.Min)
- // r.Size = math32.FromPoint(pr.Rect.Size())
- // r.SetProperty("fill", colors.AsHex(u.C))
+ rs.PDF.FillBox(math32.Identity2(), math32.B2FromRect(pr.Rect), u)
return
}
diff --git a/svg/svg_test.go b/svg/svg_test.go
index 8663c1f574..9a7475c962 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -155,7 +155,7 @@ func TestEmoji(t *testing.T) {
}
func TestFontEmoji(t *testing.T) {
- // t.Skip("special-case testing -- requires noto-emoji file")
+ t.Skip("special-case testing -- requires noto-emoji file")
// dir := filepath.Join("testdata", "noto-emoji")
os.MkdirAll("testdata/font-emoji-src", 0777)
fname := "/Library/Fonts/NotoColorEmoji-Regular.ttf"
diff --git a/text/paginate/layout.go b/text/paginate/layout.go
index 381da52344..cb4c2f0984 100644
--- a/text/paginate/layout.go
+++ b/text/paginate/layout.go
@@ -5,8 +5,6 @@
package paginate
import (
- "fmt"
-
"cogentcore.org/core/core"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
@@ -17,11 +15,10 @@ import (
func (p *pager) pagify(its []*item) [][]*item {
widg := core.AsWidget
size := func(it *item) float32 {
- ih := widg(it.w).Geom.Size.Actual.Total.Y
- if ih == 0 {
- fmt.Println("zero height", ih, it.w)
- }
- return ih + it.gap.Y
+ wb := widg(it.w)
+ ih := wb.Geom.Size.Actual.Total.Y // todo: something wrong with size!
+ sz := ih + it.gap.Y
+ return sz
}
maxY := p.opts.BodyDots.Y
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index ee74c58992..2ea1f2e2ca 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -31,6 +31,8 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
p.preRender()
p.paginate()
+ // osc := ins[0].AsWidget().Scene
+
sc := core.NewScene()
sz := math32.Geom2DInt{}
sz.Size = opts.SizeDots.ToPointCeil()
@@ -57,7 +59,7 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
return true
})
- sc.RenderWidget()
+ p.RenderWidget()
rend := sc.Painter.RenderDone()
pdr.RenderPage(rend)
From a497b88f2f84602001fc8f7ed19ebfadee3341ed Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 8 Oct 2025 09:12:04 +0200
Subject: [PATCH 37/99] pdf: pdf sizing issues all fixed -- need to pre-render
in the same generic config as final rendering will use.
---
content/content.go | 3 +++
core/events.go | 4 ++++
text/paginate/layout.go | 46 +++++++++++++++++++++++++++++++--------
text/paginate/page.go | 10 ++++-----
text/paginate/paginate.go | 8 +++----
text/paginate/pdf.go | 9 ++++----
6 files changed, 56 insertions(+), 24 deletions(-)
diff --git a/content/content.go b/content/content.go
index 88404eabc0..01966d1956 100644
--- a/content/content.go
+++ b/content/content.go
@@ -500,6 +500,9 @@ func (ct *Content) PagePDF(path string) error {
paginate.PDF(f, opts.PDF, ct.rightFrame)
err = f.Close()
ct.reloadPage()
+
core.MessageSnackbar(ct, "PDF saved to: "+fname)
+ af := errors.Log1(filepath.Abs(fname))
+ core.TheApp.OpenURL("file://" + af)
return err
}
diff --git a/core/events.go b/core/events.go
index a545ed94fb..399f52261c 100644
--- a/core/events.go
+++ b/core/events.go
@@ -1212,6 +1212,10 @@ func (em *Events) managerKeyChordEvents(e events.Event) {
pd := pdr.Render(rend).Source()
fnm = filepath.Join(TheApp.AppDataDir(), "screenshot-"+sc.Name+"-"+dstr+".pdf")
errors.Log(os.WriteFile(fnm, pd, 0666))
+
+ sc.SetScene(sc)
+ sc.Update()
+
MessageSnackbar(sc, "Saved SVG, PDF screenshots to: "+strings.ReplaceAll(fnm, " ", `\ `)+sz)
e.SetHandled()
case keymap.ZoomIn:
diff --git a/text/paginate/layout.go b/text/paginate/layout.go
index cb4c2f0984..6df9d26aa5 100644
--- a/text/paginate/layout.go
+++ b/text/paginate/layout.go
@@ -17,8 +17,7 @@ func (p *pager) pagify(its []*item) [][]*item {
size := func(it *item) float32 {
wb := widg(it.w)
ih := wb.Geom.Size.Actual.Total.Y // todo: something wrong with size!
- sz := ih + it.gap.Y
- return sz
+ return ih + it.gap.Y
}
maxY := p.opts.BodyDots.Y
@@ -36,7 +35,7 @@ func (p *pager) pagify(its []*item) [][]*item {
over := ht+sz > maxY
if !over && nobrk {
if i < n-1 {
- nsz := size(its[i+1]) + 100 // extra space to be sure
+ nsz := size(its[i+1]) // extra space to be sure
if ht+sz+nsz > maxY {
over = true // break now
// } else {
@@ -64,14 +63,15 @@ func (p *pager) pagify(its []*item) [][]*item {
return pgs
}
-func (p *pager) outputPages(pgs [][]*item) {
- for _, pg := range pgs {
+func (p *pager) outputPages(pgs [][]*item, newPage func(gap math32.Vector2, pageNo int) (page, body *core.Frame)) []*core.Frame {
+ var outs []*core.Frame
+ for pn, pg := range pgs {
lastGap := math32.Vector2{}
lastLeft := float32(0)
if len(pg) > 0 {
lastGap = pg[0].gap
}
- page, body := p.newPage(lastGap)
+ page, body := newPage(lastGap, pn+1)
cpar := body
for _, it := range pg {
gap := it.gap
@@ -83,12 +83,18 @@ func (p *pager) outputPages(pgs [][]*item) {
}
tree.MoveToParent(it.w, cpar)
}
- p.outs = append(p.outs, page)
+ outs = append(outs, page)
}
+ return outs
}
func (p *pager) newOutFrame(par *core.Frame, gap math32.Vector2, left float32) *core.Frame {
- fr := core.NewFrame(par)
+ var fr *core.Frame
+ if par != nil {
+ fr = core.NewFrame(par)
+ } else {
+ fr = core.NewFrame()
+ }
fr.Styler(func(s *styles.Style) {
s.Direction = styles.Column
s.ZeroSpace()
@@ -101,10 +107,32 @@ func (p *pager) newOutFrame(par *core.Frame, gap math32.Vector2, left float32) *
return fr
}
+// pre-render everything in the offscreen scene that will be used for final
+// to get accurate element sizes.
+func (p *pager) preRender(its []*item) {
+ pg := [][]*item{its}
+ op := p.outputPages(pg, func(gap math32.Vector2, pageNo int) (page, body *core.Frame) {
+ fr := p.newOutFrame(nil, gap, 0)
+ page, body = fr, fr
+ return
+ })
+ sc := core.NewScene()
+ sz := math32.Geom2DInt{}
+ sz.Size = p.opts.SizeDots.ToPointCeil()
+ sc.Resize(sz)
+ sc.MakeTextShaper()
+
+ tree.MoveToParent(op[0], sc)
+ op[0].SetScene(sc)
+ sc.StyleTree()
+ sc.LayoutScene()
+}
+
// layout reorders items within the pages and generates final output.
func (p *pager) layout(its []*item) {
+ p.preRender(its)
pgs := p.pagify(its)
// note: could rearrange elements to put text at bottom and non-text at top?
// but this is probably not necessary?
- p.outputPages(pgs)
+ p.outs = p.outputPages(pgs, p.newPage)
}
diff --git a/text/paginate/page.go b/text/paginate/page.go
index 650047b983..7c092768a7 100644
--- a/text/paginate/page.go
+++ b/text/paginate/page.go
@@ -20,10 +20,8 @@ func styMinMax(s *styles.Style, x, y float32) {
s.Max.Y.Dot(y)
}
-func (p *pager) newPage(gap math32.Vector2) (page, body *core.Frame) {
-
- curPage := len(p.outs) + 1
- pn := fmt.Sprintf("page-%d", curPage)
+func (p *pager) newPage(gap math32.Vector2, pageNo int) (page, body *core.Frame) {
+ pn := fmt.Sprintf("page-%d", pageNo)
page = core.NewFrame()
page.SetName(pn)
@@ -51,7 +49,7 @@ func (p *pager) newPage(gap math32.Vector2) (page, body *core.Frame) {
styMinMax(s, p.opts.BodyDots.X, p.opts.MargDots.Top)
})
if p.opts.Header != nil {
- p.opts.Header(hdr, p.opts, curPage)
+ p.opts.Header(hdr, p.opts, pageNo)
}
body = core.NewFrame(bfr)
@@ -70,7 +68,7 @@ func (p *pager) newPage(gap math32.Vector2) (page, body *core.Frame) {
styMinMax(s, p.opts.BodyDots.X, p.opts.MargDots.Bottom)
})
if p.opts.Footer != nil {
- p.opts.Footer(ftr, p.opts, curPage)
+ p.opts.Footer(ftr, p.opts, pageNo)
}
return
diff --git a/text/paginate/paginate.go b/text/paginate/paginate.go
index 2dc60a59e3..b50274479c 100644
--- a/text/paginate/paginate.go
+++ b/text/paginate/paginate.go
@@ -6,7 +6,6 @@ package paginate
import (
"cogentcore.org/core/core"
- "cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/text/rich"
@@ -45,9 +44,9 @@ func (p *pager) optsUpdate() {
p.opts.ToDots(&p.ctx)
}
-// preRender re-renders inputs with styles enforced to fit page size,
-// and setting the font family and size for text elements.
-func (p *pager) preRender() {
+// assemble collects everything to be rendered into one big list,
+// and sets the font family and size for text elements.
+func (p *pager) assemble() {
sc := core.AsWidget(p.ins[0]).Scene
// sc.AsyncLock()
if p.opts.Title != nil {
@@ -60,7 +59,6 @@ func (p *pager) preRender() {
p.opts.Title(tf, p.opts)
p.ins = append([]core.Widget{tf.This.(core.Widget)}, p.ins...)
tf.StyleTree()
- tf.LayoutFrame(math32.FromPoint(tf.Scene.SceneGeom.Size))
}
for _, in := range p.ins {
iw := core.AsWidget(in)
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index 2ea1f2e2ca..cd1656fde3 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -12,6 +12,7 @@ import (
"cogentcore.org/core/paint"
"cogentcore.org/core/paint/pdf"
"cogentcore.org/core/paint/renderers/pdfrender"
+ "cogentcore.org/core/styles/units"
"cogentcore.org/core/tree"
)
@@ -27,12 +28,12 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
cset := pdf.UseStandardFonts()
p := pager{opts: &opts, ins: ins}
- p.optsUpdate()
- p.preRender()
+ p.opts.Update()
+ p.ctx = *units.NewContext() // generic, invariant of actual context
+ p.opts.ToDots(&p.ctx)
+ p.assemble()
p.paginate()
- // osc := ins[0].AsWidget().Scene
-
sc := core.NewScene()
sz := math32.Geom2DInt{}
sz.Size = opts.SizeDots.ToPointCeil()
From 20df3c5c0d9ecd228fcda09f3218fee7a594376f Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 8 Oct 2025 10:57:36 +0200
Subject: [PATCH 38/99] pdf: js/web version downloads the PDF file -- fixed
crash there and have to rebuild core tool to avoid error with Authors change
to string instead of []string.
---
content/buttons.go | 2 +-
content/typegen.go | 2 +-
content/url_js.go | 19 ++++++++++++++-----
paint/pdf/page.go | 6 +++++-
4 files changed, 21 insertions(+), 8 deletions(-)
diff --git a/content/buttons.go b/content/buttons.go
index d751d6649f..50cf3db8da 100644
--- a/content/buttons.go
+++ b/content/buttons.go
@@ -75,7 +75,7 @@ func (ct *Content) MakeToolbar(p *tree.Plan) {
})
})
tree.Add(p, func(w *core.Button) {
- w.SetText("PDF").SetIcon(icons.PictureAsPdf)
+ w.SetText("PDF").SetIcon(icons.PictureAsPdf).SetTooltip("PDF generates and opens / downloads the current page as a printable PDF file")
w.OnClick(func(e events.Event) {
ct.PagePDF("pdfs")
})
diff --git a/content/typegen.go b/content/typegen.go
index 62b815c568..a9df91b5c8 100644
--- a/content/typegen.go
+++ b/content/typegen.go
@@ -8,7 +8,7 @@ import (
"cogentcore.org/core/types"
)
-var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/content.Content", IDName: "content", Doc: "Content manages and displays the content of a set of pages.", Embeds: []types.Field{{Name: "Splits"}}, Fields: []types.Field{{Name: "Source", Doc: "Source is the source filesystem for the content.\nIt should be set using [Content.SetSource] or [Content.SetContent]."}, {Name: "Context", Doc: "Context is the [htmlcore.Context] used to render the content,\nwhich can be modified for things such as adding wikilink handlers."}, {Name: "References", Doc: "References is a list of references used for generating citation text\nfor literature reference wikilinks in the format [[@CiteKey]]."}, {Name: "pages", Doc: "pages are the pages that constitute the content."}, {Name: "pagesByName", Doc: "pagesByName has the [bcontent.Page] for each [bcontent.Page.Name]\ntransformed into lowercase. See [Content.pageByName] for a helper\nfunction that automatically transforms into lowercase."}, {Name: "pagesByURL", Doc: "pagesByURL has the [bcontent.Page] for each [bcontent.Page.URL]."}, {Name: "pagesByCategory", Doc: "pagesByCategory has the [bcontent.Page]s for each of all [bcontent.Page.Categories]."}, {Name: "categories", Doc: "categories has all unique [bcontent.Page.Categories], sorted such that the categories\nwith the most pages are listed first."}, {Name: "history", Doc: "history is the history of pages that have been visited.\nThe oldest page is first."}, {Name: "historyIndex", Doc: "historyIndex is the current position in [Content.history]."}, {Name: "currentPage", Doc: "currentPage is the currently open page."}, {Name: "renderedPage", Doc: "renderedPage is the most recently rendered page."}, {Name: "leftFrame", Doc: "leftFrame is the frame on the left side of the widget,\nused for displaying the table of contents and the categories."}, {Name: "rightFrame", Doc: "rightFrame is the frame on the right side of the widget,\nused for displaying the page content."}, {Name: "tocNodes", Doc: "tocNodes are all of the tree nodes in the table of contents\nby kebab-case heading name."}, {Name: "currentHeading", Doc: "currentHeading is the currently selected heading in the table of contents,\nif any (in kebab-case)."}, {Name: "prevPage", Doc: "The previous and next page, if applicable. They must be stored on this struct\nto avoid stale local closure variables."}, {Name: "nextPage", Doc: "The previous and next page, if applicable. They must be stored on this struct\nto avoid stale local closure variables."}}})
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/content.Content", IDName: "content", Doc: "Content manages and displays the content of a set of pages.", Embeds: []types.Field{{Name: "Splits"}}, Fields: []types.Field{{Name: "Source", Doc: "Source is the source filesystem for the content.\nIt should be set using [Content.SetSource] or [Content.SetContent]."}, {Name: "Context", Doc: "Context is the [htmlcore.Context] used to render the content,\nwhich can be modified for things such as adding wikilink handlers."}, {Name: "References", Doc: "References is a list of references used for generating citation text\nfor literature reference wikilinks in the format [[@CiteKey]]."}, {Name: "pages", Doc: "pages are the pages that constitute the content."}, {Name: "pagesByName", Doc: "pagesByName has the [bcontent.Page] for each [bcontent.Page.Name]\ntransformed into lowercase. See [Content.pageByName] for a helper\nfunction that automatically transforms into lowercase."}, {Name: "pagesByURL", Doc: "pagesByURL has the [bcontent.Page] for each [bcontent.Page.URL]."}, {Name: "pagesByCategory", Doc: "pagesByCategory has the [bcontent.Page]s for each of all [bcontent.Page.Categories]."}, {Name: "categories", Doc: "categories has all unique [bcontent.Page.Categories], sorted such that the categories\nwith the most pages are listed first. \"Other\" is always last, and is used for pages that\ndo not have a category, unless they are a category themselves."}, {Name: "history", Doc: "history is the history of pages that have been visited.\nThe oldest page is first."}, {Name: "historyIndex", Doc: "historyIndex is the current position in [Content.history]."}, {Name: "currentPage", Doc: "currentPage is the currently open page."}, {Name: "renderedPage", Doc: "renderedPage is the most recently rendered page."}, {Name: "leftFrame", Doc: "leftFrame is the frame on the left side of the widget,\nused for displaying the table of contents and the categories."}, {Name: "rightFrame", Doc: "rightFrame is the frame on the right side of the widget,\nused for displaying the page content."}, {Name: "tocNodes", Doc: "tocNodes are all of the tree nodes in the table of contents\nby kebab-case heading name."}, {Name: "currentHeading", Doc: "currentHeading is the currently selected heading in the table of contents,\nif any (in kebab-case)."}, {Name: "inPDFRender", Doc: "inPDFRender indicates that it is rendering a PDF now, turning off\nelements that are not appropriate for that."}, {Name: "prevPage", Doc: "The previous and next page, if applicable. They must be stored on this struct\nto avoid stale local closure variables."}, {Name: "nextPage", Doc: "The previous and next page, if applicable. They must be stored on this struct\nto avoid stale local closure variables."}}})
// NewContent returns a new [Content] with the given optional parent:
// Content manages and displays the content of a set of pages.
diff --git a/content/url_js.go b/content/url_js.go
index fae4fa1843..c8b194b5b0 100644
--- a/content/url_js.go
+++ b/content/url_js.go
@@ -14,11 +14,17 @@ import (
"cogentcore.org/core/base/errors"
)
-// firstContent is the first [Content] used for [Content.getWebURL] or [Content.saveWebURL],
-// which is used to prevent nested [Content] widgets from incorrectly affecting the URL.
-var firstContent *Content
+var (
+ // firstContent is the first [Content] used for [Content.getWebURL] or [Content.saveWebURL],
+ // which is used to prevent nested [Content] widgets from incorrectly affecting the URL.
+ firstContent *Content
-var documentData = js.Global().Get("document").Get("documentElement").Get("dataset")
+ documentData = js.Global().Get("document").Get("documentElement").Get("dataset")
+
+ // OfflineURL is the non-web base url, which can be set to allow
+ // docs to refer to this in frontmatter.
+ OfflineURL = ""
+)
func (ct *Content) getPrintURL() string { return ct.getWebURL() }
@@ -35,7 +41,10 @@ func (ct *Content) getWebURL() string {
if errors.Log(err) != nil {
return ""
}
- return strings.Trim(strings.TrimPrefix(full.String(), base.String()), "/")
+ ur := strings.Trim(strings.TrimPrefix(full.String(), base.String()), "/")
+ // fmt.Println("url is:", ur)
+ OfflineURL = ur
+ return ur
}
// saveWebURL saves the current page URL to the user's address bar and history.
diff --git a/paint/pdf/page.go b/paint/pdf/page.go
index d4ec1b3e50..2faf53cc32 100644
--- a/paint/pdf/page.go
+++ b/paint/pdf/page.go
@@ -11,6 +11,7 @@ import (
"bytes"
"fmt"
"image"
+ "image/color"
"log/slog"
"cogentcore.org/core/base/errors"
@@ -117,7 +118,10 @@ func (w *pdfPage) SetFillColor(fill *styles.Fill) {
// TODO: should we unset cs?
// fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(fill.Gradient))
case *image.Uniform:
- clr := colors.ApplyOpacity(colors.AsRGBA(x), fill.Opacity)
+ var clr color.RGBA
+ if x != nil {
+ clr = colors.ApplyOpacity(colors.AsRGBA(x), fill.Opacity)
+ }
a := float32(clr.A) / 255.0
if clr.R == clr.G && clr.R == clr.B {
fmt.Fprintf(w, " %v g", dec(float32(clr.R)/255.0/a))
From 6441853aa61309b8187c55ecf4f88ea1da90248f Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 8 Oct 2025 11:59:49 +0200
Subject: [PATCH 39/99] pdf: pdf generates references for current page; updated
csl to use fs.FS for compatibility.
---
content/content.go | 31 ++++++++++++++++++++++++++-
text/csl/md.go | 42 ++++++++++--------------------------
text/csl/mdcite/mdcite.go | 24 ++++++++++++++++++++-
text/paginate/paginate.go | 44 --------------------------------------
text/paginate/pdf.go | 45 +++++++++++++++++++++++++++++++++++++++
5 files changed, 109 insertions(+), 77 deletions(-)
diff --git a/content/content.go b/content/content.go
index 01966d1956..1ab18de796 100644
--- a/content/content.go
+++ b/content/content.go
@@ -487,6 +487,8 @@ func (ct *Content) PagePDF(path string) error {
ct.Update()
ct.inPDFRender = false
+ refs := ct.PageRefs(ct.currentPage)
+
fname := ct.currentPage.Name + ".pdf"
if path != "" {
os.MkdirAll(path, 0777)
@@ -497,8 +499,13 @@ func (ct *Content) PagePDF(path string) error {
return err
}
opts := Settings.PageSettings(ct, ct.currentPage)
- paginate.PDF(f, opts.PDF, ct.rightFrame)
+ if refs != nil {
+ paginate.PDF(f, opts.PDF, ct.rightFrame, refs)
+ } else {
+ paginate.PDF(f, opts.PDF, ct.rightFrame)
+ }
err = f.Close()
+
ct.reloadPage()
core.MessageSnackbar(ct, "PDF saved to: "+fname)
@@ -506,3 +513,25 @@ func (ct *Content) PagePDF(path string) error {
core.TheApp.OpenURL("file://" + af)
return err
}
+
+// PageRefs returns a core.Frame with the contents of the references cited
+// on the given page. if References is nil, or error, result will be nil.
+func (ct *Content) PageRefs(page *bcontent.Page) *core.Frame {
+ if ct.References == nil {
+ return nil
+ }
+ sty := csl.APA // todo: settings
+ var b bytes.Buffer
+ _, err := csl.GenerateMarkdown(&b, ct.Source, "## References", ct.References, sty, page.Filename)
+ if errors.Log(err) != nil {
+ return nil
+ }
+
+ fr := core.NewFrame()
+ err = htmlcore.ReadMD(ct.Context, fr, b.Bytes())
+ if errors.Log(err) != nil {
+ return nil
+ }
+ fr.SetScene(ct.Scene)
+ return fr
+}
diff --git a/text/csl/md.go b/text/csl/md.go
index d60e934c11..ed5d0d2a72 100644
--- a/text/csl/md.go
+++ b/text/csl/md.go
@@ -7,50 +7,29 @@ package csl
import (
"bufio"
"io"
- "os"
- "path/filepath"
+ "io/fs"
"regexp"
"strings"
"cogentcore.org/core/base/errors"
- "cogentcore.org/core/base/fsx"
)
// GenerateMarkdown extracts markdown citations in the format [@Ref; @Ref]
-// from .md markdown files in given directory, looking up in given source [KeyList],
-// and writing the results in given style to given .md file (references.md default).
+// from given mds markdown files in given filesystem (use os.DirFS for filesys),
+// looking up in given source [KeyList], and writing the results in given style to writer.
// Heading is written first: must include the appropriate markdown heading level
// (## typically). Returns the [KeyList] of references that were cited.
-func GenerateMarkdown(dir, refFile, heading string, kl *KeyList, sty Styles) (*KeyList, error) {
+func GenerateMarkdown(w io.Writer, fsys fs.FS, heading string, kl *KeyList, sty Styles, mds ...string) (*KeyList, error) {
cited := &KeyList{}
- if dir == "" {
- dir = "./"
- }
- mds := fsx.Filenames(dir, ".md")
- if len(mds) == 0 {
- return cited, errors.New("No .md files found in: " + dir)
- }
var errs []error
- for i := range mds {
- mds[i] = filepath.Join(dir, mds[i])
- }
- err := ExtractMarkdownCites(mds, kl, cited)
- if err != nil {
- errs = append(errs, err)
- }
- if refFile == "" {
- refFile = filepath.Join(dir, "references.md")
- }
- of, err := os.Create(refFile)
+ err := ExtractMarkdownCites(fsys, mds, kl, cited)
if err != nil {
errs = append(errs, err)
- return cited, errors.Join(errs...)
}
- defer of.Close()
if heading != "" {
- of.WriteString(heading + "\n\n")
+ w.Write([]byte(heading + "\n\n"))
}
- err = WriteRefsMarkdown(of, cited, sty)
+ err = WriteRefsMarkdown(w, cited, sty)
if err != nil {
errs = append(errs, err)
}
@@ -58,12 +37,13 @@ func GenerateMarkdown(dir, refFile, heading string, kl *KeyList, sty Styles) (*K
}
// ExtractMarkdownCites extracts markdown citations in the format [@Ref; @Ref]
-// from given list of .md files, looking up in given source [KeyList], adding to cited.
-func ExtractMarkdownCites(files []string, src, cited *KeyList) error {
+// from given list of .md files in given FS,
+// looking up in given source [KeyList], adding to cited.
+func ExtractMarkdownCites(fsys fs.FS, files []string, src, cited *KeyList) error {
exp := regexp.MustCompile(`\[(@\^?([[:alnum:]]+-?)+(;[[:blank:]]+)?)+\]`)
var errs []error
for _, fn := range files {
- f, err := os.Open(fn)
+ f, err := fsys.Open(fn)
if err != nil {
errs = append(errs, err)
continue
diff --git a/text/csl/mdcite/mdcite.go b/text/csl/mdcite/mdcite.go
index 1e22b7c540..0fed65513f 100644
--- a/text/csl/mdcite/mdcite.go
+++ b/text/csl/mdcite/mdcite.go
@@ -5,6 +5,11 @@
package main
import (
+ "os"
+ "path/filepath"
+
+ "cogentcore.org/core/base/errors"
+ "cogentcore.org/core/base/fsx"
"cogentcore.org/core/cli"
"cogentcore.org/core/text/csl"
)
@@ -45,7 +50,24 @@ func Generate(c *Config) error {
return err
}
kl := csl.NewKeyList(refs)
- cited, err := csl.GenerateMarkdown(c.Dir, c.Output, c.Heading, kl, c.Style)
+
+ if c.Dir == "" {
+ c.Dir = "./"
+ }
+ mds := fsx.Filenames(c.Dir, ".md")
+ if len(mds) == 0 {
+ return errors.New("No .md files found in: " + c.Dir)
+ }
+ if c.Output == "" {
+ c.Output = filepath.Join(c.Dir, "references.md")
+ }
+ of, err := os.Create(c.Output)
+ if errors.Log(err) != nil {
+ return err
+ }
+ defer of.Close()
+
+ cited, err := csl.GenerateMarkdown(of, os.DirFS(c.Dir), c.Heading, kl, c.Style, mds...)
cf := c.CitedData
if cf == "" {
cf = "citedrefs.json"
diff --git a/text/paginate/paginate.go b/text/paginate/paginate.go
index b50274479c..28dc11d75c 100644
--- a/text/paginate/paginate.go
+++ b/text/paginate/paginate.go
@@ -6,9 +6,7 @@ package paginate
import (
"cogentcore.org/core/core"
- "cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
- "cogentcore.org/core/text/rich"
_ "cogentcore.org/core/text/tex"
)
@@ -44,48 +42,6 @@ func (p *pager) optsUpdate() {
p.opts.ToDots(&p.ctx)
}
-// assemble collects everything to be rendered into one big list,
-// and sets the font family and size for text elements.
-func (p *pager) assemble() {
- sc := core.AsWidget(p.ins[0]).Scene
- // sc.AsyncLock()
- if p.opts.Title != nil {
- tf := core.NewFrame()
- tf.Scene = sc
- tf.FinalStyler(func(s *styles.Style) {
- s.Min.X.Dot(p.opts.BodyDots.X)
- s.Min.Y.Dot(p.opts.BodyDots.Y)
- })
- p.opts.Title(tf, p.opts)
- p.ins = append([]core.Widget{tf.This.(core.Widget)}, p.ins...)
- tf.StyleTree()
- }
- for _, in := range p.ins {
- iw := core.AsWidget(in)
-
- iw.FinalStyler(func(s *styles.Style) {
- s.Min.X.Dot(p.opts.BodyDots.X)
- s.Min.Y.Dot(p.opts.BodyDots.Y)
- })
- iw.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
- if tx, ok := cwb.This.(*core.Text); ok {
- if tx.Styles.Font.Family == rich.SansSerif {
- if _, ok := cwb.Parent.(*core.Frame); ok { // not inside buttons etc
- cwb.Styler(func(s *styles.Style) {
- s.Font.Family = p.opts.FontFamily
- })
- }
- }
- }
- return true
- })
-
- iw.Scene.StyleTree()
- iw.Scene.LayoutRenderScene()
- }
- // sc.AsyncUnlock()
-}
-
func (p *pager) paginate() {
its := p.extract()
p.layout(its)
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index cd1656fde3..812d6ee0cf 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -12,7 +12,9 @@ import (
"cogentcore.org/core/paint"
"cogentcore.org/core/paint/pdf"
"cogentcore.org/core/paint/renderers/pdfrender"
+ "cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
+ "cogentcore.org/core/text/rich"
"cogentcore.org/core/tree"
)
@@ -72,3 +74,46 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
pdr.EndRender()
pdf.RestorePreviousFonts(cset)
}
+
+// assemble collects everything to be rendered into one big list,
+// and sets the font family and size for text elements.
+// only for full format rendering (e.g., PDF)
+func (p *pager) assemble() {
+ sc := core.AsWidget(p.ins[0]).Scene
+ // sc.AsyncLock()
+ if p.opts.Title != nil {
+ tf := core.NewFrame()
+ tf.Scene = sc
+ tf.FinalStyler(func(s *styles.Style) {
+ s.Min.X.Dot(p.opts.BodyDots.X)
+ s.Min.Y.Dot(p.opts.BodyDots.Y)
+ })
+ p.opts.Title(tf, p.opts)
+ p.ins = append([]core.Widget{tf.This.(core.Widget)}, p.ins...)
+ tf.StyleTree()
+ }
+ for _, in := range p.ins {
+ iw := core.AsWidget(in)
+
+ iw.FinalStyler(func(s *styles.Style) {
+ s.Min.X.Dot(p.opts.BodyDots.X)
+ s.Min.Y.Dot(p.opts.BodyDots.Y)
+ })
+ iw.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
+ if tx, ok := cwb.This.(*core.Text); ok {
+ if tx.Styles.Font.Family == rich.SansSerif {
+ if _, ok := cwb.Parent.(*core.Frame); ok { // not inside buttons etc
+ cwb.Styler(func(s *styles.Style) {
+ s.Font.Family = p.opts.FontFamily
+ })
+ }
+ }
+ }
+ return true
+ })
+
+ iw.Scene.StyleTree()
+ iw.Scene.LayoutRenderScene()
+ }
+ // sc.AsyncUnlock()
+}
From f8dc3f6d9a79298cfc1158bb0e76b6c1f24f6e31 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 09:58:19 +0200
Subject: [PATCH 40/99] pdf: pdf links fully working finally. wow that was an
ordeal. also: made delayed loading of html images optional and off by default
-- content does not at all benefit from this, and it interferes with the PDF
rendering.
---
content/content.go | 2 +-
content/handlers.go | 22 +++-
content/settings.go | 2 +-
core/image.go | 5 +-
core/text.go | 6 ++
htmlcore/context.go | 4 +
htmlcore/handler.go | 142 +++++++++++++++----------
paint/painter.go | 24 +++--
paint/pdf/links.go | 53 +++++++++
paint/pdf/page.go | 77 +-------------
paint/pdf/pdf.go | 8 +-
paint/pdf/pdf_test.go | 23 ++++
paint/pdf/text.go | 5 +-
paint/pdf/writer.go | 106 ++++++++++++++++--
paint/pimage/pimage.go | 5 +
paint/renderers/pdfrender/pdfrender.go | 4 +
text/shaped/lines.go | 5 +
17 files changed, 337 insertions(+), 156 deletions(-)
create mode 100644 paint/pdf/links.go
diff --git a/content/content.go b/content/content.go
index 1ab18de796..343fd4b60a 100644
--- a/content/content.go
+++ b/content/content.go
@@ -484,7 +484,7 @@ func (ct *Content) PagePDF(path string) error {
return errors.Log(errors.New("Page empty"))
}
ct.inPDFRender = true
- ct.Update()
+ ct.reloadPage()
ct.inPDFRender = false
refs := ct.PageRefs(ct.currentPage)
diff --git a/content/handlers.go b/content/handlers.go
index 8e70584524..372d65a9a1 100644
--- a/content/handlers.go
+++ b/content/handlers.go
@@ -81,7 +81,9 @@ func (ct *Content) htmlPreHandler(ctx *htmlcore.Context) bool {
if id != "" {
fr.SetProperty("id", id)
fr.SetName(id)
- tree.MoveToParent(parent.Children[parent.NumChildren()-2], fr) // get title text
+ ttx := parent.Children[parent.NumChildren()-2].(core.Widget)
+ ttx.AsWidget().SetProperty("id", id) // link target
+ tree.MoveToParent(ttx, fr) // get title text
}
if collapsed != "" {
cl := core.NewCollapser(fr)
@@ -208,10 +210,12 @@ func (ct *Content) moveToBlockFrame(w core.Widget, id, txt string, top bool) {
var tx *core.Text
if top {
tx = core.NewText(fr).SetText(txt)
+ tx.SetProperty("id", id) // good link destination
}
tree.MoveToParent(w, fr)
if !top {
tx = core.NewText(fr).SetText(txt)
+ wb.SetProperty("id", id) // link here
}
tx.Styler(func(s *styles.Style) {
s.Max.X.In(8)
@@ -255,7 +259,11 @@ func (ct *Content) citeWikilink(text string) (url string, label string) {
cs = csl.Narrative
ref = ref[1:]
}
- url = "ref://" + ref
+ if ct.inPDFRender {
+ url = "#" + ref
+ } else {
+ url = "ref://" + ref
+ }
if ct.References == nil {
return url, ref
}
@@ -297,6 +305,16 @@ func (ct *Content) mainWikilink(text string) (url string, label string) {
label = name
}
}
+ if ct.inPDFRender {
+ if heading != "" {
+ if pg == ct.currentPage {
+ return "#" + heading, label
+ }
+ return ct.getPrintURL() + "/" + pg.URL + "#" + heading, label
+ }
+ return ct.getPrintURL() + "/" + pg.URL, label
+ }
+
if heading != "" {
return pg.URL + "#" + heading, label
}
diff --git a/content/settings.go b/content/settings.go
index 2a20ba9151..b993e76497 100644
--- a/content/settings.go
+++ b/content/settings.go
@@ -43,7 +43,7 @@ func (s *SettingsData) Defaults() {
ps := &SettingsData{}
*ps = Settings
pt := curPage.Title
- if ps.SiteTitle != "" {
+ if ps.SiteTitle != "" && pt == curPage.Name {
pt = ps.SiteTitle + ": " + pt
}
ps.PDF.Header = paginate.NoFirst(paginate.HeaderLeftPageNumber(pt))
diff --git a/core/image.go b/core/image.go
index 2f69c65549..df20a3b6f0 100644
--- a/core/image.go
+++ b/core/image.go
@@ -109,7 +109,10 @@ func (im *Image) Render() {
rimg = im.Styles.ResizeImage(im.Image, im.Geom.Size.Actual.Content)
im.prevRenderImage = rimg
}
- im.Scene.Painter.DrawImage(rimg, r, sp, draw.Over)
+ pim := im.Scene.Painter.DrawImage(rimg, r, sp, draw.Over)
+ if id, ok := im.Properties["id"]; ok {
+ pim.Anchor = id.(string)
+ }
}
func (im *Image) MakeToolbar(p *tree.Plan) {
diff --git a/core/text.go b/core/text.go
index ff51b3ad30..9ee392df6f 100644
--- a/core/text.go
+++ b/core/text.go
@@ -428,6 +428,9 @@ func (tx *Text) configTextSize(sz math32.Vector2) {
sty, tsty := tx.Styles.NewRichText()
tsty.Align, tsty.AlignV = text.Start, text.Start
tx.paintText = tx.Scene.TextShaper().WrapLines(tx.richText, sty, tsty, sz)
+ if id, ok := tx.Properties["id"]; ok {
+ tx.paintText.Anchor = id.(string)
+ }
}
// configTextAlloc is used for determining how much space the text
@@ -452,6 +455,9 @@ func (tx *Text) configTextAlloc(sz math32.Vector2) math32.Vector2 {
rsz = tx.paintText.Bounds.Size().Ceil()
}
tx.paintText = tsh.WrapLines(tx.richText, sty, tsty, rsz)
+ if id, ok := tx.Properties["id"]; ok {
+ tx.paintText.Anchor = id.(string)
+ }
return tx.paintText.Bounds.Size().Ceil()
}
diff --git a/htmlcore/context.go b/htmlcore/context.go
index f1ed184cbf..f82a7587e1 100644
--- a/htmlcore/context.go
+++ b/htmlcore/context.go
@@ -93,6 +93,10 @@ type Context struct {
// type of widget, for example.
WidgetHandlers []func(w core.Widget)
+ // DelayedImageLoad causes images to be loaded at a delay.
+ // if not set, then they are loaded immediately.
+ DelayedImageLoad bool
+
// firstRow indicates the start of a table, where number of columns is counted.
firstRow bool
}
diff --git a/htmlcore/handler.go b/htmlcore/handler.go
index fdeebf8536..d12210a93d 100644
--- a/htmlcore/handler.go
+++ b/htmlcore/handler.go
@@ -53,12 +53,6 @@ func handleElement(ctx *Context) {
return
}
- pid := ""
- pstyle := ""
- if ctx.BlockParent != nil { // these attributes get put on a block parent element
- pstyle = GetAttr(ctx.Node.Parent, "style")
- pid = GetAttr(ctx.Node.Parent, "id")
- }
var newWidget core.Widget
switch tag {
@@ -219,56 +213,7 @@ func handleElement(ctx *Context) {
readHTMLNode(ctx, ctx.Parent(), sublist)
}
case "img":
- n := ctx.Node
- src := GetAttr(n, "src")
- alt := GetAttr(n, "alt")
- if pstyle != "" {
- ctx.setStyleAttr(n, pstyle)
- }
- // Can be either image or svg.
- var img *core.Image
- var svg *core.SVG
- if strings.HasSuffix(src, ".svg") {
- svg = New[core.SVG](ctx)
- svg.SetTooltip(alt)
- if pid != "" {
- svg.SetName(pid)
- svg.SetProperty("id", pid)
- }
- newWidget = svg
- } else {
- img = New[core.Image](ctx)
- img.SetTooltip(alt)
- if pid != "" {
- img.SetName(pid)
- img.SetProperty("id", pid)
- }
- newWidget = img
- }
-
- go func() {
- resp, err := Get(ctx, src)
- if errors.Log(err) != nil {
- return
- }
- defer resp.Body.Close()
- if svg != nil {
- svg.AsyncLock()
- errors.Log(svg.Read(resp.Body))
- svg.Update()
- svg.AsyncUnlock()
- } else {
- im, _, err := imagex.Read(resp.Body)
- if err != nil {
- slog.Error("error loading image", "url", src, "err", err)
- return
- }
- img.AsyncLock()
- img.SetImage(im)
- img.Update()
- img.AsyncUnlock()
- }
- }()
+ newWidget = handleImage(ctx, tag)
case "input":
ityp := GetAttr(ctx.Node, "type")
val := GetAttr(ctx.Node, "value")
@@ -355,6 +300,91 @@ func handleElement(ctx *Context) {
}
}
+func handleImage(ctx *Context, tag string) core.Widget {
+ pid := ""
+ pstyle := ""
+ if ctx.BlockParent != nil { // these attributes get put on a block parent element
+ pstyle = GetAttr(ctx.Node.Parent, "style")
+ pid = GetAttr(ctx.Node.Parent, "id")
+ }
+ n := ctx.Node
+ src := GetAttr(n, "src")
+ alt := GetAttr(n, "alt")
+ if pstyle != "" {
+ ctx.setStyleAttr(n, pstyle)
+ }
+ var newWidget core.Widget
+ var resp *http.Response
+ var err error
+ if !ctx.DelayedImageLoad {
+ resp, err = Get(ctx, src)
+ if errors.Log(err) != nil {
+ return nil
+ }
+ defer resp.Body.Close()
+ }
+
+ // Can be either image or svg.
+ var img *core.Image
+ var svg *core.SVG
+ if strings.HasSuffix(src, ".svg") {
+ svg = New[core.SVG](ctx)
+ svg.SetTooltip(alt)
+ if pid != "" {
+ svg.SetName(pid)
+ svg.SetProperty("id", pid)
+ }
+ if !ctx.DelayedImageLoad {
+ errors.Log(svg.Read(resp.Body))
+ }
+ newWidget = svg
+ } else {
+ img = New[core.Image](ctx)
+ img.SetTooltip(alt)
+ if pid != "" {
+ img.SetName(pid)
+ img.SetProperty("id", pid)
+ }
+ if !ctx.DelayedImageLoad {
+ im, _, err := imagex.Read(resp.Body)
+ if err != nil {
+ slog.Error("error loading image", "url", src, "err", err)
+ } else {
+ img.SetImage(im)
+ }
+ }
+ newWidget = img
+ }
+
+ if !ctx.DelayedImageLoad {
+ return newWidget
+ }
+ go func() {
+ resp, err := Get(ctx, src)
+ if errors.Log(err) != nil {
+ return
+ }
+ defer resp.Body.Close()
+ if svg != nil {
+ svg.AsyncLock()
+ errors.Log(svg.Read(resp.Body))
+ svg.Update()
+ svg.AsyncUnlock()
+ } else {
+ im, _, err := imagex.Read(resp.Body)
+ if err != nil {
+ slog.Error("error loading image", "url", src, "err", err)
+ return
+ }
+ img.AsyncLock()
+ img.SetImage(im)
+ img.Update()
+ img.AsyncUnlock()
+ }
+ }()
+ return newWidget
+}
+
// handleText creates a new [core.Text] from the given information, setting the text and
// the text click function so that URLs are opened according to [Context.OpenURL].
func handleText(ctx *Context, tag string) *core.Text {
diff --git a/paint/painter.go b/paint/painter.go
index 9e94b1802a..275635e08d 100644
--- a/paint/painter.go
+++ b/paint/painter.go
@@ -508,39 +508,47 @@ func (pc *Painter) SetPixel(x, y int) {
// using the bounds of the source image in rectangle rect, using
// the given draw operration: Over = overlay (alpha blend with destination)
// Src = copy source directly, overwriting destination pixels.
-func (pc *Painter) DrawImage(src image.Image, rect image.Rectangle, srcStart image.Point, op draw.Op) {
- pc.Render.Add(pimage.NewDraw(rect, src, srcStart, op))
+func (pc *Painter) DrawImage(src image.Image, rect image.Rectangle, srcStart image.Point, op draw.Op) *pimage.Params {
+ pim := pimage.NewDraw(rect, src, srcStart, op)
+ pc.Render.Add(pim)
+ return pim
}
// DrawImageAnchored draws the specified image at the specified anchor point.
// The anchor point is x - w * ax, y - h * ay, where w, h is the size of the
// image. Use ax=0.5, ay=0.5 to center the image at the specified point.
-func (pc *Painter) DrawImageAnchored(src image.Image, x, y, ax, ay float32) {
+func (pc *Painter) DrawImageAnchored(src image.Image, x, y, ax, ay float32) *pimage.Params {
s := src.Bounds().Size()
x -= ax * float32(s.X)
y -= ay * float32(s.Y)
m := pc.Transform().Translate(x, y)
+ var pim *pimage.Params
if pc.Mask == nil {
- pc.Render.Add(pimage.NewTransform(m, src.Bounds(), src, draw.Over))
+ pim = pimage.NewTransform(m, src.Bounds(), src, draw.Over)
} else {
- pc.Render.Add(pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{}))
+ pim = pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{})
}
+ pc.Render.Add(pim)
+ return pim
}
// DrawImageScaled draws the specified image starting at given upper-left point,
// such that the size of the image is rendered as specified by w, h parameters
// (an additional scaling is applied to the transform matrix used in rendering)
-func (pc *Painter) DrawImageScaled(src image.Image, x, y, w, h float32) {
+func (pc *Painter) DrawImageScaled(src image.Image, x, y, w, h float32) *pimage.Params {
s := src.Bounds().Size()
isz := math32.FromPoint(s)
isc := math32.Vec2(w, h).Div(isz)
m := pc.Transform().Translate(x, y).Scale(isc.X, isc.Y)
+ var pim *pimage.Params
if pc.Mask == nil {
- pc.Render.Add(pimage.NewTransform(m, src.Bounds(), src, draw.Over))
+ pim = pimage.NewTransform(m, src.Bounds(), src, draw.Over)
} else {
- pc.Render.Add(pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{}))
+ pim = pimage.NewTransformMask(m, src.Bounds(), src, draw.Over, pc.Mask, image.Point{})
}
+ pc.Render.Add(pim)
+ return pim
}
// BoundingBox computes the bounding box for an element in pixel int
diff --git a/paint/pdf/links.go b/paint/pdf/links.go
new file mode 100644
index 0000000000..453e1166fc
--- /dev/null
+++ b/paint/pdf/links.go
@@ -0,0 +1,53 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package pdf
+
+import (
+ "cogentcore.org/core/math32"
+)
+
+// AddAnchor adds a uniquely-named link anchor location.
+// The position is in "default user space" coordinates = standard page coordinates,
+// without the current CTM transform.// This function will handle the base page
+// transform for scaling and flipping of coordinates to top-left system.
+func (w *pdfPage) AddAnchor(name string, pos math32.Vector2) {
+ ms := math32.Scale2D(w.pdf.globalScale, w.pdf.globalScale)
+ pos = ms.MulVector2AsPoint(pos)
+ if w.pdf.anchors == nil {
+ w.pdf.anchors = make(pdfMap)
+ }
+ // pageNo replaced at end with ref:
+ w.pdf.anchors[name] = pdfArray{w.pageNo, pdfName("XYZ"), 0, w.height - pos.Y, 0}
+ // fmt.Println("anchor:", w.pageNo, name, pos)
+}
+
+// AddLink adds a link annotation. The rect is in "default user space"
+// coordinates = standard page coordinates, without the current CTM transform.
+// This function will handle the base page transform for scaling and
+// flipping of coordinates to top-left system.
+func (w *pdfPage) AddLink(uri string, rect math32.Box2) {
+ ms := math32.Scale2D(w.pdf.globalScale, w.pdf.globalScale)
+ rect = rect.MulMatrix2(ms)
+ isLocal := false
+ if uri[0] == '#' { // local anchor actions
+ uri = uri[1:]
+ isLocal = true
+ }
+ annot := pdfDict{
+ "Type": pdfName("Annot"),
+ "Subtype": pdfName("Link"),
+ "Border": pdfArray{0, 0, 0},
+ "Rect": pdfArray{rect.Min.X, w.height - rect.Max.Y, rect.Max.X, w.height - rect.Min.Y},
+ }
+ if isLocal {
+ annot["Dest"] = uri
+ } else {
+ annot["A"] = pdfDict{
+ "S": pdfName("URI"),
+ pdfName("URI"): uri,
+ }
+ }
+ w.annots = append(w.annots, annot)
+}
diff --git a/paint/pdf/page.go b/paint/pdf/page.go
index 2faf53cc32..df087fb87e 100644
--- a/paint/pdf/page.go
+++ b/paint/pdf/page.go
@@ -29,6 +29,7 @@ import (
type pdfPage struct {
*bytes.Buffer
pdf *pdfWriter
+ pageNo int
width, height float32
resources pdfDict
annots pdfArray
@@ -73,27 +74,6 @@ func (w *pdfPage) writePage(parent pdfRef) pdfRef {
return w.pdf.writeObject(page)
}
-// AddAnnotation adds an annotation. The rect is in "default user space"
-// coordinates = standard page coordinates, without the current CTM transform.
-// This function will handle the base page transform for scaling and
-// flipping of coordinates to top-left system.
-func (w *pdfPage) AddURIAction(uri string, rect math32.Box2) {
- ms := math32.Scale2D(w.pdf.globalScale, w.pdf.globalScale)
- rect = rect.MulMatrix2(ms)
- annot := pdfDict{
- "Type": pdfName("Annot"),
- "Subtype": pdfName("Link"),
- "Border": pdfArray{0, 0, 0},
- "Rect": pdfArray{rect.Min.X, w.height - rect.Max.Y, rect.Max.X, w.height - rect.Min.Y},
- "Contents": uri,
- "A": pdfDict{
- "S": pdfName("URI"),
- "URI": uri,
- },
- }
- w.annots = append(w.annots, annot)
-}
-
// SetFill sets the fill style values where different from current.
func (w *pdfPage) SetFill(fill *styles.Fill) {
csty := w.style()
@@ -384,63 +364,8 @@ func (w *pdfPage) WriteText(tx string) error {
fmt.Fprintf(w, ")")
}
- // position := w.textPosition
- // if glyphs, ok := TJ[0].([]canvasText.Glyph); ok && 0 < len(glyphs) && mode != ppath.HorizontalTB && !glyphs[0].Vertical {
- // glyphRotation, glyphOffset := glyphs[0].Rotation(), glyphs[0].YOffset-int32(glyphs[0].SFNT.Head.UnitsPerEm/2)
- // if glyphRotation != canvasText.NoRotation || glyphOffset != 0 {
- // w.SetTextPosition(position.Rotate(float32(glyphRotation)).Translate(0.0, glyphs[0].Size/float32(glyphs[0].SFNT.Head.UnitsPerEm)*mmPerPt*float32(glyphOffset)))
- // }
- // }
-
- // f := 1000.0 / float32(w.font.SFNT.Head.UnitsPerEm)
fmt.Fprintf(w, "[")
write(tx)
-
- // for _, tj := range TJ {
- // switch val := tj.(type) {
- // case []canvasText.Glyph:
- // i := 0
- // for j, glyph := range val {
- // if mode == ppath.HorizontalTB || !glyph.Vertical {
- // origXAdvance := int32(w.font.SFNT.GlyphAdvance(glyph.ID))
- // if glyph.XAdvance != origXAdvance {
- // write(val[i : j+1])
- // fmt.Fprintf(w, " %d", -int(f*float32(glyph.XAdvance-origXAdvance)+0.5))
- // i = j + 1
- // }
- // } else {
- // origYAdvance := -int32(w.font.SFNT.GlyphVerticalAdvance(glyph.ID))
- // if glyph.YAdvance != origYAdvance {
- // write(val[i : j+1])
- // fmt.Fprintf(w, " %d", -int(f*float32(glyph.YAdvance-origYAdvance)+0.5))
- // i = j + 1
- // }
- // }
- // }
- // write(val[i:])
- // case string:
- // i := 0
- // if mode == ppath.HorizontalTB {
- // var rPrev rune
- // for j, r := range val {
- // if i < j {
- // kern := w.font.SFNT.Kerning(w.font.SFNT.GlyphIndex(rPrev), w.font.SFNT.GlyphIndex(r))
- // if kern != 0 {
- // writeString(val[i:j])
- // fmt.Fprintf(w, " %d", -int(f*float32(kern)+0.5))
- // i = j
- // }
- // }
- // rPrev = r
- // }
- // }
- // writeString(val[i:])
- // case float32:
- // fmt.Fprintf(w, " %d", -int(val*1000.0/w.fontSize+0.5))
- // case int:
- // fmt.Fprintf(w, " %d", -int(float32(val)*1000.0/w.fontSize+0.5))
- // }
- // }
fmt.Fprintf(w, "]TJ")
return nil
}
diff --git a/paint/pdf/pdf.go b/paint/pdf/pdf.go
index 640766873c..e4930bcdf9 100644
--- a/paint/pdf/pdf.go
+++ b/paint/pdf/pdf.go
@@ -100,7 +100,7 @@ func (r *PDF) NewPage(width, height float32) {
// AddLink adds a link to the PDF document.
func (r *PDF) AddLink(uri string, rect math32.Box2) {
- r.w.AddURIAction(uri, rect)
+ r.w.AddLink(uri, rect)
}
// Close finished and closes the PDF.
@@ -267,3 +267,9 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
func (r *PDF) Image(img image.Image, m math32.Matrix2) {
r.w.DrawImage(img, m)
}
+
+// AddAnchor adds a uniquely-named link anchor location,
+// which can then be a target for links.
+func (r *PDF) AddAnchor(name string, pos math32.Vector2) {
+ r.w.AddAnchor(name, pos)
+}
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
index 4468a1eadd..00d6e2c02c 100644
--- a/paint/pdf/pdf_test.go
+++ b/paint/pdf/pdf_test.go
@@ -113,3 +113,26 @@ func TestMathDisplay(t *testing.T) {
RestorePreviousFonts(prv)
})
}
+
+func TestLinks(t *testing.T) {
+ RunTest(t, "links", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+ m := math32.Identity2()
+ rsty := &sty.Font
+ tsty := &sty.Text
+ sz := math32.Vec2(250, 250)
+
+ txt := func(src string, pos math32.Vector2) {
+ tx, err := htmltext.HTMLToRich([]byte(src), rsty, nil)
+ assert.NoError(t, err)
+ lns := sh.WrapLines(tx, rsty, tsty, sz)
+ pd.Text(sty, m, pos, lns)
+ }
+ txt("Some random text here", math32.Vec2(10, 10))
+ pd.w.AddAnchor("test", math32.Vec2(10, 10))
+ txt(`A link to that text`, math32.Vec2(10, 30))
+
+ RestorePreviousFonts(prv)
+ })
+}
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
index c8491674f0..514c9c6e8e 100644
--- a/paint/pdf/text.go
+++ b/paint/pdf/text.go
@@ -24,6 +24,9 @@ import (
// Text renders text to the canvas using a transformation matrix,
// (the translation component specifies the starting offset)
func (r *PDF) Text(style *styles.Paint, m math32.Matrix2, pos math32.Vector2, lns *shaped.Lines) {
+ if lns.Anchor != "" {
+ r.w.AddAnchor(lns.Anchor, pos)
+ }
mt := m.Mul(math32.Translate2D(pos.X, pos.Y))
r.w.PushTransform(mt)
off := lns.Offset
@@ -220,6 +223,6 @@ func (r *PDF) links(lns *shaped.Lines, m math32.Matrix2, pos math32.Vector2) {
}
rb := srb.Translate(pos)
rb = rb.MulMatrix2(m)
- r.w.AddURIAction(lk.URL, rb)
+ r.w.AddLink(lk.URL, rb)
}
}
diff --git a/paint/pdf/writer.go b/paint/pdf/writer.go
index 93b4753499..24d57f44e7 100644
--- a/paint/pdf/writer.go
+++ b/paint/pdf/writer.go
@@ -15,6 +15,7 @@ import (
"image"
"io"
"math"
+ "slices"
"sort"
"strings"
"time"
@@ -26,6 +27,7 @@ import (
"cogentcore.org/core/styles/units"
"cogentcore.org/core/text/rich"
"cogentcore.org/core/text/text"
+ "golang.org/x/exp/maps"
)
// TODO: Invalid graphics transparency, Group has a transparency S entry or the S entry is null
@@ -49,6 +51,7 @@ type pdfWriter struct {
// fontsV map[*text.Font]pdfRef
images map[image.Image]pdfRef
layers pdfLayers
+ anchors pdfMap // things that can be linked to within doc
compress bool
subset bool
title string
@@ -76,7 +79,7 @@ func newPDFWriter(writer io.Writer, un *units.Context) *pdfWriter {
w.globalScale = w.unitContext.Convert(1, units.UnitDot, units.UnitPt)
- w.write("%%PDF-1.7\n")
+ w.write("%%PDF-1.7\n%%Ŧǟċơ\n")
return w
}
@@ -142,6 +145,7 @@ type pdfRef int
type pdfName string
type pdfArray []interface{}
type pdfDict map[pdfName]interface{}
+type pdfMap map[string]interface{}
type pdfFilter string
type pdfStream struct {
dict pdfDict
@@ -177,10 +181,7 @@ func (w *pdfWriter) writeVal(i interface{}) {
case float64:
w.write("%v", dec(v))
case string:
- v = strings.Replace(v, `\`, `\\`, -1)
- v = strings.Replace(v, `(`, `\(`, -1)
- v = strings.Replace(v, `)`, `\)`, -1)
- w.write("(%v)", v)
+ w.write("(%v)", escape(v))
case pdfRef:
w.write("%v 0 R", v)
case pdfName, pdfFilter:
@@ -210,9 +211,16 @@ func (w *pdfWriter) writeVal(i interface{}) {
}
w.writeVal(val)
}
+ if val, ok := v["S"]; ok {
+ w.write("/S")
+ if pdfValContinuesName(val) {
+ w.write(" ")
+ }
+ w.writeVal(val)
+ }
keys := []string{}
for key := range v {
- if key != "Type" && key != "Subtype" {
+ if key != "Type" && key != "Subtype" && key != "S" {
keys = append(keys, string(key))
}
}
@@ -225,6 +233,20 @@ func (w *pdfWriter) writeVal(i interface{}) {
w.writeVal(v[pdfName(key)])
}
w.write(">>")
+ case pdfMap:
+ w.write("<<")
+ keys := maps.Keys(v)
+ sort.Strings(keys)
+ nk := len(keys)
+ for i, key := range keys {
+ w.writeVal(pdfName(key))
+ w.write(" ")
+ w.writeVal(v[key])
+ if i < nk-1 {
+ w.write(" ")
+ }
+ }
+ w.write(">>")
case pdfStream:
if v.dict == nil {
v.dict = pdfDict{}
@@ -270,7 +292,7 @@ func (w *pdfWriter) writeVal(i interface{}) {
v.objNum = len(w.objOffsets)
w.write("<>", pdfName(v.name))
default:
- panic(fmt.Sprintf("unknown PDF type %T", i))
+ // panic(fmt.Sprintf("unknown PDF type %T", i))
}
}
@@ -360,7 +382,6 @@ func (w *pdfWriter) getFont(sty *rich.Style, tsty *text.Style) pdfRef {
// Close finished the document.
func (w *pdfWriter) Close() error {
- // TODO: support cross reference table streams and compressed objects for all dicts
if w.page != nil {
w.pages = append(w.pages, w.page.writePage(pdfRef(3)))
}
@@ -378,7 +399,25 @@ func (w *pdfWriter) Close() error {
catalog := pdfDict{
"Type": pdfName("Catalog"),
"Pages": pdfRef(3),
- // TODO: add metadata?
+ }
+
+ if len(w.anchors) > 0 {
+ ancrefs := pdfMap{}
+ var nms []string
+ for nm, v := range w.anchors {
+ vary := v.(pdfArray)
+ vary[0] = w.pages[vary[0].(int)] // replace page no with ref
+ anc := w.writeObject(pdfDict{"D": vary})
+ ancrefs[nm] = anc
+ nms = append(nms, nm)
+ }
+ slices.Sort(nms)
+ var nmary pdfArray
+ for _, nm := range nms {
+ nmary = append(nmary, nm, ancrefs[nm])
+ }
+ nmref := w.writeObject(pdfDict{"Names": nmary})
+ catalog[pdfName("Names")] = pdfDict{"Dests": nmref}
}
// document info
@@ -481,6 +520,7 @@ func (w *pdfWriter) NewPage(width, height float32) *pdfPage {
pdf: w,
width: width,
height: height,
+ pageNo: len(w.pages),
resources: pdfDict{},
graphicsStates: map[float32]pdfName{},
inTextObject: false,
@@ -490,6 +530,7 @@ func (w *pdfWriter) NewPage(width, height float32) *pdfPage {
}
w.page.stack.Push(newContext(styles.NewPaint(), math32.Identity2()))
w.page.setTopTransform()
+ // fmt.Println("added page:", w.page.pageNo)
return w.page
}
@@ -519,3 +560,50 @@ func (f dec) String() string {
func mat2(m math32.Matrix2) string {
return fmt.Sprintf("%v %v %v %v %v %v", dec(m.XX), dec(m.XY), dec(m.YX), dec(m.YY), dec(m.X0), dec(m.Y0))
}
+
+// Escape special characters in strings
+func escape(s string) string {
+ s = strings.Replace(s, "\\", "\\\\", -1)
+ s = strings.Replace(s, "(", "\\(", -1)
+ s = strings.Replace(s, ")", "\\)", -1)
+ s = strings.Replace(s, "\r", "\\r", -1)
+ return s
+}
+
+// utf8toutf16 converts UTF-8 to UTF-16BE; from http://www.fpdf.org/
+func utf8toutf16(s string, withBOM ...bool) string {
+ bom := true
+ if len(withBOM) > 0 {
+ bom = withBOM[0]
+ }
+ res := make([]byte, 0, 8)
+ if bom {
+ res = append(res, 0xFE, 0xFF)
+ }
+ nb := len(s)
+ i := 0
+ for i < nb {
+ c1 := byte(s[i])
+ i++
+ switch {
+ case c1 >= 224:
+ // 3-byte character
+ c2 := byte(s[i])
+ i++
+ c3 := byte(s[i])
+ i++
+ res = append(res, ((c1&0x0F)<<4)+((c2&0x3C)>>2),
+ ((c2&0x03)<<6)+(c3&0x3F))
+ case c1 >= 192:
+ // 2-byte character
+ c2 := byte(s[i])
+ i++
+ res = append(res, ((c1 & 0x1C) >> 2),
+ ((c1&0x03)<<6)+(c2&0x3F))
+ default:
+ // Single-byte character
+ res = append(res, 0, c1)
+ }
+ }
+ return string(res)
+}
diff --git a/paint/pimage/pimage.go b/paint/pimage/pimage.go
index 8225ced717..6068849b5f 100644
--- a/paint/pimage/pimage.go
+++ b/paint/pimage/pimage.go
@@ -65,6 +65,11 @@ type Params struct {
// BlurRadius is the Gaussian standard deviation for Blur function
BlurRadius float32
+
+ // Anchor provides a named link destination associated with this text item.
+ // Used for document navigation, e.g., in PDF rendering. Must be a unique name.
+ // Is set in [core.Text] based on the id property, which is set by [content].
+ Anchor string
}
func (pr *Params) IsRenderItem() {}
diff --git a/paint/renderers/pdfrender/pdfrender.go b/paint/renderers/pdfrender/pdfrender.go
index 5a191f80ae..c42b8d64b3 100644
--- a/paint/renderers/pdfrender/pdfrender.go
+++ b/paint/renderers/pdfrender/pdfrender.go
@@ -156,6 +156,10 @@ func (rs *Renderer) RenderImage(pr *pimage.Params) {
pr.Rect = image.Rectangle{Max: rs.size.ToPoint()}
}
+ if pr.Anchor != "" {
+ rs.PDF.AddAnchor(pr.Anchor, math32.FromPoint(pr.Rect.Min))
+ }
+
// todo: handle masks!
// Fast path for [image.Uniform]
diff --git a/text/shaped/lines.go b/text/shaped/lines.go
index dca22faeed..876682d7b4 100644
--- a/text/shaped/lines.go
+++ b/text/shaped/lines.go
@@ -65,6 +65,11 @@ type Lines struct {
// HighlightColor is the color to use for rendering highlighted regions.
HighlightColor image.Image
+
+ // Anchor provides a named link destination associated with this text item.
+ // Used for document navigation, e.g., in PDF rendering. Must be a unique name.
+ // Is set in [core.Text] based on the id property, which is set by [content].
+ Anchor string
}
// Line is one line of shaped text, containing multiple Runs.
From 9e7ab299656c0d9f979bc3c9b2c311dea2cdcace Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 09:59:35 +0200
Subject: [PATCH 41/99] pdf: content set DelayedImageLoad = false regardless of
current default, in case it changes
---
content/content.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/content/content.go b/content/content.go
index 343fd4b60a..157c65f2ca 100644
--- a/content/content.go
+++ b/content/content.go
@@ -129,6 +129,7 @@ func (ct *Content) Init() {
ct.SetSplits(0.2, 0.8)
ct.Context = htmlcore.NewContext()
+ ct.Context.DelayedImageLoad = false // not useful for content
ct.Context.OpenURL = func(url string) {
ct.Open(url)
}
From d79315cf05889553e73909bfcb3e1f8f3d549f52 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 12:11:41 +0200
Subject: [PATCH 42/99] pdf: csl generates html formatted text for references,
including links etc. all good.
---
content/content.go | 5 +++++
text/csl/apa.go | 6 +++---
text/csl/keylist.go | 6 ++++--
text/csl/md.go | 4 +++-
text/htmltext/tohtml.go | 27 ++++++++++++++++++++-------
text/paginate/pdf.go | 5 -----
6 files changed, 35 insertions(+), 18 deletions(-)
diff --git a/content/content.go b/content/content.go
index 157c65f2ca..1afe30dd26 100644
--- a/content/content.go
+++ b/content/content.go
@@ -527,12 +527,17 @@ func (ct *Content) PageRefs(page *bcontent.Page) *core.Frame {
if errors.Log(err) != nil {
return nil
}
+ // os.WriteFile("tmp-refs.md", b.Bytes(), 0666)
fr := core.NewFrame()
+ fr.Styler(func(s *styles.Style) {
+ s.Direction = styles.Column
+ })
err = htmlcore.ReadMD(ct.Context, fr, b.Bytes())
if errors.Log(err) != nil {
return nil
}
+ fr.StyleTree()
fr.SetScene(ct.Scene)
return fr
}
diff --git a/text/csl/apa.go b/text/csl/apa.go
index d1278e4c67..a25ac8e63d 100644
--- a/text/csl/apa.go
+++ b/text/csl/apa.go
@@ -72,8 +72,8 @@ func RefLinks(it *Item, tx *rich.Text) {
tx.AddLink(link, it.URL, it.URL)
}
if it.DOI != "" {
- url := " http://doi.org/" + it.DOI
- tx.AddLink(link, url, url)
+ url := "http://doi.org/" + it.DOI
+ tx.AddLink(link, url, " "+url)
}
}
@@ -118,7 +118,7 @@ func RefAPABook(it *Item) rich.Text {
}
}
if it.Publisher != "" {
- tx.AddSpanString(sty, EnsurePeriod(it.Publisher)+" ")
+ tx.AddSpanString(sty, " "+EnsurePeriod(it.Publisher)+" ")
}
RefLinks(it, &tx)
return tx
diff --git a/text/csl/keylist.go b/text/csl/keylist.go
index 69415be250..fdb08e7f58 100644
--- a/text/csl/keylist.go
+++ b/text/csl/keylist.go
@@ -28,10 +28,12 @@ func NewKeyList(items []Item) *KeyList {
return kl
}
-// AlphaKeys returns an alphabetically sorted list of keys.
+// AlphaKeys returns an alphabetically sorted list of keys (case insensitive).
func (kl *KeyList) AlphaKeys() []string {
ks := slices.Clone(kl.Keys)
- slices.Sort(ks)
+ slices.SortFunc(ks, func(a, b string) int {
+ return strings.Compare(strings.ToLower(a), strings.ToLower(b))
+ })
return ks
}
diff --git a/text/csl/md.go b/text/csl/md.go
index ed5d0d2a72..00d5c10a3b 100644
--- a/text/csl/md.go
+++ b/text/csl/md.go
@@ -12,6 +12,7 @@ import (
"strings"
"cogentcore.org/core/base/errors"
+ "cogentcore.org/core/text/htmltext"
)
// GenerateMarkdown extracts markdown citations in the format [@Ref; @Ref]
@@ -88,7 +89,8 @@ func WriteRefsMarkdown(w io.Writer, kl *KeyList, sty Styles) error {
if err != nil {
return err
}
- _, err = w.Write([]byte(string(ref.Join()) + "
")
-func mdToHTML(ctx *Context, md []byte) []byte {
+func MDToHTML(ctx *Context, md []byte) []byte {
// create markdown parser with extensions
extensions := parser.CommonExtensions | parser.AutoHeadingIDs | parser.NoEmptyLineBeforeBlock | parser.Attributes | parser.Mmark
p := parser.NewWithExtensions(extensions)
@@ -43,7 +43,7 @@ func mdToHTML(ctx *Context, md []byte) []byte {
// ReadMD reads MD (markdown) from the given bytes and adds corresponding
// Cogent Core widgets to the given [core.Widget], using the given context.
func ReadMD(ctx *Context, parent core.Widget, b []byte) error {
- htm := mdToHTML(ctx, b)
+ htm := MDToHTML(ctx, b)
// os.WriteFile("htmlcore_tmp.html", htm, 0666) // note: keep here, needed for debugging
buf := bytes.NewBuffer(htm)
return ReadHTML(ctx, parent, buf)
From 77499310ebd85ca3e439a07a2c33aa2b6708589c Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 14:44:43 +0200
Subject: [PATCH 44/99] pdf: fixed remaining pdf incompatibility issues: now
fully compliant! layer stuff was very broken -- now has a test but is not
used by default b/c basically meaningless. but is there as an option. last
major issue is the gradients (and then embedded fonts eventually)
---
content/settings.go | 1 -
paint/pdf/layer.go | 84 +++++++++-----------------
paint/pdf/page.go | 4 +-
paint/pdf/pdf.go | 44 ++++++++++----
paint/pdf/pdf_test.go | 25 ++++++++
paint/pdf/writer.go | 22 +++++--
paint/renderers/pdfrender/pdfrender.go | 32 +++++-----
7 files changed, 123 insertions(+), 89 deletions(-)
diff --git a/content/settings.go b/content/settings.go
index b993e76497..d2e8183381 100644
--- a/content/settings.go
+++ b/content/settings.go
@@ -47,7 +47,6 @@ func (s *SettingsData) Defaults() {
pt = ps.SiteTitle + ": " + pt
}
ps.PDF.Header = paginate.NoFirst(paginate.HeaderLeftPageNumber(pt))
- // todo: add affiliations and abstract
ur := ct.getPrintURL() + "/" + curPage.URL
ura := `` + ur + ``
dt := ""
diff --git a/paint/pdf/layer.go b/paint/pdf/layer.go
index e2e7879ee1..8132a86f41 100644
--- a/paint/pdf/layer.go
+++ b/paint/pdf/layer.go
@@ -14,7 +14,8 @@ import "fmt"
type pdfLayer struct {
name string
visible bool
- objNum int // object number
+ index int
+ ref pdfRef
}
// pdfLayers is all the layers
@@ -33,33 +34,43 @@ func (w *pdfWriter) layerInit() {
// AddLayer defines a layer that can be shown or hidden when the document is
// displayed. name specifies the layer name that the document reader will
// display in the layer list. visible specifies whether the layer will be
-// initially visible. The return value is an integer ID that is used in a call
-// to BeginLayer().
-func (w *pdfWriter) AddLayer(name string, visible bool) (layerID int) {
- layerID = len(w.layers.list)
- w.layers.list = append(w.layers.list, pdfLayer{name: name, visible: visible})
- return
+// initially visible. The return value is unique layer index that must be
+// used in subsequent calls to BeginLayer.
+func (w *pdfWriter) AddLayer(name string, visible bool) int {
+ layno := len(w.layers.list)
+ lay := pdfLayer{name: name, visible: visible, index: layno}
+ lay.ref = w.writeObject(pdfDict{"Type": pdfName("OCG"), "Name": name})
+ w.layers.list = append(w.layers.list, lay)
+ return layno
}
-// BeginLayer is called to begin adding content to the specified layer. All
-// content added to the page between a call to BeginLayer and a call to
-// EndLayer is added to the layer specified by id. See AddLayer for more
-// details.
-func (w *pdfWriter) BeginLayer(id int) {
+// BeginLayer is called to begin adding content to the specified layer index
+// in a given page. Layer must have already been added via AddLayer.
+// All content added to the page between a call to BeginLayer and a call to
+// EndLayer is added to the layer specified by id.
+func (w *pdfPage) BeginLayer(id int) {
w.EndLayer()
- if id >= 0 && id < len(w.layers.list) {
- w.write("/OC /OC%d BDC", id)
- w.layers.currentLayer = id
+ if id < 0 || id >= len(w.pdf.layers.list) {
+ return
+ }
+ ocn := fmt.Sprintf("oc%d", id)
+ fmt.Fprintf(w, " /OC /%s BDC", ocn)
+ w.pdf.layers.currentLayer = id
+ if _, ok := w.resources["Properties"]; !ok {
+ w.resources["Properties"] = pdfDict{}
}
+ l := &w.pdf.layers.list[id]
+ w.resources["Properties"].(pdfDict)[pdfName(ocn)] = l.ref
}
// EndLayer is called to stop adding content to the currently active layer.
// See BeginLayer for more details.
-func (w *pdfWriter) EndLayer() {
- if w.layers.currentLayer >= 0 {
- w.write(" EMC")
- w.layers.currentLayer = -1
+func (w *pdfPage) EndLayer() {
+ if w.pdf.layers.currentLayer < 0 {
+ return
}
+ fmt.Fprintf(w, " EMC")
+ w.pdf.layers.currentLayer = -1
}
// OpenLayerPane advises the document reader to open the layer pane when the
@@ -67,38 +78,3 @@ func (w *pdfWriter) EndLayer() {
func (w *pdfWriter) OpenLayerPane() {
w.layers.openLayerPane = true
}
-
-func (w *pdfWriter) writeLayers() {
- for _, l := range w.layers.list {
- w.writeObject(&l)
- }
-}
-
-func (w *pdfWriter) writeLayerResourceDict() {
- if len(w.layers.list) == 0 {
- return
- }
- w.write("/Properties <<")
- for j, layer := range w.layers.list {
- w.write("/OC%d %d 0 R", j, layer.objNum)
- }
- w.write(">>")
-}
-
-func (w *pdfWriter) writeLayerCatalog() {
- if len(w.layers.list) == 0 {
- return
- }
- onStr := ""
- offStr := ""
- for _, layer := range w.layers.list {
- onStr += fmt.Sprintf("%d 0 R ", layer.objNum)
- if !layer.visible {
- offStr += fmt.Sprintf("%d 0 R ", layer.objNum)
- }
- }
- w.write("/OCProperties <>>>", onStr, offStr, onStr)
- if w.layers.openLayerPane {
- w.write("/PageMode /UseOC")
- }
-}
diff --git a/paint/pdf/page.go b/paint/pdf/page.go
index df087fb87e..66d12fba33 100644
--- a/paint/pdf/page.go
+++ b/paint/pdf/page.go
@@ -103,7 +103,9 @@ func (w *pdfPage) SetFillColor(fill *styles.Fill) {
clr = colors.ApplyOpacity(colors.AsRGBA(x), fill.Opacity)
}
a := float32(clr.A) / 255.0
- if clr.R == clr.G && clr.R == clr.B {
+ if a == 0 {
+ fmt.Fprintf(w, " 0 g")
+ } else if clr.R == clr.G && clr.R == clr.B {
fmt.Fprintf(w, " %v g", dec(float32(clr.R)/255.0/a))
} else {
fmt.Fprintf(w, " %v %v %v rg", dec(float32(clr.R)/255.0/a), dec(float32(clr.G)/255.0/a), dec(float32(clr.B)/255.0/a))
diff --git a/paint/pdf/pdf.go b/paint/pdf/pdf.go
index e4930bcdf9..f1efff2347 100644
--- a/paint/pdf/pdf.go
+++ b/paint/pdf/pdf.go
@@ -122,24 +122,42 @@ func (r *PDF) AddLayer(name string, visible bool) (layerID int) {
return r.w.pdf.AddLayer(name, visible)
}
-// BeginLayer is called to begin adding content to the specified layer. All
-// content added to the page between a call to BeginLayer and a call to
+// BeginLayer is called to begin adding content to the specified layer.
+// All content added to the page between a call to BeginLayer and a call to
// EndLayer is added to the layer specified by id. See AddLayer for more
-// details. Also adds a graphics stack push, and sets the given transform
-// matrix, if not identity.
-func (r *PDF) BeginLayer(id int, m math32.Matrix2) {
- r.w.pdf.BeginLayer(id)
- r.w.PushStack()
- if !m.IsIdentity() {
- r.w.SetTransform(m)
- }
+// details.
+func (r *PDF) BeginLayer(id int) {
+ r.w.BeginLayer(id)
}
-// EndLayer is called to stop adding content to the currently active layer. See
-// BeginLayer for more details.
+// EndLayer is called to stop adding content to the currently active layer.
+// See BeginLayer for more details.
func (r *PDF) EndLayer() {
+ r.w.EndLayer()
+}
+
+// PushStack adds a graphics stack push, which must
+// be paired with a corresponding Pop.
+func (r *PDF) PushStack() {
+ r.w.PushStack()
+}
+
+// PopStack adds a graphics stack pop which must
+// be paired with a corresponding Push.
+func (r *PDF) PopStack() {
r.w.PopStack()
- r.w.pdf.EndLayer()
+}
+
+// SetTransform adds a cm to set the current matrix transform (CMT).
+func (r *PDF) SetTransform(m math32.Matrix2) {
+ r.w.SetTransform(m)
+}
+
+// PushTransform adds a graphics stack push (q) and then
+// cm to set the current matrix transform (CMT).
+func (r *PDF) PushTransform(m math32.Matrix2) {
+ r.PushStack()
+ r.SetTransform(m)
}
// Path renders a path to the canvas using a style and a transformation matrix.
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
index 00d6e2c02c..0a37b0039c 100644
--- a/paint/pdf/pdf_test.go
+++ b/paint/pdf/pdf_test.go
@@ -136,3 +136,28 @@ func TestLinks(t *testing.T) {
RestorePreviousFonts(prv)
})
}
+
+func TestLayers(t *testing.T) {
+ RunTest(t, "layers", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+
+ src := "PDF can put HTML formatted Text where you want"
+ rsty := &sty.Font
+ tsty := &sty.Text
+
+ tx, err := htmltext.HTMLToRich([]byte(src), rsty, nil)
+ // fmt.Println(tx)
+ assert.NoError(t, err)
+ lns := sh.WrapLines(tx, rsty, tsty, math32.Vec2(250, 250))
+
+ // mi := math32.Identity2()
+ mr := math32.Rotate2D(math32.DegToRad(15))
+
+ g := pd.AddLayer("first layer", true)
+ pd.BeginLayer(g)
+ pd.Text(sty, mr, math32.Vec2(20, 20), lns)
+ pd.EndLayer()
+ RestorePreviousFonts(prv)
+ })
+}
diff --git a/paint/pdf/writer.go b/paint/pdf/writer.go
index 24d57f44e7..0ec17e6f29 100644
--- a/paint/pdf/writer.go
+++ b/paint/pdf/writer.go
@@ -288,9 +288,6 @@ func (w *pdfWriter) writeVal(i interface{}) {
w.write("stream\n")
w.writeBytes(b)
w.write("\nendstream\n")
- case *pdfLayer:
- v.objNum = len(w.objOffsets)
- w.write("<>", pdfName(v.name))
default:
// panic(fmt.Sprintf("unknown PDF type %T", i))
}
@@ -420,6 +417,23 @@ func (w *pdfWriter) Close() error {
catalog[pdfName("Names")] = pdfDict{"Dests": nmref}
}
+ if len(w.layers.list) > 0 {
+ var refs, off pdfArray
+ for _, l := range w.layers.list {
+ refs = append(refs, l.ref)
+ if !l.visible {
+ off = append(off, l.ref)
+ }
+ }
+ catalog[pdfName("OCProperties")] = pdfDict{
+ "OCGs": refs,
+ "D": pdfDict{"OFF": off, "Order": refs},
+ }
+ if w.layers.openLayerPane {
+ catalog[pdfName("PageMode")] = pdfName("UseOC")
+ }
+ }
+
// document info
info := pdfDict{
"Producer": "cogentcore/pdf",
@@ -472,14 +486,12 @@ func (w *pdfWriter) Close() error {
w.objOffsets[0] = w.pos
w.write("%v 0 obj\n", 1)
w.writeVal(catalog)
- w.writeLayerCatalog()
w.write("\nendobj\n")
// document info
w.objOffsets[1] = w.pos
w.write("%v 0 obj\n", 2)
w.writeVal(info)
- w.writeLayerResourceDict()
w.write("\nendobj\n")
// page tree
diff --git a/paint/renderers/pdfrender/pdfrender.go b/paint/renderers/pdfrender/pdfrender.go
index c42b8d64b3..a029b83e0d 100644
--- a/paint/renderers/pdfrender/pdfrender.go
+++ b/paint/renderers/pdfrender/pdfrender.go
@@ -8,7 +8,6 @@ import (
"bytes"
"image"
"io"
- "strconv"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/base/stack"
@@ -30,8 +29,8 @@ type Renderer struct {
buff *bytes.Buffer
- // lyStack is a stack of layers used while building the pdf (int layer id)
- lyStack stack.Stack[int]
+ // gStack is a stack of graphic state context in the PDF
+ gStack stack.Stack[int]
}
func New(size math32.Vector2, un *units.Context) render.Renderer {
@@ -77,7 +76,7 @@ func (rs *Renderer) StartRender(w io.Writer) {
sx := rs.unitContext.Convert(float32(rs.size.X), rs.sizeUnits, units.UnitPt)
sy := rs.unitContext.Convert(float32(rs.size.Y), rs.sizeUnits, units.UnitPt)
rs.PDF = pdf.New(w, sx, sy, &rs.unitContext)
- rs.lyStack = nil
+ rs.gStack = nil
}
// EndRender finishes the render
@@ -110,18 +109,21 @@ func (rs *Renderer) RenderPage(r render.Render) {
}
}
-func (rs *Renderer) PushLayer(m math32.Matrix2) int {
- cg := rs.lyStack.Peek()
- nm := strconv.Itoa(cg + 1)
- g := rs.PDF.AddLayer(nm, true)
- rs.PDF.BeginLayer(g, m)
- rs.lyStack.Push(g)
+func (rs *Renderer) PushTransform(m math32.Matrix2) int {
+ cg := rs.gStack.Peek()
+ g := cg + 1
+ if m.IsIdentity() {
+ rs.PDF.PushStack()
+ } else {
+ rs.PDF.PushTransform(m)
+ }
+ rs.gStack.Push(g)
return g
}
-func (rs *Renderer) PopLayer() int {
- cg := rs.lyStack.Pop()
- rs.PDF.EndLayer()
+func (rs *Renderer) PopStack() int {
+ cg := rs.gStack.Pop()
+ rs.PDF.PopStack()
return cg
}
@@ -132,11 +134,11 @@ func (rs *Renderer) RenderPath(pt *render.Path) {
}
func (rs *Renderer) PushContext(pt *render.ContextPush) {
- rs.PushLayer(pt.Context.Transform)
+ rs.PushTransform(pt.Context.Transform)
}
func (rs *Renderer) PopContext(pt *render.ContextPop) {
- rs.PopLayer()
+ rs.PopStack()
}
func (rs *Renderer) RenderText(pt *render.Text) {
From 76471ba7af1c788932094f36f02c4f7fee117258 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 15:02:35 +0200
Subject: [PATCH 45/99] pdf: README with validator link
---
paint/pdf/README.md | 9 +++++++++
1 file changed, 9 insertions(+)
create mode 100644 paint/pdf/README.md
diff --git a/paint/pdf/README.md b/paint/pdf/README.md
new file mode 100644
index 0000000000..ab60489d9f
--- /dev/null
+++ b/paint/pdf/README.md
@@ -0,0 +1,9 @@
+# PDF rendering
+
+This package adapts the PDF rendering code from https://github.com/tdewolff/canvas (Copyright (c) 2015 Taco de Wolff, under an MIT License.) to the Cogent Core rendering framework. It is used for rendering PDFs of content documentation, producing publication-ready output from markdown input. It is also used by the Cogent Canvas SVG drawing app to render PDFs of SVGs.
+
+
+## Helpful links
+
+* Validator: https://www.pdf-online.com/osa/validate.aspx
+
From c4dc38c832269efa31a4c08350da77bf2708c5b4 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 16:16:04 +0200
Subject: [PATCH 46/99] use ImageRenderer in pdf -- key for web
---
core/events.go | 2 +-
core/render_notjs.go | 2 +-
core/rendersource.go | 4 ++--
core/scene.go | 10 +++++-----
paint/pdf/README.md | 2 ++
text/paginate/layout.go | 7 +------
text/paginate/paginate.go | 22 ++++++++++++++++++++++
text/paginate/pdf.go | 7 +------
8 files changed, 35 insertions(+), 21 deletions(-)
diff --git a/core/events.go b/core/events.go
index 399f52261c..9d3fc70f83 100644
--- a/core/events.go
+++ b/core/events.go
@@ -1190,7 +1190,7 @@ func (em *Events) managerKeyChordEvents(e events.Event) {
e.SetHandled()
}
case keymap.WinSnapshot:
- img := sc.renderer.Image()
+ img := sc.Renderer.Image()
dstr := time.Now().Format(time.DateOnly + "-" + "15-04-05")
var sz string
if img != nil {
diff --git a/core/render_notjs.go b/core/render_notjs.go
index e159d98c23..c03fb03b41 100644
--- a/core/render_notjs.go
+++ b/core/render_notjs.go
@@ -20,7 +20,7 @@ import (
// If it returns nil, then the image could not be fetched.
func grabRenderFrom(w Widget) *image.RGBA {
wb := w.AsWidget()
- scimg := wb.Scene.renderer.Image()
+ scimg := wb.Scene.Renderer.Image()
if scimg == nil {
return nil
}
diff --git a/core/rendersource.go b/core/rendersource.go
index 42943e7500..b7f9bffa52 100644
--- a/core/rendersource.go
+++ b/core/rendersource.go
@@ -20,11 +20,11 @@ import (
// SceneSource returns a [composer.Source] for the given scene
// using the given suggested draw operation.
func SceneSource(sc *Scene, op draw.Op) composer.Source {
- if sc.Painter.State == nil || sc.renderer == nil {
+ if sc.Painter.State == nil || sc.Renderer == nil {
return nil
}
render := sc.Painter.RenderDone()
- return &paintSource{render: render, renderer: sc.renderer, drawOp: op, drawPos: sc.SceneGeom.Pos}
+ return &paintSource{render: render, renderer: sc.Renderer, drawOp: op, drawPos: sc.SceneGeom.Pos}
}
// paintSource is the [composer.Source] for [paint.Painter] content, such as for a [Scene].
diff --git a/core/scene.go b/core/scene.go
index 01727c1644..8a5d271df6 100644
--- a/core/scene.go
+++ b/core/scene.go
@@ -86,7 +86,7 @@ type Scene struct { //core:no-new
selectedWidgetChan chan Widget `json:"-" xml:"-"`
// source renderer for rendering the scene
- renderer render.Renderer `copier:"-" json:"-" xml:"-" display:"-" set:"-"`
+ Renderer render.Renderer `copier:"-" json:"-" xml:"-" display:"-" set:"-"`
// lastRender captures key params from last render.
// If different then a new ApplyStyleScene is needed.
@@ -305,8 +305,8 @@ func (sc *Scene) Resize(geom math32.Geom2DInt) bool {
sc.Painter.Paint.UnitContext = sc.Styles.UnitContext
}
sc.SceneGeom.Pos = geom.Pos
- if sc.renderer != nil {
- img := sc.renderer.Image()
+ if sc.Renderer != nil {
+ img := sc.Renderer.Image()
if img != nil {
isz := img.Bounds().Size()
if isz == geom.Size {
@@ -314,11 +314,11 @@ func (sc *Scene) Resize(geom math32.Geom2DInt) bool {
}
}
} else {
- sc.renderer = paint.NewSourceRenderer(sz)
+ sc.Renderer = paint.NewSourceRenderer(sz)
}
sc.Painter.Paint.UnitContext = sc.Styles.UnitContext
sc.Painter.State.Init(sc.Painter.Paint, sz)
- sc.renderer.SetSize(units.UnitDot, sz)
+ sc.Renderer.SetSize(units.UnitDot, sz)
sc.SceneGeom.Size = geom.Size // make sure
sc.updateScene()
diff --git a/paint/pdf/README.md b/paint/pdf/README.md
index ab60489d9f..294eca8de7 100644
--- a/paint/pdf/README.md
+++ b/paint/pdf/README.md
@@ -5,5 +5,7 @@ This package adapts the PDF rendering code from https://github.com/tdewolff/canv
## Helpful links
+* Current 2.0 Standard: https://pdfa.org/sponsored-standards/
+* Version 1.2: https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/pdfreference1.2.pdf
* Validator: https://www.pdf-online.com/osa/validate.aspx
diff --git a/text/paginate/layout.go b/text/paginate/layout.go
index 6df9d26aa5..85df16c464 100644
--- a/text/paginate/layout.go
+++ b/text/paginate/layout.go
@@ -116,12 +116,7 @@ func (p *pager) preRender(its []*item) {
page, body = fr, fr
return
})
- sc := core.NewScene()
- sz := math32.Geom2DInt{}
- sz.Size = p.opts.SizeDots.ToPointCeil()
- sc.Resize(sz)
- sc.MakeTextShaper()
-
+ sc := p.offScene()
tree.MoveToParent(op[0], sc)
op[0].SetScene(sc)
sc.StyleTree()
diff --git a/text/paginate/paginate.go b/text/paginate/paginate.go
index 28dc11d75c..8e5181aac3 100644
--- a/text/paginate/paginate.go
+++ b/text/paginate/paginate.go
@@ -6,6 +6,8 @@ package paginate
import (
"cogentcore.org/core/core"
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/paint"
"cogentcore.org/core/styles/units"
_ "cogentcore.org/core/text/tex"
)
@@ -32,6 +34,7 @@ type pager struct {
outs []*core.Frame
ctx units.Context
+ sc *core.Scene
}
// optsUpdate updates the option sizes based on unit context in first input.
@@ -46,3 +49,22 @@ func (p *pager) paginate() {
its := p.extract()
p.layout(its)
}
+
+// offScene returns a scene suitable for offline rendering,
+// sized to the full page size.
+func (p *pager) offScene() *core.Scene {
+ if p.sc != nil {
+ p.sc.DeleteChildren()
+ return p.sc
+ }
+ sz := p.opts.SizeDots
+ sc := core.NewScene()
+ // critical to be image not htmlcanvas, and not to be the correct size yet..
+ sc.Renderer = paint.NewImageRenderer(sz.SubScalar(1))
+ scsz := math32.Geom2DInt{}
+ scsz.Size = sz.ToPointCeil()
+ sc.Resize(scsz)
+ sc.MakeTextShaper()
+ p.sc = sc
+ return sc
+}
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index d91cadd825..eb53c012d1 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -8,7 +8,6 @@ import (
"io"
"cogentcore.org/core/core"
- "cogentcore.org/core/math32"
"cogentcore.org/core/paint"
"cogentcore.org/core/paint/pdf"
"cogentcore.org/core/paint/renderers/pdfrender"
@@ -36,11 +35,7 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
p.assemble()
p.paginate()
- sc := core.NewScene()
- sz := math32.Geom2DInt{}
- sz.Size = opts.SizeDots.ToPointCeil()
- sc.Resize(sz)
- sc.MakeTextShaper()
+ sc := p.offScene()
pdr := paint.NewPDFRenderer(opts.SizeDots, &p.ctx).(*pdfrender.Renderer)
pdr.StartRender(w)
From a6148817eb35f8edbcf2a44fc4e4bd374fe5983d Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 16:24:34 +0200
Subject: [PATCH 47/99] pdf: use the right base url
---
content/url_js.go | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/content/url_js.go b/content/url_js.go
index c8b194b5b0..0b885b455a 100644
--- a/content/url_js.go
+++ b/content/url_js.go
@@ -26,7 +26,10 @@ var (
OfflineURL = ""
)
-func (ct *Content) getPrintURL() string { return ct.getWebURL() }
+func (ct *Content) getPrintURL() string {
+ p := originalBase.String()
+ return p[:len(p)-1]
+}
// getWebURL returns the current relative web URL that should be passed to [Content.Open]
// on startup and in [Content.handleWebPopState].
From 368bd53c8cc8df28fe34a5fed89dc568d13511c1 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 17:40:24 +0200
Subject: [PATCH 48/99] pdf: fix for web-based PDF generation: images were not
available b/c of optimized wrapping -- undoing that for special case of Scene
that uses rasterx renderer on web.
---
base/iox/imagex/wrapjs_js.go | 6 +++++
base/iox/imagex/wrapjs_notjs.go | 6 +++++
content/examples/basic/content/func-button.md | 11 +++++++++
content/handlers.go | 2 +-
core/image.go | 21 ++++++++++++++++-
htmlcore/context.go | 6 ++---
styles/object.go | 23 +++++++++++++++++++
7 files changed, 70 insertions(+), 5 deletions(-)
diff --git a/base/iox/imagex/wrapjs_js.go b/base/iox/imagex/wrapjs_js.go
index 3b4b2c93a4..97a5107f9d 100644
--- a/base/iox/imagex/wrapjs_js.go
+++ b/base/iox/imagex/wrapjs_js.go
@@ -130,6 +130,12 @@ func (im *JSRGBA) Update() {
im.JS.SetRGBA(im.RGBA)
}
+// note: per https://github.com/whatwg/html/issues/4785 there is no way
+// to actually get the data back from the ImageBitmap :(
+
+// Underlying returns the underlying image data as a local image.RGBA.
+// Note that this may not be possible and a nil will be returned, because
+// there is no way to get the image data back from an ImageBitmap at this point.
func (im *JSRGBA) Underlying() image.Image {
return im.RGBA
}
diff --git a/base/iox/imagex/wrapjs_notjs.go b/base/iox/imagex/wrapjs_notjs.go
index ccaff60e33..39e8836dd3 100644
--- a/base/iox/imagex/wrapjs_notjs.go
+++ b/base/iox/imagex/wrapjs_notjs.go
@@ -13,6 +13,12 @@ import (
"github.com/anthonynsimon/bild/transform"
)
+// JSRGBA is a dummy version of this type so code does not have to be
+// conditionalized.
+type JSRGBA struct {
+ *image.RGBA
+}
+
// WrapJS returns a JavaScript optimized wrapper around the given
// [image.Image] on web, and just returns the image on other platforms.
func WrapJS(src image.Image) image.Image {
diff --git a/content/examples/basic/content/func-button.md b/content/examples/basic/content/func-button.md
index 1608e9533c..559e2f8333 100644
--- a/content/examples/basic/content/func-button.md
+++ b/content/examples/basic/content/func-button.md
@@ -3,3 +3,14 @@ Categories = ["Widgets"]
+++
A **func button** is a [[button]] that is [[value binding|bound]] to a function.
+
+{id="figure_one" style="height=10em"}
+
+
+As you can see in [[#figure_one]], the data turned out just as expected. However, you can never be too careful, because along comes [[#figure_two]], and now look.
+
+{id="figure_two" style="height=5em"}
+
+
+That just goes to show: science is always interesting.
+
diff --git a/content/handlers.go b/content/handlers.go
index 45ea7c6f9d..0d83d010cc 100644
--- a/content/handlers.go
+++ b/content/handlers.go
@@ -245,7 +245,7 @@ func (ct *Content) widgetHandlerFigure(w core.Widget, id string) {
}
altf := htmlcore.MDToHTML(ct.Context, []byte(alt))
lbl := ct.currentPage.SpecialLabel(id)
- lbf := "" + lbl + ": " + string(altf) + "
"
+ lbf := "" + lbl + ": " + string(altf) + "
"
ct.moveToBlockFrame(w, id, lbf, false)
}
diff --git a/core/image.go b/core/image.go
index df20a3b6f0..e02b1a901f 100644
--- a/core/image.go
+++ b/core/image.go
@@ -11,8 +11,10 @@ import (
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/icons"
"cogentcore.org/core/math32"
+ "cogentcore.org/core/paint/renderers/rasterx"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
+ "cogentcore.org/core/system"
"cogentcore.org/core/tree"
"golang.org/x/image/draw"
)
@@ -99,6 +101,19 @@ func (im *Image) Render() {
}
sp := im.Geom.ScrollOffset()
+ useLocalResize := false
+ if TheApp.Platform() == system.Web && im.Scene != nil {
+ // we are using an image renderer, e.g. PDF, offline rendering.
+ // note that there is no way to get transformed image back from javascript :(
+ // which would be the cleaner solution here.
+ if _, ok := im.Scene.Renderer.(*rasterx.Renderer); ok {
+ if _, ok := im.prevRenderImage.(*imagex.JSRGBA); ok {
+ im.prevImage = nil // clear
+ }
+ useLocalResize = true
+ }
+ }
+
var rimg image.Image
if im.prevImage == im.Image && im.Styles.ObjectFit == im.prevObjectFit && im.Geom.Size.Actual.Content == im.prevSize {
rimg = im.prevRenderImage
@@ -106,7 +121,11 @@ func (im *Image) Render() {
im.prevImage = im.Image
im.prevObjectFit = im.Styles.ObjectFit
im.prevSize = im.Geom.Size.Actual.Content
- rimg = im.Styles.ResizeImage(im.Image, im.Geom.Size.Actual.Content)
+ if useLocalResize {
+ rimg = im.Styles.ResizeImageLocal(im.Image, im.Geom.Size.Actual.Content)
+ } else {
+ rimg = im.Styles.ResizeImage(im.Image, im.Geom.Size.Actual.Content)
+ }
im.prevRenderImage = rimg
}
pim := im.Scene.Painter.DrawImage(rimg, r, sp, draw.Over)
diff --git a/htmlcore/context.go b/htmlcore/context.go
index f82a7587e1..647a7e859d 100644
--- a/htmlcore/context.go
+++ b/htmlcore/context.go
@@ -163,9 +163,9 @@ func (c *Context) config(w core.Widget) {
func (c *Context) setStyleAttr(node *html.Node, style string) error {
// our CSS parser is strict about semicolons, but
// they aren't needed in normal inline styles in HTML
- if !strings.HasSuffix(style, ";") {
- style += ";"
- }
+ // if !strings.HasSuffix(style, ";") {
+ // style += ";"
+ // }
decls, err := parser.ParseDeclarations(style)
if errors.Log(err) != nil {
return err
diff --git a/styles/object.go b/styles/object.go
index 083bb579a3..c8fc3d7ae6 100644
--- a/styles/object.go
+++ b/styles/object.go
@@ -6,9 +6,11 @@ package styles
import (
"image"
+ "image/draw"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/math32"
+ "github.com/anthonynsimon/bild/transform"
)
// ObjectFits are the different ways in which a replaced element
@@ -96,3 +98,24 @@ func (s *Style) ResizeImage(img image.Image, box math32.Vector2) image.Image {
drect := image.Rect(0, 0, int(min(sz.X, box.X)), int(min(sz.Y, box.Y)))
return imagex.Crop(rimg, drect)
}
+
+// ResizeImageLocal resizes the given image according to [Style.ObjectFit]
+// in an object of the given box size. This version uses local Go image
+// resizing regardless of what platform, for use with local Image rendering.
+func (s *Style) ResizeImageLocal(img image.Image, box math32.Vector2) image.Image {
+ obj := math32.FromPoint(img.Bounds().Size())
+ sz := ObjectSizeFromFit(s.ObjectFit, obj, box)
+ if s.ObjectFit == FitScaleDown && sz.X >= obj.X {
+ return img
+ }
+ szi := sz.ToPointFloor()
+ rimg := transform.Resize(img, szi.X, szi.Y, transform.Linear)
+ if s.ObjectFit != FitCover || (box.X >= sz.X && box.Y >= sz.Y) {
+ return rimg
+ }
+ // need to crop the destination size to the size of the containing object
+ drect := image.Rect(0, 0, int(min(sz.X, box.X)), int(min(sz.Y, box.Y)))
+ dst := image.NewRGBA(drect)
+ draw.Draw(dst, drect, rimg, image.Point{}, draw.Src)
+ return dst
+}
From 92d841479c19701a27a4a678005295ffb3360ac3 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 9 Oct 2025 18:29:11 +0200
Subject: [PATCH 49/99] pdf: yeah that semicolon thing is real. what wasn't
real was my bad style syntax in test.
---
content/examples/basic/content/func-button.md | 4 ++--
htmlcore/context.go | 6 +++---
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/content/examples/basic/content/func-button.md b/content/examples/basic/content/func-button.md
index 559e2f8333..3ce04aab9c 100644
--- a/content/examples/basic/content/func-button.md
+++ b/content/examples/basic/content/func-button.md
@@ -4,12 +4,12 @@ Categories = ["Widgets"]
A **func button** is a [[button]] that is [[value binding|bound]] to a function.
-{id="figure_one" style="height=10em"}
+{id="figure_one" style="height:10em"}

As you can see in [[#figure_one]], the data turned out just as expected. However, you can never be too careful, because along comes [[#figure_two]], and now look.
-{id="figure_two" style="height=5em"}
+{id="figure_two" style="height:5em"}

That just goes to show: science is always interesting.
diff --git a/htmlcore/context.go b/htmlcore/context.go
index 647a7e859d..f82a7587e1 100644
--- a/htmlcore/context.go
+++ b/htmlcore/context.go
@@ -163,9 +163,9 @@ func (c *Context) config(w core.Widget) {
func (c *Context) setStyleAttr(node *html.Node, style string) error {
// our CSS parser is strict about semicolons, but
// they aren't needed in normal inline styles in HTML
- // if !strings.HasSuffix(style, ";") {
- // style += ";"
- // }
+ if !strings.HasSuffix(style, ";") {
+ style += ";"
+ }
decls, err := parser.ParseDeclarations(style)
if errors.Log(err) != nil {
return err
From 17c7fa4ca4558c6ce110053c738d202185933142 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Fri, 10 Oct 2025 06:42:49 +0200
Subject: [PATCH 50/99] pdf: parse/golang: prevent recursive type loop from
crashing
---
content/content.go | 2 ++
text/parse/languages/golang/types.go | 11 +++++++++++
2 files changed, 13 insertions(+)
diff --git a/content/content.go b/content/content.go
index 1afe30dd26..7dc1dce54e 100644
--- a/content/content.go
+++ b/content/content.go
@@ -484,6 +484,8 @@ func (ct *Content) PagePDF(path string) error {
if ct.currentPage == nil {
return errors.Log(errors.New("Page empty"))
}
+ core.MessageSnackbar(ct, "Generating PDF...")
+
ct.inPDFRender = true
ct.reloadPage()
ct.inPDFRender = false
diff --git a/text/parse/languages/golang/types.go b/text/parse/languages/golang/types.go
index dcfca1ea12..ca3864336c 100644
--- a/text/parse/languages/golang/types.go
+++ b/text/parse/languages/golang/types.go
@@ -291,8 +291,19 @@ func (gl *GoLang) TypeFromASTPrim(fs *parse.FileState, pkg *syms.Symbol, ty *sym
return nil, false
}
+var typeDepth int
+
// TypeFromASTComp handles composite type processing
func (gl *GoLang) TypeFromASTComp(fs *parse.FileState, pkg *syms.Symbol, ty *syms.Type, tyast *parser.AST) (*syms.Type, bool) {
+ typeDepth++
+ if typeDepth > 100 {
+ typeDepth = 0
+ return nil, false
+ }
+ defer func() {
+ typeDepth--
+ }()
+
tnm := gl.ASTTypeName(tyast)
newTy := false
if ty == nil {
From dea8cd12140b4c0a17766512cd5914d56dbe8aab Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Fri, 10 Oct 2025 07:54:56 +0200
Subject: [PATCH 51/99] pdf: printer.Settings handles all the standard printer
settings, plugs into global settings system where needed (call
core.AddPrinterSettings())
---
content/settings.go | 2 +
core/settings.go | 12 +++
go.mod | 7 +-
go.sum | 4 +-
system/app.go | 4 +
system/driver/base/app.go | 5 ++
system/locale.go | 31 +++++++
text/paginate/options.go | 32 +-------
text/paginate/pagesizes/enumgen.go | 46 -----------
text/paginate/paginate.go | 1 -
text/paginate/pdf.go | 1 -
text/printer/enumgen.go | 50 ++++++++++++
.../pagesizes => printer}/pagesizes.go | 29 ++++---
text/printer/settings.go | 81 +++++++++++++++++++
text/printer/typegen.go | 13 +++
15 files changed, 225 insertions(+), 93 deletions(-)
create mode 100644 system/locale.go
delete mode 100644 text/paginate/pagesizes/enumgen.go
create mode 100644 text/printer/enumgen.go
rename text/{paginate/pagesizes => printer}/pagesizes.go (78%)
create mode 100644 text/printer/settings.go
create mode 100644 text/printer/typegen.go
diff --git a/content/settings.go b/content/settings.go
index d2e8183381..a6c9a47c5a 100644
--- a/content/settings.go
+++ b/content/settings.go
@@ -8,11 +8,13 @@ import (
"time"
"cogentcore.org/core/content/bcontent"
+ "cogentcore.org/core/core"
"cogentcore.org/core/text/paginate"
"cogentcore.org/core/text/rich"
)
func init() {
+ core.AddPrinterSettings()
Settings.Defaults()
}
diff --git a/core/settings.go b/core/settings.go
index 34adf26649..9e427a2ede 100644
--- a/core/settings.go
+++ b/core/settings.go
@@ -11,6 +11,7 @@ import (
"os/user"
"path/filepath"
"reflect"
+ "slices"
"time"
"cogentcore.org/core/base/errors"
@@ -27,6 +28,7 @@ import (
"cogentcore.org/core/icons"
"cogentcore.org/core/keymap"
"cogentcore.org/core/system"
+ "cogentcore.org/core/text/printer"
"cogentcore.org/core/text/rich"
"cogentcore.org/core/text/text"
"cogentcore.org/core/tree"
@@ -38,6 +40,16 @@ import (
// app settings.
var AllSettings = []Settings{AppearanceSettings, SystemSettings, TimingSettings, DebugSettings}
+// AddAppSettings inserts app-specific settings after the AppearanceSettings
+func AddAppSettings(sets ...Settings) {
+ AllSettings = slices.Insert(AllSettings, 1, sets...)
+}
+
+// AddPrinterSettings adds printer settings, for apps that use these.
+func AddPrinterSettings() {
+ AddAppSettings(&printer.Settings)
+}
+
// Settings is the interface that describes the functionality common
// to all settings data types.
type Settings interface {
diff --git a/go.mod b/go.mod
index 335965af9b..4de227f8ee 100644
--- a/go.mod
+++ b/go.mod
@@ -3,11 +3,11 @@ module cogentcore.org/core
go 1.23.4
require (
- codeberg.org/go-pdf/fpdf v0.11.1
github.com/Bios-Marcel/wastebasket/v2 v2.0.3
github.com/Masterminds/semver/v3 v3.2.1
github.com/Masterminds/vcs v1.13.3
github.com/adrg/strutil v0.3.1
+ github.com/alecthomas/assert/v2 v2.6.0
github.com/alecthomas/chroma/v2 v2.13.0
github.com/anthonynsimon/bild v0.13.0
github.com/aymerick/douceur v0.2.0
@@ -29,6 +29,7 @@ require (
github.com/h2non/filetype v1.1.3
github.com/hack-pad/hackpadfs v0.2.1
github.com/jackmordaunt/icns/v2 v2.2.7
+ github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade
github.com/jinzhu/copier v0.4.0
github.com/mattn/go-shellwords v1.0.12
github.com/mitchellh/go-homedir v1.1.0
@@ -42,12 +43,14 @@ require (
golang.org/x/net v0.38.0
golang.org/x/oauth2 v0.27.0
golang.org/x/sys v0.31.0
+ golang.org/x/text v0.23.0
golang.org/x/tools v0.29.0
gopkg.in/yaml.v3 v3.0.1
star-tex.org/x/tex v0.6.0
)
require (
+ github.com/alecthomas/repr v0.4.0 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dlclark/regexp2 v1.11.0 // indirect
@@ -57,6 +60,7 @@ require (
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
github.com/hack-pad/safejs v0.1.1 // indirect
github.com/hajimehoshi/oto v0.7.1 // indirect
+ github.com/hexops/gotextdiff v1.0.3 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
@@ -69,7 +73,6 @@ require (
golang.org/x/mobile v0.0.0-20231127183840-76ac6878050a // indirect
golang.org/x/mod v0.22.0 // indirect
golang.org/x/sync v0.12.0 // indirect
- golang.org/x/text v0.23.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
modernc.org/knuth v0.5.4 // indirect
modernc.org/token v1.1.0 // indirect
diff --git a/go.sum b/go.sum
index ecfd6fdc63..4d1a3ed256 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,3 @@
-codeberg.org/go-pdf/fpdf v0.11.1 h1:U8+coOTDVLxHIXZgGvkfQEi/q0hYHYvEHFuGNX2GzGs=
-codeberg.org/go-pdf/fpdf v0.11.1/go.mod h1:Y0DGRAdZ0OmnZPvjbMp/1bYxmIPxm0ws4tfoPOc4LjU=
github.com/Bios-Marcel/wastebasket/v2 v2.0.3 h1:TkoDPcSqluhLGE+EssHu7UGmLgUEkWg7kNyHyyJ3Q9g=
github.com/Bios-Marcel/wastebasket/v2 v2.0.3/go.mod h1:769oPCv6eH7ugl90DYIsWwjZh4hgNmMS3Zuhe1bH6KU=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
@@ -102,6 +100,8 @@ github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBD
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jackmordaunt/icns/v2 v2.2.7 h1:K/RbfvuzjmjVY5y4g+XENRs8ZZatwz4YnLHypa2KwQg=
github.com/jackmordaunt/icns/v2 v2.2.7/go.mod h1:ovoTxGguSuoUGKMk5Nn3R7L7BgMQkylsO+bblBuI22A=
+github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
+github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
github.com/jfreymuth/oggvorbis v1.0.1/go.mod h1:NqS+K+UXKje0FUYUPosyQ+XTVvjmVjps1aEZH1sumIk=
github.com/jfreymuth/vorbis v1.0.0/go.mod h1:8zy3lUAm9K/rJJk223RKy6vjCZTWC61NA2QD06bfOE0=
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
diff --git a/system/app.go b/system/app.go
index 2a3754ccfb..f5c0eccceb 100644
--- a/system/app.go
+++ b/system/app.go
@@ -61,6 +61,10 @@ type App interface {
// returns the platform of the underlying operating system.
SystemPlatform() Platforms
+ // SystemLocale returns the https://www.rfc-editor.org/rfc/bcp/bcp47.txt standard
+ // language tag, consisting of language and region, e.g., "en-US", "fr-FR", "ja-JP".
+ SystemLocale() Locale
+
// SystemInfo returns any additional information about the underlying system
// that is not otherwise given. It is used in crash logs.
SystemInfo() string
diff --git a/system/driver/base/app.go b/system/driver/base/app.go
index 11969dcdbf..f3248d59fa 100644
--- a/system/driver/base/app.go
+++ b/system/driver/base/app.go
@@ -20,6 +20,7 @@ import (
"cogentcore.org/core/events/key"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
+ "github.com/jeandeaual/go-locale"
)
// App contains the data and logic common to all implementations of [system.App].
@@ -119,6 +120,10 @@ func (a *App) SystemInfo() string {
return "" // no-op by default
}
+func (a *App) SystemLocale() system.Locale {
+ return system.Locale(errors.Log1(locale.GetLocale()))
+}
+
func (a *App) Name() string {
return a.Nm
}
diff --git a/system/locale.go b/system/locale.go
new file mode 100644
index 0000000000..e8f47afc70
--- /dev/null
+++ b/system/locale.go
@@ -0,0 +1,31 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package system
+
+import "strings"
+
+// Locale represents a https://www.rfc-editor.org/rfc/bcp/bcp47.txt standard
+// language tag, consisting of language and region, e.g., "en-US", "fr-FR", "ja-JP".
+type Locale string
+
+// Language returns the language portion of the locale tag (e.g., en, fr, ja)
+func (l Locale) Language() string {
+ if l == "" {
+ return ""
+ }
+ return strings.Split(string(l), "-")[0]
+}
+
+// Region returns the region portion of the locale tag (e.g., US, FR, JA)
+func (l Locale) Region() string {
+ if l == "" {
+ return ""
+ }
+ pos := strings.LastIndex(string(l), "-")
+ if pos < 0 {
+ return ""
+ }
+ return string(l)[:pos]
+}
diff --git a/text/paginate/options.go b/text/paginate/options.go
index e86e7ef2b7..b7de958483 100644
--- a/text/paginate/options.go
+++ b/text/paginate/options.go
@@ -11,26 +11,12 @@ import (
"cogentcore.org/core/math32"
"cogentcore.org/core/styles/sides"
"cogentcore.org/core/styles/units"
- "cogentcore.org/core/text/paginate/pagesizes"
+ "cogentcore.org/core/text/printer"
"cogentcore.org/core/text/rich"
)
// Options has the parameters for pagination.
type Options struct {
- // PageSize specifies a standard page size, or Custom.
- PageSize pagesizes.Sizes
-
- // Units are the units in which size is specified.
- // Will automatically be set if PageSize != Custom.
- Units units.Units
-
- // Size is the size in given units.
- // Will automatically be set if PageSize != Custom.
- Size math32.Vector2
-
- // Margins specify the page margins in the size units.
- Margins sides.Floats `display:"inline"`
-
// FontFamily specifies the default font family to apply
// to all core.Text elements.
FontFamily rich.Family
@@ -71,23 +57,9 @@ func NewOptions() Options {
}
func (o *Options) Defaults() {
- // todo: make this contingent on localization somehow!
- o.PageSize = pagesizes.A4
- o.Margins.Set(25) // basically one inch
o.Footer = CenteredPageNumber
- o.Update()
-}
-
-func (o *Options) Update() {
- if o.PageSize != pagesizes.Custom {
- o.Units, o.Size = o.PageSize.Size()
- }
}
func (o *Options) ToDots(un *units.Context) {
- sc := un.ToDots(1, o.Units)
- o.SizeDots = o.Size.MulScalar(sc)
- o.MargDots = o.Margins.MulScalar(sc)
- o.BodyDots.X = o.SizeDots.X - (o.MargDots.Left + o.MargDots.Right)
- o.BodyDots.Y = o.SizeDots.Y - (o.MargDots.Top + o.MargDots.Bottom)
+ o.SizeDots, o.BodyDots, o.MargDots = printer.Settings.ToDots(un)
}
diff --git a/text/paginate/pagesizes/enumgen.go b/text/paginate/pagesizes/enumgen.go
deleted file mode 100644
index 0d3a4b3edb..0000000000
--- a/text/paginate/pagesizes/enumgen.go
+++ /dev/null
@@ -1,46 +0,0 @@
-// Code generated by "core generate"; DO NOT EDIT.
-
-package pagesizes
-
-import (
- "cogentcore.org/core/enums"
-)
-
-var _SizesValues = []Sizes{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21}
-
-// SizesN is the highest valid value for type Sizes, plus one.
-const SizesN Sizes = 22
-
-var _SizesValueMap = map[string]Sizes{`Custom`: 0, `Img1280x720`: 1, `Img1920x1080`: 2, `Img3840x2160`: 3, `Img7680x4320`: 4, `Img1024x768`: 5, `Img720x480`: 6, `Img640x480`: 7, `Img320x240`: 8, `A4`: 9, `USLetter`: 10, `USLegal`: 11, `A0`: 12, `A1`: 13, `A2`: 14, `A3`: 15, `A5`: 16, `A6`: 17, `A7`: 18, `A8`: 19, `A9`: 20, `A10`: 21}
-
-var _SizesDescMap = map[Sizes]string{0: `Custom = nonstandard`, 1: `Image 1280x720 Px = 720p`, 2: `Image 1920x1080 Px = 1080p HD`, 3: `Image 3840x2160 Px = 4K`, 4: `Image 7680x4320 Px = 8K`, 5: `Image 1024x768 Px = XGA`, 6: `Image 720x480 Px = DVD`, 7: `Image 640x480 Px = VGA`, 8: `Image 320x240 Px = old CRT`, 9: `A4 = 210 x 297 mm`, 10: `USLetter = 8.5 x 11 in = 612 x 792 pt`, 11: `USLegal = 8.5 x 14 in = 612 x 1008 pt`, 12: `A0 = 841 x 1189 mm`, 13: `A1 = 594 x 841 mm`, 14: `A2 = 420 x 594 mm`, 15: `A3 = 297 x 420 mm`, 16: `A5 = 148 x 210 mm`, 17: `A6 = 105 x 148 mm`, 18: `A7 = 74 x 105`, 19: `A8 = 52 x 74 mm`, 20: `A9 = 37 x 52`, 21: `A10 = 26 x 37`}
-
-var _SizesMap = map[Sizes]string{0: `Custom`, 1: `Img1280x720`, 2: `Img1920x1080`, 3: `Img3840x2160`, 4: `Img7680x4320`, 5: `Img1024x768`, 6: `Img720x480`, 7: `Img640x480`, 8: `Img320x240`, 9: `A4`, 10: `USLetter`, 11: `USLegal`, 12: `A0`, 13: `A1`, 14: `A2`, 15: `A3`, 16: `A5`, 17: `A6`, 18: `A7`, 19: `A8`, 20: `A9`, 21: `A10`}
-
-// String returns the string representation of this Sizes value.
-func (i Sizes) String() string { return enums.String(i, _SizesMap) }
-
-// SetString sets the Sizes value from its string representation,
-// and returns an error if the string is invalid.
-func (i *Sizes) SetString(s string) error { return enums.SetString(i, s, _SizesValueMap, "Sizes") }
-
-// Int64 returns the Sizes value as an int64.
-func (i Sizes) Int64() int64 { return int64(i) }
-
-// SetInt64 sets the Sizes value from an int64.
-func (i *Sizes) SetInt64(in int64) { *i = Sizes(in) }
-
-// Desc returns the description of the Sizes value.
-func (i Sizes) Desc() string { return enums.Desc(i, _SizesDescMap) }
-
-// SizesValues returns all possible values for the type Sizes.
-func SizesValues() []Sizes { return _SizesValues }
-
-// Values returns all possible values for the type Sizes.
-func (i Sizes) Values() []enums.Enum { return enums.Values(_SizesValues) }
-
-// MarshalText implements the [encoding.TextMarshaler] interface.
-func (i Sizes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
-
-// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
-func (i *Sizes) UnmarshalText(text []byte) error { return enums.UnmarshalText(i, text, "Sizes") }
diff --git a/text/paginate/paginate.go b/text/paginate/paginate.go
index 8e5181aac3..cd5ae79e70 100644
--- a/text/paginate/paginate.go
+++ b/text/paginate/paginate.go
@@ -39,7 +39,6 @@ type pager struct {
// optsUpdate updates the option sizes based on unit context in first input.
func (p *pager) optsUpdate() {
- p.opts.Update()
in0 := p.ins[0].AsWidget()
p.ctx = in0.Styles.UnitContext
p.opts.ToDots(&p.ctx)
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index eb53c012d1..9f0879b3db 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -29,7 +29,6 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
cset := pdf.UseStandardFonts()
p := pager{opts: &opts, ins: ins}
- p.opts.Update()
p.ctx = *units.NewContext() // generic, invariant of actual context
p.opts.ToDots(&p.ctx)
p.assemble()
diff --git a/text/printer/enumgen.go b/text/printer/enumgen.go
new file mode 100644
index 0000000000..4d0ad4add2
--- /dev/null
+++ b/text/printer/enumgen.go
@@ -0,0 +1,50 @@
+// Code generated by "core generate -add-types"; DO NOT EDIT.
+
+package printer
+
+import (
+ "cogentcore.org/core/enums"
+)
+
+var _PageSizesValues = []PageSizes{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21}
+
+// PageSizesN is the highest valid value for type PageSizes, plus one.
+const PageSizesN PageSizes = 22
+
+var _PageSizesValueMap = map[string]PageSizes{`Custom`: 0, `Img1280x720`: 1, `Img1920x1080`: 2, `Img3840x2160`: 3, `Img7680x4320`: 4, `Img1024x768`: 5, `Img720x480`: 6, `Img640x480`: 7, `Img320x240`: 8, `A4`: 9, `USLetter`: 10, `USLegal`: 11, `A0`: 12, `A1`: 13, `A2`: 14, `A3`: 15, `A5`: 16, `A6`: 17, `A7`: 18, `A8`: 19, `A9`: 20, `A10`: 21}
+
+var _PageSizesDescMap = map[PageSizes]string{0: `Custom = nonstandard`, 1: `Image 1280x720 Px = 720p`, 2: `Image 1920x1080 Px = 1080p HD`, 3: `Image 3840x2160 Px = 4K`, 4: `Image 7680x4320 Px = 8K`, 5: `Image 1024x768 Px = XGA`, 6: `Image 720x480 Px = DVD`, 7: `Image 640x480 Px = VGA`, 8: `Image 320x240 Px = old CRT`, 9: `A4 = 210 x 297 mm`, 10: `USLetter = 8.5 x 11 in = 612 x 792 pt`, 11: `USLegal = 8.5 x 14 in = 612 x 1008 pt`, 12: `A0 = 841 x 1189 mm`, 13: `A1 = 594 x 841 mm`, 14: `A2 = 420 x 594 mm`, 15: `A3 = 297 x 420 mm`, 16: `A5 = 148 x 210 mm`, 17: `A6 = 105 x 148 mm`, 18: `A7 = 74 x 105`, 19: `A8 = 52 x 74 mm`, 20: `A9 = 37 x 52`, 21: `A10 = 26 x 37`}
+
+var _PageSizesMap = map[PageSizes]string{0: `Custom`, 1: `Img1280x720`, 2: `Img1920x1080`, 3: `Img3840x2160`, 4: `Img7680x4320`, 5: `Img1024x768`, 6: `Img720x480`, 7: `Img640x480`, 8: `Img320x240`, 9: `A4`, 10: `USLetter`, 11: `USLegal`, 12: `A0`, 13: `A1`, 14: `A2`, 15: `A3`, 16: `A5`, 17: `A6`, 18: `A7`, 19: `A8`, 20: `A9`, 21: `A10`}
+
+// String returns the string representation of this PageSizes value.
+func (i PageSizes) String() string { return enums.String(i, _PageSizesMap) }
+
+// SetString sets the PageSizes value from its string representation,
+// and returns an error if the string is invalid.
+func (i *PageSizes) SetString(s string) error {
+ return enums.SetString(i, s, _PageSizesValueMap, "PageSizes")
+}
+
+// Int64 returns the PageSizes value as an int64.
+func (i PageSizes) Int64() int64 { return int64(i) }
+
+// SetInt64 sets the PageSizes value from an int64.
+func (i *PageSizes) SetInt64(in int64) { *i = PageSizes(in) }
+
+// Desc returns the description of the PageSizes value.
+func (i PageSizes) Desc() string { return enums.Desc(i, _PageSizesDescMap) }
+
+// PageSizesValues returns all possible values for the type PageSizes.
+func PageSizesValues() []PageSizes { return _PageSizesValues }
+
+// Values returns all possible values for the type PageSizes.
+func (i PageSizes) Values() []enums.Enum { return enums.Values(_PageSizesValues) }
+
+// MarshalText implements the [encoding.TextMarshaler] interface.
+func (i PageSizes) MarshalText() ([]byte, error) { return []byte(i.String()), nil }
+
+// UnmarshalText implements the [encoding.TextUnmarshaler] interface.
+func (i *PageSizes) UnmarshalText(text []byte) error {
+ return enums.UnmarshalText(i, text, "PageSizes")
+}
diff --git a/text/paginate/pagesizes/pagesizes.go b/text/printer/pagesizes.go
similarity index 78%
rename from text/paginate/pagesizes/pagesizes.go
rename to text/printer/pagesizes.go
index d8a3ab77c1..abd5f185c2 100644
--- a/text/paginate/pagesizes/pagesizes.go
+++ b/text/printer/pagesizes.go
@@ -2,12 +2,7 @@
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
-//go:generate core generate
-
-// package pagesizes provides an enum of standard page sizes
-// including image (e.g., 1080p, 4K, etc) and printed page sizes
-// (e.g., A4, USLetter).
-package pagesizes
+package printer
import (
"cogentcore.org/core/math32"
@@ -15,11 +10,11 @@ import (
)
// Sizes are standard physical drawing sizes
-type Sizes int32 //enums:enum
+type PageSizes int32 //enums:enum
const (
// Custom = nonstandard
- Custom Sizes = iota
+ Custom PageSizes = iota
// Image 1280x720 Px = 720p
Img1280x720
@@ -86,13 +81,13 @@ const (
)
// Size returns the corresponding size values and units.
-func (s Sizes) Size() (un units.Units, size math32.Vector2) {
+func (s PageSizes) Size() (un units.Units, size math32.Vector2) {
v := sizesMap[s]
return v.un, math32.Vec2(v.x, v.y)
}
// Match returns a matching standard size for given units and dimension.
-func Match(un units.Units, wd, ht float32) Sizes {
+func Match(un units.Units, wd, ht float32) PageSizes {
trgl := values{un: un, x: wd, y: ht}
trgp := values{un: un, x: ht, y: wd}
for k, v := range sizesMap {
@@ -111,7 +106,7 @@ type values struct {
}
// sizesMap is the map of size values for each standard size
-var sizesMap = map[Sizes]*values{
+var sizesMap = map[PageSizes]*values{
Img1280x720: {units.UnitPx, 1280, 720},
Img1920x1080: {units.UnitPx, 1920, 1080},
Img3840x2160: {units.UnitPx, 3840, 2160},
@@ -134,3 +129,15 @@ var sizesMap = map[Sizes]*values{
A9: {units.UnitMm, 37, 52},
A10: {units.UnitMm, 26, 37},
}
+
+// DefaultPageSizeForRegion returns the default page size based on locale
+// region value, per the https://www.rfc-editor.org/rfc/bcp/bcp47.txt standard.
+// See [system.Locale] and [system.App] for how to get this.
+func DefaultPageSizeForRegion(region string) PageSizes {
+ switch region {
+ case "US", "CA", "BZ", "CL", "CR", "GT", "NI", "PA", "PR", "SV", "VE", "MX", "CO", "PH":
+ return USLetter
+ default:
+ return A4
+ }
+}
diff --git a/text/printer/settings.go b/text/printer/settings.go
new file mode 100644
index 0000000000..a4b2dea5cc
--- /dev/null
+++ b/text/printer/settings.go
@@ -0,0 +1,81 @@
+// Copyright (c) 2025, Cogent Core. 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 core generate -add-types
+
+// package printer provides standard printer settings.
+package printer
+
+import (
+ "path/filepath"
+
+ "cogentcore.org/core/math32"
+ "cogentcore.org/core/styles/sides"
+ "cogentcore.org/core/styles/units"
+ "cogentcore.org/core/system"
+ "cogentcore.org/core/tree"
+)
+
+func init() {
+ Settings.Defaults()
+}
+
+// Settings provides the default printer settings.
+var Settings SettingsData
+
+// Settings has standard printer settings.
+type SettingsData struct {
+ // PageSize specifies a standard page size, or Custom.
+ PageSize PageSizes
+
+ // Units are the units in which size is specified.
+ // Will automatically be set if PageSize != Custom.
+ Units units.Units
+
+ // Size is the size in given units.
+ // Will automatically be set if PageSize != Custom.
+ Size math32.Vector2
+
+ // Margins specify the page margins in the size units.
+ Margins sides.Floats `display:"inline"`
+}
+
+func (ps *SettingsData) Defaults() {
+ ps.PageSize = DefaultPageSizeForRegion(system.TheApp.SystemLocale().Region())
+ ps.Margins.Set(25) // basically one inch
+ ps.Update()
+}
+
+func (ps *SettingsData) Update() {
+ if ps.PageSize != Custom {
+ ps.Units, ps.Size = ps.PageSize.Size()
+ }
+}
+
+func (ps *SettingsData) Apply() {
+ ps.Update()
+}
+
+func (ps *SettingsData) Label() string {
+ return "Printer"
+}
+
+func (ps *SettingsData) Filename() string {
+ return filepath.Join(system.TheApp.CogentCoreDataDir(), "printer-settings.toml")
+}
+
+func (ps *SettingsData) MakeToolbar(p *tree.Plan) {
+}
+
+// ToDots returns the measurement values in rendering dots (actual pixels)
+// based on the given units context.
+// size = page size; body = content area inside margins
+func (ps *SettingsData) ToDots(un *units.Context) (size, body math32.Vector2, margins sides.Floats) {
+ sc := un.ToDots(1, ps.Units)
+ size = ps.Size.MulScalar(sc)
+ margins = ps.Margins.MulScalar(sc)
+ body.X = size.X - (margins.Left + margins.Right)
+ body.Y = size.Y - (margins.Top + margins.Bottom)
+ return
+}
diff --git a/text/printer/typegen.go b/text/printer/typegen.go
new file mode 100644
index 0000000000..2c56e39cdc
--- /dev/null
+++ b/text/printer/typegen.go
@@ -0,0 +1,13 @@
+// Code generated by "core generate -add-types"; DO NOT EDIT.
+
+package printer
+
+import (
+ "cogentcore.org/core/types"
+)
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/printer.PageSizes", IDName: "page-sizes", Doc: "Sizes are standard physical drawing sizes"})
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/printer.values", IDName: "values", Doc: "values are values for standard sizes", Fields: []types.Field{{Name: "un"}, {Name: "x"}, {Name: "y"}}})
+
+var _ = types.AddType(&types.Type{Name: "cogentcore.org/core/text/printer.SettingsData", IDName: "settings-data", Doc: "Settings has standard printer settings.", Fields: []types.Field{{Name: "PageSize", Doc: "PageSize specifies a standard page size, or Custom."}, {Name: "Units", Doc: "Units are the units in which size is specified.\nWill automatically be set if PageSize != Custom."}, {Name: "Size", Doc: "Size is the size in given units.\nWill automatically be set if PageSize != Custom."}, {Name: "Margins", Doc: "Margins specify the page margins in the size units."}}})
From 7c550bb0bc971c8afd3681a9f0bfe114f922ec29 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Fri, 10 Oct 2025 10:41:44 +0200
Subject: [PATCH 52/99] pdf: misc settings fixes -- all good
---
system/driver/base/app.go | 4 ++++
system/locale.go | 8 ++++++--
text/printer/settings.go | 27 +++++++++++++++++++--------
3 files changed, 29 insertions(+), 10 deletions(-)
diff --git a/system/driver/base/app.go b/system/driver/base/app.go
index f3248d59fa..10c3e34f2f 100644
--- a/system/driver/base/app.go
+++ b/system/driver/base/app.go
@@ -20,6 +20,7 @@ import (
"cogentcore.org/core/events/key"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
+ "cogentcore.org/core/text/printer"
"github.com/jeandeaual/go-locale"
)
@@ -68,6 +69,9 @@ func Init(a system.App, ab *App) {
ab.This = a
system.TheApp = a
key.SystemPlatform = a.SystemPlatform().String()
+ // sl := a.SystemLocale()
+ // fmt.Println("locale:", sl, sl.Language(), sl.Region())
+ printer.Settings.Defaults() // depends on system.TheApp
}
func (a *App) MainLoop() {
diff --git a/system/locale.go b/system/locale.go
index e8f47afc70..0ae52e8191 100644
--- a/system/locale.go
+++ b/system/locale.go
@@ -15,7 +15,11 @@ func (l Locale) Language() string {
if l == "" {
return ""
}
- return strings.Split(string(l), "-")[0]
+ pos := strings.LastIndex(string(l), "-")
+ if pos < 0 {
+ return string(l)
+ }
+ return string(l)[:pos]
}
// Region returns the region portion of the locale tag (e.g., US, FR, JA)
@@ -27,5 +31,5 @@ func (l Locale) Region() string {
if pos < 0 {
return ""
}
- return string(l)[:pos]
+ return string(l)[pos+1:]
}
diff --git a/text/printer/settings.go b/text/printer/settings.go
index a4b2dea5cc..5ed4714151 100644
--- a/text/printer/settings.go
+++ b/text/printer/settings.go
@@ -17,11 +17,9 @@ import (
"cogentcore.org/core/tree"
)
-func init() {
- Settings.Defaults()
-}
-
// Settings provides the default printer settings.
+// This is initialized by the system.App because it depends on
+// the locale for initialization.
var Settings SettingsData
// Settings has standard printer settings.
@@ -29,11 +27,11 @@ type SettingsData struct {
// PageSize specifies a standard page size, or Custom.
PageSize PageSizes
- // Units are the units in which size is specified.
+ // Units are the units in which the page size is specified.
// Will automatically be set if PageSize != Custom.
Units units.Units
- // Size is the size in given units.
+ // Size is the page size in given units.
// Will automatically be set if PageSize != Custom.
Size math32.Vector2
@@ -43,13 +41,26 @@ type SettingsData struct {
func (ps *SettingsData) Defaults() {
ps.PageSize = DefaultPageSizeForRegion(system.TheApp.SystemLocale().Region())
- ps.Margins.Set(25) // basically one inch
+ switch ps.Units {
+ case units.UnitMm:
+ ps.Margins.Set(25) // basically one inch
+ case units.UnitPt:
+ ps.Margins.Set(72)
+ case units.UnitPx:
+ ps.Margins.Set(24)
+ }
ps.Update()
}
func (ps *SettingsData) Update() {
if ps.PageSize != Custom {
+ pU := ps.Units
ps.Units, ps.Size = ps.PageSize.Size()
+ if pU != ps.Units {
+ uc := units.NewContext()
+ sc := uc.Convert(1, pU, ps.Units)
+ ps.Margins = ps.Margins.MulScalar(sc)
+ }
}
}
@@ -73,7 +84,7 @@ func (ps *SettingsData) MakeToolbar(p *tree.Plan) {
// size = page size; body = content area inside margins
func (ps *SettingsData) ToDots(un *units.Context) (size, body math32.Vector2, margins sides.Floats) {
sc := un.ToDots(1, ps.Units)
- size = ps.Size.MulScalar(sc)
+ size = ps.Size.MulScalar(sc).Floor()
margins = ps.Margins.MulScalar(sc)
body.X = size.X - (margins.Left + margins.Right)
body.Y = size.Y - (margins.Top + margins.Bottom)
From 6f93ef2b284caaac41fc30ce9d60caca04f1177f Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sun, 12 Oct 2025 09:03:50 +0200
Subject: [PATCH 53/99] pdf: more printer formatting settings and enforcement
of actual font size, line spacing etc to accomplish an exact target font
size.
---
content/content.go | 2 +
content/settings.go | 2 -
paint/pdf/page.go | 411 --------------------------------------
paint/pdf/paint.go | 295 +++++++++++++++++++++++++++
paint/pdf/pdf_test.go | 35 ++++
paint/pdf/text.go | 147 ++++++++++++++
paint/pdf/writer.go | 2 +
svg/testdata/svg/test.svg | 2 +-
text/paginate/options.go | 12 +-
text/paginate/pdf.go | 23 ++-
text/paginate/runners.go | 41 ++--
text/printer/settings.go | 27 +++
12 files changed, 552 insertions(+), 447 deletions(-)
create mode 100644 paint/pdf/paint.go
diff --git a/content/content.go b/content/content.go
index 7dc1dce54e..4647f7e288 100644
--- a/content/content.go
+++ b/content/content.go
@@ -486,6 +486,8 @@ func (ct *Content) PagePDF(path string) error {
}
core.MessageSnackbar(ct, "Generating PDF...")
+ Settings.PDF.FontScale = (100.0 / core.AppearanceSettings.DocsFontSize)
+
ct.inPDFRender = true
ct.reloadPage()
ct.inPDFRender = false
diff --git a/content/settings.go b/content/settings.go
index a6c9a47c5a..5411eee148 100644
--- a/content/settings.go
+++ b/content/settings.go
@@ -10,7 +10,6 @@ import (
"cogentcore.org/core/content/bcontent"
"cogentcore.org/core/core"
"cogentcore.org/core/text/paginate"
- "cogentcore.org/core/text/rich"
)
func init() {
@@ -38,7 +37,6 @@ type SettingsData struct {
func (s *SettingsData) Defaults() {
s.PDF.Defaults()
- s.PDF.FontFamily = rich.Serif
s.PDF.Footer = nil
s.PageSettings = func(ct *Content, curPage *bcontent.Page) *SettingsData {
diff --git a/paint/pdf/page.go b/paint/pdf/page.go
index 66d12fba33..6271bf9571 100644
--- a/paint/pdf/page.go
+++ b/paint/pdf/page.go
@@ -11,19 +11,9 @@ import (
"bytes"
"fmt"
"image"
- "image/color"
- "log/slog"
- "cogentcore.org/core/base/errors"
"cogentcore.org/core/base/stack"
- "cogentcore.org/core/colors"
- "cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
- "cogentcore.org/core/paint/ppath"
- "cogentcore.org/core/styles"
- "cogentcore.org/core/text/rich"
- "cogentcore.org/core/text/text"
- "golang.org/x/text/encoding/charmap"
)
type pdfPage struct {
@@ -74,304 +64,6 @@ func (w *pdfPage) writePage(parent pdfRef) pdfRef {
return w.pdf.writeObject(page)
}
-// SetFill sets the fill style values where different from current.
-func (w *pdfPage) SetFill(fill *styles.Fill) {
- csty := w.style()
- if csty.Fill.Color != fill.Color || csty.Fill.Opacity != fill.Opacity {
- w.SetFillColor(fill)
- }
- csty.Fill = *fill
-}
-
-// SetAlpha sets the transparency value.
-func (w *pdfPage) SetAlpha(alpha float32) {
- gs := w.getOpacityGS(alpha)
- fmt.Fprintf(w, " /%v gs", gs)
-}
-
-// SetFillColor sets the filling color (image).
-func (w *pdfPage) SetFillColor(fill *styles.Fill) {
- switch x := fill.Color.(type) {
- // todo: pattern, image
- case *gradient.Linear:
- case *gradient.Radial:
- // TODO: should we unset cs?
- // fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(fill.Gradient))
- case *image.Uniform:
- var clr color.RGBA
- if x != nil {
- clr = colors.ApplyOpacity(colors.AsRGBA(x), fill.Opacity)
- }
- a := float32(clr.A) / 255.0
- if a == 0 {
- fmt.Fprintf(w, " 0 g")
- } else if clr.R == clr.G && clr.R == clr.B {
- fmt.Fprintf(w, " %v g", dec(float32(clr.R)/255.0/a))
- } else {
- fmt.Fprintf(w, " %v %v %v rg", dec(float32(clr.R)/255.0/a), dec(float32(clr.G)/255.0/a), dec(float32(clr.B)/255.0/a))
- }
- w.SetAlpha(a)
- }
-}
-
-// SetStroke sets the stroke style values where different from current.
-func (w *pdfPage) SetStroke(stroke *styles.Stroke) {
- csty := w.style()
- if csty.Stroke.Color != stroke.Color || csty.Stroke.Opacity != stroke.Opacity {
- w.SetStrokeColor(stroke)
- }
- if csty.Stroke.Width.Dots != stroke.Width.Dots {
- w.SetStrokeWidth(stroke.Width.Dots)
- }
- if csty.Stroke.Cap != stroke.Cap {
- w.SetStrokeCap(stroke.Cap)
- }
- if csty.Stroke.Join != stroke.Join || (csty.Stroke.Join == ppath.JoinMiter && csty.Stroke.MiterLimit != stroke.MiterLimit) {
- w.SetStrokeJoin(stroke.Join, stroke.MiterLimit)
- }
- if len(stroke.Dashes) > 0 { // always do
- w.SetDashes(stroke.DashOffset, stroke.Dashes)
- } else {
- if len(csty.Stroke.Dashes) > 0 {
- w.SetDashes(0, nil)
- }
- }
- csty.Stroke = *stroke
-}
-
-// SetStrokeColor sets the stroking color (image).
-func (w *pdfPage) SetStrokeColor(stroke *styles.Stroke) {
- switch x := stroke.Color.(type) {
- case *gradient.Linear:
- case *gradient.Radial:
- // TODO: should we unset cs?
- // fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(stroke.Gradient))
- case *image.Uniform:
- clr := colors.ApplyOpacity(colors.AsRGBA(x), stroke.Opacity)
- a := float32(clr.A) / 255.0
- if clr.R == clr.G && clr.R == clr.B {
- fmt.Fprintf(w, " %v G", dec(float32(clr.R)/255.0/a))
- } else {
- fmt.Fprintf(w, " %v %v %v RG", dec(float32(clr.R)/255.0/a), dec(float32(clr.G)/255.0/a), dec(float32(clr.B)/255.0/a))
- }
- w.SetAlpha(a)
- }
-}
-
-// SetStrokeWidth sets the stroke width.
-func (w *pdfPage) SetStrokeWidth(lineWidth float32) {
- fmt.Fprintf(w, " %v w", dec(lineWidth))
-}
-
-// SetStrokeCap sets the stroke cap type.
-func (w *pdfPage) SetStrokeCap(capper ppath.Caps) {
- var lineCap int
- switch capper {
- case ppath.CapButt:
- lineCap = 0
- case ppath.CapRound:
- lineCap = 1
- case ppath.CapSquare:
- lineCap = 2
- default:
- slog.Error("pdfWriter", "StrokeCap not supported", capper)
- }
- fmt.Fprintf(w, " %d J", lineCap)
-}
-
-// SetStrokeJoin sets the stroke join type.
-func (w *pdfPage) SetStrokeJoin(joiner ppath.Joins, miterLimit float32) {
- var lineJoin int
- switch joiner {
- case ppath.JoinBevel:
- lineJoin = 2
- case ppath.JoinRound:
- lineJoin = 1
- case ppath.JoinMiter:
- lineJoin = 0
- default:
- slog.Error("pdfWriter", "StrokeJoin not supported", joiner)
- }
- fmt.Fprintf(w, " %d j", lineJoin)
- if lineJoin == 0 {
- fmt.Fprintf(w, " %v M", dec(miterLimit))
- }
-}
-
-// SetDashes sets the dash phase and array.
-func (w *pdfPage) SetDashes(dashPhase float32, dashArray []float32) {
- if len(dashArray)%2 == 1 {
- dashArray = append(dashArray, dashArray...)
- }
-
- // PDF can't handle negative dash phases
- if dashPhase < 0.0 {
- totalLength := float32(0.0)
- for _, dash := range dashArray {
- totalLength += dash
- }
- for dashPhase < 0.0 {
- dashPhase += totalLength
- }
- }
-
- dashes := append(dashArray, dashPhase)
- if len(dashes) == 1 {
- fmt.Fprintf(w, " [] 0 d")
- dashes[0] = 0.0
- } else {
- fmt.Fprintf(w, " [%v", dec(dashes[0]))
- for _, dash := range dashes[1 : len(dashes)-1] {
- fmt.Fprintf(w, " %v", dec(dash))
- }
- fmt.Fprintf(w, "] %v d", dec(dashes[len(dashes)-1]))
- }
-}
-
-// SetFont sets the font.
-func (w *pdfPage) SetFont(sty *rich.Style, tsty *text.Style) error {
- if !w.inTextObject {
- return errors.Log(errors.New("pdfWriter: must be in text object"))
- }
- size := tsty.FontHeight(sty) // * w.pdf.globalScale
- ref := w.pdf.getFont(sty, tsty)
- if _, ok := w.resources["Font"]; !ok {
- w.resources["Font"] = pdfDict{}
- } else {
- for name, fontRef := range w.resources["Font"].(pdfDict) {
- if ref == fontRef {
- fmt.Fprintf(w, " /%v %v Tf", name, dec(size))
- return nil
- }
- }
- }
-
- name := pdfName(fmt.Sprintf("F%d", len(w.resources["Font"].(pdfDict))))
- w.resources["Font"].(pdfDict)[name] = ref
- fmt.Fprintf(w, " /%v %v Tf", name, dec(size))
- return nil
-}
-
-// SetTextPosition sets the text offset position.
-func (w *pdfPage) SetTextPosition(off math32.Vector2) error {
- if !w.inTextObject {
- return errors.Log(errors.New("pdfWriter: must be in text object"))
- }
- do := off.Sub(w.textPosition)
- // and finally apply an offset from there, in reverse for Y
- fmt.Fprintf(w, " %v %v Td", dec(do.X), dec(-do.Y))
- w.textPosition = off
- return nil
-}
-
-// SetTextRenderMode sets the text rendering mode.
-// 0 = fill text, 1 = stroke text, 2 = fill, then stroke.
-// higher numbers support clip path.
-func (w *pdfPage) SetTextRenderMode(mode int) error {
- if !w.inTextObject {
- return errors.Log(errors.New("pdfWriter: must be in text object"))
- }
- fmt.Fprintf(w, " %d Tr", mode)
- w.textRenderMode = mode
- return nil
-}
-
-// SetTextCharSpace sets the text character spacing.
-func (w *pdfPage) SetTextCharSpace(space float32) error {
- if !w.inTextObject {
- return errors.Log(errors.New("pdfWriter: must be in text object"))
- }
- fmt.Fprintf(w, " %v Tc", dec(space))
- w.textCharSpace = space
- return nil
-}
-
-// StartTextObject starts a text object, adding to the graphics
-// CTM transform matrix as given by the arg, and setting an inverting
-// text transform, so text is rendered upright.
-func (w *pdfPage) StartTextObject(m math32.Matrix2) error {
- if w.inTextObject {
- return errors.Log(errors.New("pdfWriter: already in text object"))
- }
- // set the graphics transform to m first
- w.PushTransform(m)
- fmt.Fprintf(w, " BT")
- // then apply an inversion text matrix
- tm := math32.Scale2D(1, -1)
- fmt.Fprintf(w, " %s Tm", mat2(tm))
- w.inTextObject = true
- w.textPosition = math32.Vector2{}
- return nil
-}
-
-// EndTextObject ends a text object.
-func (w *pdfPage) EndTextObject() error {
- if !w.inTextObject {
- return errors.Log(errors.New("pdfWriter: must be in text object"))
- }
- fmt.Fprintf(w, " ET")
- w.PopStack()
- w.inTextObject = false
- return nil
-}
-
-// WriteText writes text using current text style.
-func (w *pdfPage) WriteText(tx string) error {
- if !w.inTextObject {
- return errors.Log(errors.New("pdfWriter: must be in text object"))
- }
- if len(tx) == 0 {
- return nil
- }
-
- first := true
- write := func(s string) {
- if first {
- fmt.Fprintf(w, "(")
- first = false
- } else {
- fmt.Fprintf(w, " (")
- }
- rs := []rune(s)
- for _, r := range rs {
- c, ok := charmap.Windows1252.EncodeRune(r)
- if !ok {
- if '\u2000' <= r && r <= '\u200A' {
- c = ' '
- }
- }
- switch c {
- case '\n':
- w.WriteByte('\\')
- w.WriteByte('n')
- case '\r':
- w.WriteByte('\\')
- w.WriteByte('r')
- case '\t':
- w.WriteByte('\\')
- w.WriteByte('t')
- case '\b':
- w.WriteByte('\\')
- w.WriteByte('b')
- case '\f':
- w.WriteByte('\\')
- w.WriteByte('f')
- case '\\', '(', ')':
- w.WriteByte('\\')
- w.WriteByte(c)
- default:
- w.WriteByte(c)
- }
- }
- fmt.Fprintf(w, ")")
- }
-
- fmt.Fprintf(w, "[")
- write(tx)
- fmt.Fprintf(w, "]TJ")
- return nil
-}
-
// DrawImage embeds and draws an image, as a lossless (PNG)
func (w *pdfPage) DrawImage(img image.Image, m math32.Matrix2) {
size := img.Bounds().Size()
@@ -460,106 +152,3 @@ func (w *pdfPage) embedImage(img image.Image) pdfRef {
w.pdf.images[img] = ref
return ref
}
-
-func (w *pdfPage) getOpacityGS(a float32) pdfName {
- if name, ok := w.graphicsStates[a]; ok {
- return name
- }
- name := pdfName(fmt.Sprintf("A%d", len(w.graphicsStates)))
- w.graphicsStates[a] = name
-
- if _, ok := w.resources["ExtGState"]; !ok {
- w.resources["ExtGState"] = pdfDict{}
- }
- w.resources["ExtGState"].(pdfDict)[name] = pdfDict{
- "CA": a,
- "ca": a,
- }
- return name
-}
-
-/*
-func (w *pdfPage) getPattern(gradient ppath.Gradient) pdfName {
- // TODO: support patterns/gradients with alpha channel
- shading := pdfDict{
- "ColorSpace": pdfName("DeviceRGB"),
- }
- if g, ok := gradient.(*ppath.LinearGradient); ok {
- shading["ShadingType"] = 2
- shading["Coords"] = pdfArray{g.Start.X, g.Start.Y, g.End.X, g.End.Y}
- shading["Function"] = patternStopsFunction(g.Stops)
- shading["Extend"] = pdfArray{true, true}
- } else if g, ok := gradient.(*ppath.RadialGradient); ok {
- shading["ShadingType"] = 3
- shading["Coords"] = pdfArray{g.C0.X, g.C0.Y, g.R0, g.C1.X, g.C1.Y, g.R1}
- shading["Function"] = patternStopsFunction(g.Stops)
- shading["Extend"] = pdfArray{true, true}
- }
- pattern := pdfDict{
- "Type": pdfName("Pattern"),
- "PatternType": 2,
- "Shading": shading,
- }
-
- if _, ok := w.resources["Pattern"]; !ok {
- w.resources["Pattern"] = pdfDict{}
- }
- for name, pat := range w.resources["Pattern"].(pdfDict) {
- if reflect.DeepEqual(pat, pattern) {
- return name
- }
- }
- name := pdfName(fmt.Sprintf("P%d", len(w.resources["Pattern"].(pdfDict))))
- w.resources["Pattern"].(pdfDict)[name] = pattern
- return name
-}
-
-func patternStopsFunction(stops ppath.Stops) pdfDict {
- if len(stops) < 2 {
- return pdfDict{}
- }
-
- fs := []pdfDict{}
- encode := pdfArray{}
- bounds := pdfArray{}
- if !ppath.Equal(stops[0].Offset, 0.0) {
- fs = append(fs, patternStopFunction(stops[0], stops[0]))
- encode = append(encode, 0, 1)
- bounds = append(bounds, stops[0].Offset)
- }
- for i := 0; i < len(stops)-1; i++ {
- fs = append(fs, patternStopFunction(stops[i], stops[i+1]))
- encode = append(encode, 0, 1)
- if i != 0 {
- bounds = append(bounds, stops[1].Offset)
- }
- }
- if !ppath.Equal(stops[len(stops)-1].Offset, 1.0) {
- fs = append(fs, patternStopFunction(stops[len(stops)-1], stops[len(stops)-1]))
- encode = append(encode, 0, 1)
- }
- if len(fs) == 1 {
- return fs[0]
- }
- return pdfDict{
- "FunctionType": 3,
- "Domain": pdfArray{0, 1},
- "Encode": encode,
- "Bounds": bounds,
- "Functions": fs,
- }
-}
-
-func patternStopFunction(s0, s1 ppath.Stop) pdfDict {
- a0 := float32(s0.Color.A) / 255.0
- a1 := float32(s1.Color.A) / 255.0
- return pdfDict{
- "FunctionType": 2,
- "Domain": pdfArray{0, 1},
- "N": 1,
- "C0": pdfArray{float32(s0.Color.R) / 255.0 / a0, float32(s0.Color.G) / 255.0 / a0, float32(s0.Color.B) / 255.0 / a0},
- "C1": pdfArray{float32(s1.Color.R) / 255.0 / a1, float32(s1.Color.G) / 255.0 / a1, float32(s1.Color.B) / 255.0 / a1},
- }
-}
-
-*/
diff --git a/paint/pdf/paint.go b/paint/pdf/paint.go
new file mode 100644
index 0000000000..4ccfa86535
--- /dev/null
+++ b/paint/pdf/paint.go
@@ -0,0 +1,295 @@
+// Copyright (c) 2025, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// This is adapted from https://github.com/tdewolff/canvas
+// Copyright (c) 2015 Taco de Wolff, under an MIT License.
+
+package pdf
+
+import (
+ "fmt"
+ "image"
+ "image/color"
+ "log/slog"
+ "reflect"
+
+ "cogentcore.org/core/colors"
+ "cogentcore.org/core/colors/gradient"
+ "cogentcore.org/core/paint/ppath"
+ "cogentcore.org/core/styles"
+)
+
+// SetFill sets the fill style values where different from current.
+func (w *pdfPage) SetFill(fill *styles.Fill) {
+ csty := w.style()
+ if csty.Fill.Color != fill.Color || csty.Fill.Opacity != fill.Opacity {
+ w.SetFillColor(fill)
+ }
+ csty.Fill = *fill
+}
+
+// SetAlpha sets the transparency value.
+func (w *pdfPage) SetAlpha(alpha float32) {
+ gs := w.getOpacityGS(alpha)
+ fmt.Fprintf(w, " /%v gs", gs)
+}
+
+func alphaNorm(c uint8, a float32) dec {
+ if a == 0 {
+ return dec(0)
+ }
+ return dec(float32(c) / 255.0 / a)
+}
+
+func alphaNormColor(c color.RGBA, a float32) [3]dec {
+ var v [3]dec
+ v[0] = alphaNorm(c.R, a)
+ v[1] = alphaNorm(c.G, a)
+ v[2] = alphaNorm(c.B, a)
+ return v
+}
+
+// SetFillColor sets the filling color (image).
+func (w *pdfPage) SetFillColor(fill *styles.Fill) {
+ switch x := fill.Color.(type) {
+ // todo: image
+ case *gradient.Linear:
+ fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(x))
+ case *gradient.Radial:
+ fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(x))
+ case *image.Uniform:
+ var clr color.RGBA
+ if x != nil {
+ clr = colors.ApplyOpacity(colors.AsRGBA(x), fill.Opacity)
+ }
+ a := float32(clr.A) / 255.0
+ if a == 0 {
+ fmt.Fprintf(w, " 0 g")
+ } else if clr.R == clr.G && clr.R == clr.B {
+ fmt.Fprintf(w, " %v g", alphaNorm(clr.R, a))
+ } else {
+ v := alphaNormColor(clr, a)
+ fmt.Fprintf(w, " %v %v %v rg", v[0], v[1], v[2])
+ }
+ w.SetAlpha(a)
+ }
+}
+
+// SetStroke sets the stroke style values where different from current.
+func (w *pdfPage) SetStroke(stroke *styles.Stroke) {
+ csty := w.style()
+ if csty.Stroke.Color != stroke.Color || csty.Stroke.Opacity != stroke.Opacity {
+ w.SetStrokeColor(stroke)
+ }
+ if csty.Stroke.Width.Dots != stroke.Width.Dots {
+ w.SetStrokeWidth(stroke.Width.Dots)
+ }
+ if csty.Stroke.Cap != stroke.Cap {
+ w.SetStrokeCap(stroke.Cap)
+ }
+ if csty.Stroke.Join != stroke.Join || (csty.Stroke.Join == ppath.JoinMiter && csty.Stroke.MiterLimit != stroke.MiterLimit) {
+ w.SetStrokeJoin(stroke.Join, stroke.MiterLimit)
+ }
+ if len(stroke.Dashes) > 0 { // always do
+ w.SetDashes(stroke.DashOffset, stroke.Dashes)
+ } else {
+ if len(csty.Stroke.Dashes) > 0 {
+ w.SetDashes(0, nil)
+ }
+ }
+ csty.Stroke = *stroke
+}
+
+// SetStrokeColor sets the stroking color (image).
+func (w *pdfPage) SetStrokeColor(stroke *styles.Stroke) {
+ switch x := stroke.Color.(type) {
+ case *gradient.Linear:
+ case *gradient.Radial:
+ // TODO: should we unset cs?
+ // fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(stroke.Gradient))
+ case *image.Uniform:
+ clr := colors.ApplyOpacity(colors.AsRGBA(x), stroke.Opacity)
+ a := float32(clr.A) / 255.0
+ if clr.R == clr.G && clr.R == clr.B {
+ fmt.Fprintf(w, " %v G", alphaNorm(clr.R, a))
+ } else {
+ v := alphaNormColor(clr, a)
+ fmt.Fprintf(w, " %v %v %v RG", v[0], v[1], v[2])
+ }
+ w.SetAlpha(a)
+ }
+}
+
+// SetStrokeWidth sets the stroke width.
+func (w *pdfPage) SetStrokeWidth(lineWidth float32) {
+ fmt.Fprintf(w, " %v w", dec(lineWidth))
+}
+
+// SetStrokeCap sets the stroke cap type.
+func (w *pdfPage) SetStrokeCap(capper ppath.Caps) {
+ var lineCap int
+ switch capper {
+ case ppath.CapButt:
+ lineCap = 0
+ case ppath.CapRound:
+ lineCap = 1
+ case ppath.CapSquare:
+ lineCap = 2
+ default:
+ slog.Error("pdfWriter", "StrokeCap not supported", capper)
+ }
+ fmt.Fprintf(w, " %d J", lineCap)
+}
+
+// SetStrokeJoin sets the stroke join type.
+func (w *pdfPage) SetStrokeJoin(joiner ppath.Joins, miterLimit float32) {
+ var lineJoin int
+ switch joiner {
+ case ppath.JoinBevel:
+ lineJoin = 2
+ case ppath.JoinRound:
+ lineJoin = 1
+ case ppath.JoinMiter:
+ lineJoin = 0
+ default:
+ slog.Error("pdfWriter", "StrokeJoin not supported", joiner)
+ }
+ fmt.Fprintf(w, " %d j", lineJoin)
+ if lineJoin == 0 {
+ fmt.Fprintf(w, " %v M", dec(miterLimit))
+ }
+}
+
+// SetDashes sets the dash phase and array.
+func (w *pdfPage) SetDashes(dashPhase float32, dashArray []float32) {
+ if len(dashArray)%2 == 1 {
+ dashArray = append(dashArray, dashArray...)
+ }
+
+ // PDF can't handle negative dash phases
+ if dashPhase < 0.0 {
+ totalLength := float32(0.0)
+ for _, dash := range dashArray {
+ totalLength += dash
+ }
+ for dashPhase < 0.0 {
+ dashPhase += totalLength
+ }
+ }
+
+ dashes := append(dashArray, dashPhase)
+ if len(dashes) == 1 {
+ fmt.Fprintf(w, " [] 0 d")
+ dashes[0] = 0.0
+ } else {
+ fmt.Fprintf(w, " [%v", dec(dashes[0]))
+ for _, dash := range dashes[1 : len(dashes)-1] {
+ fmt.Fprintf(w, " %v", dec(dash))
+ }
+ fmt.Fprintf(w, "] %v d", dec(dashes[len(dashes)-1]))
+ }
+}
+
+func (w *pdfPage) getOpacityGS(a float32) pdfName {
+ if name, ok := w.graphicsStates[a]; ok {
+ return name
+ }
+ name := pdfName(fmt.Sprintf("A%d", len(w.graphicsStates)))
+ w.graphicsStates[a] = name
+
+ if _, ok := w.resources["ExtGState"]; !ok {
+ w.resources["ExtGState"] = pdfDict{}
+ }
+ w.resources["ExtGState"].(pdfDict)[name] = pdfDict{
+ "CA": a,
+ "ca": a,
+ }
+ return name
+}
+
+func (w *pdfPage) getPattern(gr gradient.Gradient) pdfName {
+ // TODO: support patterns/gradients with alpha channel
+ shading := pdfDict{
+ "ColorSpace": pdfName("DeviceRGB"),
+ }
+ switch g := gr.(type) {
+ case *gradient.Linear:
+ shading["ShadingType"] = 2
+ shading["Coords"] = pdfArray{g.Start.X, g.Start.Y, g.End.X, g.End.Y}
+ shading["Function"] = patternStopsFunction(g.Stops)
+ shading["Extend"] = pdfArray{true, true}
+ case *gradient.Radial:
+ shading["ShadingType"] = 3
+ shading["Coords"] = pdfArray{g.Center.X, g.Center.Y, g.Radius.X, g.Focal.X, g.Focal.Y, g.Radius.Y}
+ shading["Function"] = patternStopsFunction(g.Stops)
+ shading["Extend"] = pdfArray{true, true}
+ }
+ pattern := pdfDict{
+ "Type": pdfName("Pattern"),
+ "PatternType": 2,
+ "Shading": shading,
+ }
+
+ if _, ok := w.resources["Pattern"]; !ok {
+ w.resources["Pattern"] = pdfDict{}
+ }
+ for name, pat := range w.resources["Pattern"].(pdfDict) {
+ if reflect.DeepEqual(pat, pattern) {
+ return name
+ }
+ }
+ name := pdfName(fmt.Sprintf("P%d", len(w.resources["Pattern"].(pdfDict))))
+ w.resources["Pattern"].(pdfDict)[name] = pattern
+ return name
+}
+
+func patternStopsFunction(stops []gradient.Stop) pdfDict {
+ if len(stops) < 2 {
+ return pdfDict{}
+ }
+
+ fs := []pdfDict{}
+ encode := pdfArray{}
+ bounds := pdfArray{}
+ if !ppath.Equal(stops[0].Pos, 0.0) {
+ fs = append(fs, patternStopFunction(stops[0], stops[0]))
+ encode = append(encode, 0, 1)
+ bounds = append(bounds, stops[0].Pos)
+ }
+ for i := 0; i < len(stops)-1; i++ {
+ fs = append(fs, patternStopFunction(stops[i], stops[i+1]))
+ encode = append(encode, 0, 1)
+ if i != 0 {
+ bounds = append(bounds, stops[1].Pos)
+ }
+ }
+ if !ppath.Equal(stops[len(stops)-1].Pos, 1.0) {
+ fs = append(fs, patternStopFunction(stops[len(stops)-1], stops[len(stops)-1]))
+ encode = append(encode, 0, 1)
+ }
+ if len(fs) == 1 {
+ return fs[0]
+ }
+ return pdfDict{
+ "FunctionType": 3,
+ "Domain": pdfArray{0, 1},
+ "Encode": encode,
+ "Bounds": bounds,
+ "Functions": fs,
+ }
+}
+
+func patternStopFunction(s0, s1 gradient.Stop) pdfDict {
+ a0 := float32(s0.Color.A) / 255.0
+ a1 := float32(s1.Color.A) / 255.0
+ c0 := alphaNormColor(s0.Color, a0)
+ c1 := alphaNormColor(s1.Color, a1)
+ return pdfDict{
+ "FunctionType": 2,
+ "Domain": pdfArray{0, 1},
+ "N": 1,
+ "C0": pdfArray{c0[0], c0[1], c0[2]},
+ "C1": pdfArray{c1[0], c1[1], c1[2]},
+ }
+}
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
index 0a37b0039c..552c868425 100644
--- a/paint/pdf/pdf_test.go
+++ b/paint/pdf/pdf_test.go
@@ -14,6 +14,7 @@ import (
"testing"
"cogentcore.org/core/colors"
+ "cogentcore.org/core/colors/gradient"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint/ppath"
"cogentcore.org/core/styles"
@@ -53,6 +54,40 @@ func TestPath(t *testing.T) {
})
}
+func TestGradientLinear(t *testing.T) {
+ RunTest(t, "gradient-linear", 50, 50, func(pd *PDF, sty *styles.Paint) {
+ // pd.PushTransform(math32.Translate2D(10, 5))
+ p := ppath.New().Rectangle(0, 0, 30, 20)
+ gg := gradient.NewLinear().AddStop(colors.White, 0).AddStop(colors.Red, 1)
+ gg.Start.Set(10, 20)
+ gg.End.Set(40, 20)
+ sty.Stroke.Color = colors.Uniform(colors.Blue)
+ sty.Fill.Color = gg
+ sty.Stroke.Width.Px(2)
+ sty.ToDots()
+
+ pd.Path(*p, sty, math32.Translate2D(10, 20))
+ // pd.PopStack()
+ })
+}
+
+func TestGradientRadial(t *testing.T) {
+ RunTest(t, "gradient-radial", 50, 50, func(pd *PDF, sty *styles.Paint) {
+ p := ppath.New().Rectangle(0, 0, 30, 20)
+ // todo: this is a different definition than ours..
+ gg := gradient.NewRadial().AddStop(colors.White, 0).AddStop(colors.Red, 1)
+ gg.Center.Set(25, 20)
+ gg.Focal = gg.Center
+ gg.Radius.Set(15, 5)
+ sty.Stroke.Color = colors.Uniform(colors.Blue)
+ sty.Fill.Color = gg
+ sty.Stroke.Width.Px(2)
+ sty.ToDots()
+
+ pd.Path(*p, sty, math32.Translate2D(10, 20))
+ })
+}
+
func TestText(t *testing.T) {
RunTest(t, "text", 300, 300, func(pd *PDF, sty *styles.Paint) {
prv := UseStandardFonts()
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
index 514c9c6e8e..47215e3852 100644
--- a/paint/pdf/text.go
+++ b/paint/pdf/text.go
@@ -8,8 +8,10 @@
package pdf
import (
+ "fmt"
"image"
+ "cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
"cogentcore.org/core/paint/ppath"
@@ -19,6 +21,7 @@ import (
"cogentcore.org/core/text/shaped/shapers/shapedgt"
"cogentcore.org/core/text/text"
"cogentcore.org/core/text/textpos"
+ "golang.org/x/text/encoding/charmap"
)
// Text renders text to the canvas using a transformation matrix,
@@ -226,3 +229,147 @@ func (r *PDF) links(lns *shaped.Lines, m math32.Matrix2, pos math32.Vector2) {
r.w.AddLink(lk.URL, rb)
}
}
+
+// SetFont sets the font.
+func (w *pdfPage) SetFont(sty *rich.Style, tsty *text.Style) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ size := tsty.FontHeight(sty) // * w.pdf.globalScale
+ ref := w.pdf.getFont(sty, tsty)
+ if _, ok := w.resources["Font"]; !ok {
+ w.resources["Font"] = pdfDict{}
+ } else {
+ for name, fontRef := range w.resources["Font"].(pdfDict) {
+ if ref == fontRef {
+ fmt.Fprintf(w, " /%v %v Tf", name, dec(size))
+ return nil
+ }
+ }
+ }
+
+ name := pdfName(fmt.Sprintf("F%d", len(w.resources["Font"].(pdfDict))))
+ w.resources["Font"].(pdfDict)[name] = ref
+ fmt.Fprintf(w, " /%v %v Tf", name, dec(size))
+ return nil
+}
+
+// SetTextPosition sets the text offset position.
+func (w *pdfPage) SetTextPosition(off math32.Vector2) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ do := off.Sub(w.textPosition)
+ // and finally apply an offset from there, in reverse for Y
+ fmt.Fprintf(w, " %v %v Td", dec(do.X), dec(-do.Y))
+ w.textPosition = off
+ return nil
+}
+
+// SetTextRenderMode sets the text rendering mode.
+// 0 = fill text, 1 = stroke text, 2 = fill, then stroke.
+// higher numbers support clip path.
+func (w *pdfPage) SetTextRenderMode(mode int) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ fmt.Fprintf(w, " %d Tr", mode)
+ w.textRenderMode = mode
+ return nil
+}
+
+// SetTextCharSpace sets the text character spacing.
+func (w *pdfPage) SetTextCharSpace(space float32) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ fmt.Fprintf(w, " %v Tc", dec(space))
+ w.textCharSpace = space
+ return nil
+}
+
+// StartTextObject starts a text object, adding to the graphics
+// CTM transform matrix as given by the arg, and setting an inverting
+// text transform, so text is rendered upright.
+func (w *pdfPage) StartTextObject(m math32.Matrix2) error {
+ if w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: already in text object"))
+ }
+ // set the graphics transform to m first
+ w.PushTransform(m)
+ fmt.Fprintf(w, " BT")
+ // then apply an inversion text matrix
+ tm := math32.Scale2D(1, -1)
+ fmt.Fprintf(w, " %s Tm", mat2(tm))
+ w.inTextObject = true
+ w.textPosition = math32.Vector2{}
+ return nil
+}
+
+// EndTextObject ends a text object.
+func (w *pdfPage) EndTextObject() error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ fmt.Fprintf(w, " ET")
+ w.PopStack()
+ w.inTextObject = false
+ return nil
+}
+
+// WriteText writes text using current text style.
+func (w *pdfPage) WriteText(tx string) error {
+ if !w.inTextObject {
+ return errors.Log(errors.New("pdfWriter: must be in text object"))
+ }
+ if len(tx) == 0 {
+ return nil
+ }
+
+ first := true
+ write := func(s string) {
+ if first {
+ fmt.Fprintf(w, "(")
+ first = false
+ } else {
+ fmt.Fprintf(w, " (")
+ }
+ rs := []rune(s)
+ for _, r := range rs {
+ c, ok := charmap.Windows1252.EncodeRune(r)
+ if !ok {
+ if '\u2000' <= r && r <= '\u200A' {
+ c = ' '
+ }
+ }
+ switch c {
+ case '\n':
+ w.WriteByte('\\')
+ w.WriteByte('n')
+ case '\r':
+ w.WriteByte('\\')
+ w.WriteByte('r')
+ case '\t':
+ w.WriteByte('\\')
+ w.WriteByte('t')
+ case '\b':
+ w.WriteByte('\\')
+ w.WriteByte('b')
+ case '\f':
+ w.WriteByte('\\')
+ w.WriteByte('f')
+ case '\\', '(', ')':
+ w.WriteByte('\\')
+ w.WriteByte(c)
+ default:
+ w.WriteByte(c)
+ }
+ }
+ fmt.Fprintf(w, ")")
+ }
+
+ fmt.Fprintf(w, "[")
+ write(tx)
+ fmt.Fprintf(w, "]TJ")
+ return nil
+}
diff --git a/paint/pdf/writer.go b/paint/pdf/writer.go
index 0ec17e6f29..3add282d08 100644
--- a/paint/pdf/writer.go
+++ b/paint/pdf/writer.go
@@ -176,6 +176,8 @@ func (w *pdfWriter) writeVal(i interface{}) {
}
case int:
w.write("%d", v)
+ case dec:
+ w.write("%v", v)
case float32:
w.write("%v", dec(v))
case float64:
diff --git a/svg/testdata/svg/test.svg b/svg/testdata/svg/test.svg
index 5679868eea..c0d6c71ae4 100644
--- a/svg/testdata/svg/test.svg
+++ b/svg/testdata/svg/test.svg
@@ -15,7 +15,7 @@
offset="0"
id="stop5469" />
diff --git a/text/paginate/options.go b/text/paginate/options.go
index b7de958483..4009167e78 100644
--- a/text/paginate/options.go
+++ b/text/paginate/options.go
@@ -12,18 +12,13 @@ import (
"cogentcore.org/core/styles/sides"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/text/printer"
- "cogentcore.org/core/text/rich"
)
// Options has the parameters for pagination.
type Options struct {
- // FontFamily specifies the default font family to apply
- // to all core.Text elements.
- FontFamily rich.Family
-
- // FontSize specifies the default font size to apply
- // to all core.Text elements, if non-zero.
- FontSize units.Value
+ // FontScale is an additional font scaling factor to apply.
+ // This is used in content to reverse the DocsFontSize factor, for example.
+ FontScale float32
// Title generates the title contents for the first page,
// into the given page body frame.
@@ -57,6 +52,7 @@ func NewOptions() Options {
}
func (o *Options) Defaults() {
+ o.FontScale = 1
o.Footer = CenteredPageNumber
}
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index 9f0879b3db..c6da882fd3 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -13,6 +13,7 @@ import (
"cogentcore.org/core/paint/renderers/pdfrender"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
+ "cogentcore.org/core/text/printer"
"cogentcore.org/core/text/rich"
"cogentcore.org/core/tree"
)
@@ -85,21 +86,29 @@ func (p *pager) assemble() {
p.ins = append([]core.Widget{tf.This.(core.Widget)}, p.ins...)
tf.StyleTree()
}
- for _, in := range p.ins {
+ fsc := printer.Settings.FontScale() * p.opts.FontScale
+ for ii, in := range p.ins {
iw := core.AsWidget(in)
iw.FinalStyler(func(s *styles.Style) {
s.Min.X.Dot(p.opts.BodyDots.X)
s.Min.Y.Dot(p.opts.BodyDots.Y)
})
+ if p.opts.Title != nil && ii == 0 { // don't restyle the title
+ continue
+ }
iw.WidgetWalkDown(func(cw core.Widget, cwb *core.WidgetBase) bool {
if tx, ok := cwb.This.(*core.Text); ok {
- if tx.Styles.Font.Family == rich.SansSerif {
- if _, ok := cwb.Parent.(*core.Frame); ok { // not inside buttons etc
- cwb.Styler(func(s *styles.Style) {
- s.Font.Family = p.opts.FontFamily
- })
- }
+ if _, ok := cwb.Parent.(*core.Frame); ok { // not inside buttons etc
+ cwb.Styler(func(s *styles.Style) {
+ if tx.Styles.Font.Family == rich.SansSerif {
+ s.Font.Family = printer.Settings.FontFamily
+ }
+ if tx.Type == core.TextBodyLarge {
+ s.Text.LineHeight = printer.Settings.LineHeight
+ }
+ s.Font.Size.Value *= fsc
+ })
}
}
return true
diff --git a/text/paginate/runners.go b/text/paginate/runners.go
index 2290b996c3..527baeabe9 100644
--- a/text/paginate/runners.go
+++ b/text/paginate/runners.go
@@ -9,6 +9,7 @@ import (
"cogentcore.org/core/core"
"cogentcore.org/core/styles"
+ "cogentcore.org/core/text/printer"
"cogentcore.org/core/text/rich"
"cogentcore.org/core/text/text"
)
@@ -50,15 +51,15 @@ func HeaderLeftPageNumber(header string) func(frame *core.Frame, opts *Options,
})
core.NewText(fr).SetText(header).Styler(func(s *styles.Style) {
s.SetTextWrap(false)
- s.Font.Family = opts.FontFamily
+ s.Font.Family = printer.Settings.FontFamily
s.Font.Slant = rich.Italic
- s.Font.Size.Pt(11)
+ s.Font.Size = printer.Settings.FontSize
})
core.NewStretch(fr)
core.NewText(fr).SetText(strconv.Itoa(pageNo)).Styler(func(s *styles.Style) {
s.SetTextWrap(false)
- s.Font.Family = opts.FontFamily
- s.Font.Size.Pt(11)
+ s.Font.Family = printer.Settings.FontFamily
+ s.Font.Size = printer.Settings.FontSize
})
core.NewSpace(frame).Styler(func(s *styles.Style) { // space after
s.Min.Y.Em(3)
@@ -84,23 +85,25 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
s.Min.Y.Em(.1)
})
core.NewText(fr).SetText(title).Styler(func(s *styles.Style) {
- s.Font.Family = opts.FontFamily
- s.Font.Size.Pt(16)
+ s.Font.Family = printer.Settings.FontFamily
+ s.Font.Size = printer.Settings.FontSize
+ s.Font.Size.Value *= 16.0 / 11
s.Text.Align = text.Center
})
if authors != "" {
core.NewText(fr).SetText(authors).Styler(func(s *styles.Style) {
- s.Font.Family = opts.FontFamily
- s.Font.Size.Pt(11)
+ s.Font.Family = printer.Settings.FontFamily
+ s.Font.Size = printer.Settings.FontSize
+ s.Font.Size.Value *= 12.0 / 11
s.Text.Align = text.Center
})
}
if affiliations != "" {
core.NewText(fr).SetText(affiliations).Styler(func(s *styles.Style) {
- s.Font.Family = opts.FontFamily
- s.Font.Size.Pt(10)
+ s.Font.Family = printer.Settings.FontFamily
+ s.Font.Size = printer.Settings.FontSize
s.Text.Align = text.Center
s.Text.LineHeight = 1.1
})
@@ -109,30 +112,32 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
if date != "" {
core.NewText(fr).SetText(date).Styler(func(s *styles.Style) {
- s.Font.Family = opts.FontFamily
- s.Font.Size.Pt(10)
+ s.Font.Family = printer.Settings.FontFamily
+ s.Font.Size = printer.Settings.FontSize
s.Text.Align = text.Center
})
}
if url != "" {
core.NewText(fr).SetText(url).Styler(func(s *styles.Style) {
- s.Font.Family = opts.FontFamily
- s.Font.Size.Pt(10)
+ s.Font.Family = printer.Settings.FontFamily
+ s.Font.Size = printer.Settings.FontSize
s.Text.Align = text.Center
})
}
if abstract != "" {
core.NewText(fr).SetText("Abstract:").Styler(func(s *styles.Style) {
- s.Font.Family = opts.FontFamily
- s.Font.Size.Pt(11)
+ s.Font.Family = printer.Settings.FontFamily
+ s.Font.Size = printer.Settings.FontSize
+ s.Font.Size.Value *= 12.0 / 11
s.Font.Weight = rich.Bold
s.Align.Self = styles.Start
})
core.NewText(fr).SetText(abstract).Styler(func(s *styles.Style) {
- s.Font.Family = opts.FontFamily
- s.Font.Size.Pt(10)
+ s.Font.Family = printer.Settings.FontFamily
+ s.Font.Size = printer.Settings.FontSize
+ s.Text.LineHeight = printer.Settings.LineHeight
s.Align.Self = styles.Start
})
}
diff --git a/text/printer/settings.go b/text/printer/settings.go
index 5ed4714151..b1c7f598e5 100644
--- a/text/printer/settings.go
+++ b/text/printer/settings.go
@@ -14,6 +14,7 @@ import (
"cogentcore.org/core/styles/sides"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/system"
+ "cogentcore.org/core/text/rich"
"cogentcore.org/core/tree"
)
@@ -37,6 +38,21 @@ type SettingsData struct {
// Margins specify the page margins in the size units.
Margins sides.Floats `display:"inline"`
+
+ // FontFamily specifies the font family to use for printing.
+ // The default SansSerif font used on screen may not be desired
+ // for printouts, where Serif is more typically used.
+ FontFamily rich.Family
+
+ // FontSize specifies the base font size to use for scaling printed
+ // text output (i.e., the default Text font will be this size, with
+ // larger elements scaled appropriately).
+ FontSize units.Value
+
+ // LineHeight is the default line height for standard text elements,
+ // in proportion to the font size (e.g., 1.25), which determines the
+ // spacing between lines.
+ LineHeight float32
}
func (ps *SettingsData) Defaults() {
@@ -49,6 +65,9 @@ func (ps *SettingsData) Defaults() {
case units.UnitPx:
ps.Margins.Set(24)
}
+ ps.FontFamily = rich.Serif
+ ps.FontSize.Pt(11)
+ ps.LineHeight = 1.25
ps.Update()
}
@@ -90,3 +109,11 @@ func (ps *SettingsData) ToDots(un *units.Context) (size, body math32.Vector2, ma
body.Y = size.Y - (margins.Top + margins.Bottom)
return
}
+
+// FontScale returns the scaling factor based on FontSize,
+// relative to the core default font size of 16 Dp.
+func (ps *SettingsData) FontScale() float32 {
+ uc := units.NewContext()
+ sc := uc.Convert(16, units.UnitDp, ps.FontSize.Unit)
+ return ps.FontSize.Value / sc
+}
From 75097f628e9ccc440250de16abdecd3540f81828 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sun, 12 Oct 2025 10:02:51 +0200
Subject: [PATCH 54/99] pdf: even more printer formatting settings including
header styling function
---
content/buttons.go | 2 +-
content/examples/basic/content/button.md | 14 ++++
content/settings.go | 1 +
paint/pdf/font.go | 81 ++++++++++++++++++++++++
paint/pdf/writer.go | 77 ----------------------
text/paginate/README.md | 12 ++--
text/paginate/options.go | 4 ++
text/paginate/pdf.go | 3 +
text/paginate/runners.go | 28 ++++++++
9 files changed, 140 insertions(+), 82 deletions(-)
diff --git a/content/buttons.go b/content/buttons.go
index 50cf3db8da..d58a32c10e 100644
--- a/content/buttons.go
+++ b/content/buttons.go
@@ -75,7 +75,7 @@ func (ct *Content) MakeToolbar(p *tree.Plan) {
})
})
tree.Add(p, func(w *core.Button) {
- w.SetText("PDF").SetIcon(icons.PictureAsPdf).SetTooltip("PDF generates and opens / downloads the current page as a printable PDF file")
+ w.SetText("PDF").SetIcon(icons.PictureAsPdf).SetTooltip("PDF generates and opens / downloads the current page as a printable PDF file. See the Settings/Printer panel (Command+,) for settings.")
w.OnClick(func(e events.Event) {
ct.PagePDF("pdfs")
})
diff --git a/content/examples/basic/content/button.md b/content/examples/basic/content/button.md
index 90c21f54d0..6a5b4b13b8 100644
--- a/content/examples/basic/content/button.md
+++ b/content/examples/basic/content/button.md
@@ -100,6 +100,20 @@ Action and menu buttons are the most minimal buttons, and they are typically onl
core.NewButton(b).SetType(core.ButtonAction).SetText("Action")
```
+### Third level header
+
+This is content at the third level.
+
+#### Fourth level header
+
+This is content at the fourth level. wow.
+
+##### Fifth level header
+
+Now this is taking things a bit far.
+
+## Lists are like this
+
* List item 1
* Sub list item 1
* Sub list item 2
diff --git a/content/settings.go b/content/settings.go
index 5411eee148..9abe2bc62b 100644
--- a/content/settings.go
+++ b/content/settings.go
@@ -56,6 +56,7 @@ func (s *SettingsData) Defaults() {
dt = time.Now().Format("January 2, 2006")
}
ps.PDF.Title = paginate.CenteredTitle(pt, curPage.Authors, curPage.Affiliations, ura, dt, curPage.Abstract)
+ ps.PDF.TextStyler = paginate.APAHeaders
return ps
}
}
diff --git a/paint/pdf/font.go b/paint/pdf/font.go
index bd351e3d69..b47ecd3dc1 100644
--- a/paint/pdf/font.go
+++ b/paint/pdf/font.go
@@ -7,6 +7,87 @@
package pdf
+import (
+ "cogentcore.org/core/text/rich"
+ "cogentcore.org/core/text/text"
+)
+
+func standardFontName(sty *rich.Style) string {
+ name := "Helvetica"
+ switch sty.Family {
+ case rich.SansSerif:
+ name = "Helvetica"
+ case rich.Serif:
+ name = "Times"
+ case rich.Monospace:
+ name = "Courier"
+ case rich.Cursive:
+ name = "ZapfChancery"
+ case rich.Math:
+ name = "Symbol"
+ case rich.Emoji:
+ name = "ZapfDingbats"
+ }
+ bname := name
+ if sty.Weight > rich.Medium {
+ name += "-Bold"
+ if sty.Slant == rich.Italic {
+ if bname == "Times" {
+ name += "Italic"
+ } else {
+ name += "Oblique"
+ }
+ }
+ } else {
+ if sty.Slant == rich.Italic {
+ if bname == "Times" {
+ name += "-Italic"
+ } else {
+ name += "-Oblique"
+ }
+ }
+ }
+ if name == "Times" {
+ name = "Times-Roman" // ugh
+ }
+ return name
+}
+
+func (w *pdfWriter) getFont(sty *rich.Style, tsty *text.Style) pdfRef {
+ if sty.Family != rich.Custom {
+ stdFont := standardFontName(sty)
+ if ref, ok := w.fontsStd[stdFont]; ok {
+ return ref
+ }
+
+ dict := pdfDict{
+ "Type": pdfName("Font"),
+ "Subtype": pdfName("Type1"),
+ "BaseFont": pdfName(stdFont),
+ "Encoding": pdfName("WinAnsiEncoding"),
+ }
+ ref := w.writeObject(dict)
+ w.fontsStd[stdFont] = ref
+ return ref
+ }
+ // todo: deal with custom
+ /*
+ fonts := w.fontsH
+ if vertical {
+ fonts = w.fontsV
+ }
+ if ref, ok := fonts[font]; ok {
+ return ref
+ }
+ w.objOffsets = append(w.objOffsets, 0)
+ ref := pdfRef(len(w.objOffsets))
+ fonts[font] = ref
+ w.fontSubset[font] = ppath.NewFontSubsetter()
+ return ref
+ */
+ return 0
+}
+
/*
func (w *pdfWriter) writeFont(ref pdfRef, font *text.Font, vertical bool) {
// subset the font, we only write the used characters to the PDF CMap object to reduce its
diff --git a/paint/pdf/writer.go b/paint/pdf/writer.go
index 3add282d08..25c9fe1845 100644
--- a/paint/pdf/writer.go
+++ b/paint/pdf/writer.go
@@ -25,8 +25,6 @@ import (
"cogentcore.org/core/paint/ppath"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
- "cogentcore.org/core/text/rich"
- "cogentcore.org/core/text/text"
"golang.org/x/exp/maps"
)
@@ -304,81 +302,6 @@ func (w *pdfWriter) writeObject(val interface{}) pdfRef {
return pdfRef(len(w.objOffsets))
}
-func standardFontName(sty *rich.Style) string {
- name := "Helvetica"
- switch sty.Family {
- case rich.SansSerif:
- name = "Helvetica"
- case rich.Serif:
- name = "Times"
- case rich.Monospace:
- name = "Courier"
- case rich.Cursive:
- name = "ZapfChancery"
- case rich.Math:
- name = "Symbol"
- case rich.Emoji:
- name = "ZapfDingbats"
- }
- if sty.Weight > rich.Medium {
- name += "-Bold"
- if sty.Slant == rich.Italic {
- if name == "Times" {
- name += "Italic"
- } else {
- name += "Oblique"
- }
- }
- } else {
- if sty.Slant == rich.Italic {
- if name == "Times" {
- name += "-Italic"
- } else {
- name += "-Oblique"
- }
- }
- }
- if name == "Times" {
- name = "Times-Roman" // ugh
- }
- return name
-}
-
-func (w *pdfWriter) getFont(sty *rich.Style, tsty *text.Style) pdfRef {
- if sty.Family != rich.Custom {
- stdFont := standardFontName(sty)
- if ref, ok := w.fontsStd[stdFont]; ok {
- return ref
- }
-
- dict := pdfDict{
- "Type": pdfName("Font"),
- "Subtype": pdfName("Type1"),
- "BaseFont": pdfName(stdFont),
- "Encoding": pdfName("WinAnsiEncoding"),
- }
- ref := w.writeObject(dict)
- w.fontsStd[stdFont] = ref
- return ref
- }
- // todo: deal with custom
- /*
- fonts := w.fontsH
- if vertical {
- fonts = w.fontsV
- }
- if ref, ok := fonts[font]; ok {
- return ref
- }
- w.objOffsets = append(w.objOffsets, 0)
- ref := pdfRef(len(w.objOffsets))
- fonts[font] = ref
- w.fontSubset[font] = ppath.NewFontSubsetter()
- return ref
- */
- return 0
-}
-
// Close finished the document.
func (w *pdfWriter) Close() error {
if w.page != nil {
diff --git a/text/paginate/README.md b/text/paginate/README.md
index 6022b6ed1c..4e3e8aed23 100644
--- a/text/paginate/README.md
+++ b/text/paginate/README.md
@@ -2,7 +2,7 @@
The `paginate` package takes a set of input Widget trees and returns a corresponding set of page Frame widgets that fit within a specified height, with optional title, headers and footers.
-The main purpose is for generating PDF output, via the PDF function, which installs default PDF fonts (Helvetica, Times, Courier) and renders output.
+The main purpose is for generating PDF output, via the `PDF` function, which installs default PDF fonts (Helvetica, Times, Courier) and renders output.
The first step involves extracting a list of leaf-level widgets from surrounding core.Frame elements, that are then processed by the layout function to fit into page-sized chunks. This can be controlled by the properties as described below.
@@ -12,9 +12,13 @@ Properties can be set on widgets to inform the pagination process. This is done
* `block` -- marks a Frame as a block that is not to be further extracted from in collecting leaves. Only Frame elements that have direction = Column are
-* `float-top` -- marks a `block` frame to be floated to the top of a page
-
* `break` -- starts a new page before this element.
-* `no-break-after` -- marks an element to not have a page break inserted after it.
+* `no-break-after` -- marks an element to not have a page break inserted after it (e.g., for a header).
+
+## Options and settings
+
+The `Options` type specifies a number of optional formatting elements, which are used by the PDF generator to insert a title and running headers and footers, according to functions that can add any kind of text. See `runners.go` for some examples.
+
+The `printer.Settings` settings are used to determine page size and font formatting defaults.
diff --git a/text/paginate/options.go b/text/paginate/options.go
index 4009167e78..f3aaf024a7 100644
--- a/text/paginate/options.go
+++ b/text/paginate/options.go
@@ -20,6 +20,10 @@ type Options struct {
// This is used in content to reverse the DocsFontSize factor, for example.
FontScale float32
+ // TextStyler is an optional text styling function to apply to text elements.
+ // Can be used to adjust the sizes and formatting of headers, for example.
+ TextStyler func(tx *core.Text)
+
// Title generates the title contents for the first page,
// into the given page body frame.
Title func(frame *core.Frame, opts *Options)
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index c6da882fd3..167b4b0c58 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -108,6 +108,9 @@ func (p *pager) assemble() {
s.Text.LineHeight = printer.Settings.LineHeight
}
s.Font.Size.Value *= fsc
+ if p.opts.TextStyler != nil {
+ p.opts.TextStyler(tx)
+ }
})
}
}
diff --git a/text/paginate/runners.go b/text/paginate/runners.go
index 527baeabe9..ac74e41b73 100644
--- a/text/paginate/runners.go
+++ b/text/paginate/runners.go
@@ -144,3 +144,31 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
core.NewSpace(fr).Styler(func(s *styles.Style) { s.Min.Y.Em(1) })
}
}
+
+// APAHeaders is a TextStyler function that sets APA-style headers based
+// on the text.Type. The default material design header sizes used onscreen are
+// generally too large for print. This is designed for content, where the
+// second level header ## is used for most top-level headers within a page.
+func APAHeaders(tx *core.Text) {
+ s := &tx.Styles
+ base := printer.Settings.FontSize
+ switch tx.Type {
+ case core.TextDisplaySmall: // h1, e.g., chapter level
+ s.Font.Size = base
+ s.Font.Size.Value *= 16.0 / 11.0
+ s.Font.Weight = rich.Bold
+ case core.TextHeadlineMedium: // h2
+ s.Font.Size = base
+ s.Font.Size.Value *= 14.0 / 11.0
+ s.Font.Weight = rich.Bold
+ s.Align.Self = styles.Center
+ case core.TextTitleLarge: // h3
+ s.Font.Size = base
+ s.Font.Size.Value *= 12.0 / 11.0
+ s.Font.Weight = rich.Bold
+ case core.TextTitleMedium: // h4
+ s.Font.Size = base
+ s.Font.Weight = rich.Bold
+ s.Font.Slant = rich.Italic
+ }
+}
From 84ffd3462f2bf48f562dbd9d0432e96589f47471 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sun, 12 Oct 2025 12:13:25 +0200
Subject: [PATCH 55/99] pdf: table of contents outline generation in place
---
core/text.go | 25 ++++++++++--
paint/pdf/links.go | 84 ++++++++++++++++++++++++++++++++++++++++
paint/pdf/pdf_test.go | 28 ++++++++++++++
paint/pdf/text.go | 17 +++++++-
paint/pdf/writer.go | 13 +++++++
text/paginate/runners.go | 20 +++++++---
6 files changed, 176 insertions(+), 11 deletions(-)
diff --git a/core/text.go b/core/text.go
index 9ee392df6f..6c0f82878e 100644
--- a/core/text.go
+++ b/core/text.go
@@ -7,7 +7,9 @@ package core
import (
"fmt"
"image"
+ "strconv"
+ "cogentcore.org/core/base/errors"
"cogentcore.org/core/base/fileinfo/mimedata"
"cogentcore.org/core/colors"
"cogentcore.org/core/cursors"
@@ -428,9 +430,26 @@ func (tx *Text) configTextSize(sz math32.Vector2) {
sty, tsty := tx.Styles.NewRichText()
tsty.Align, tsty.AlignV = text.Start, text.Start
tx.paintText = tx.Scene.TextShaper().WrapLines(tx.richText, sty, tsty, sz)
+ tx.setAnchorFromProperties()
+}
+
+// setAnchorFromProperties sets the Anchor string on paintText based on
+// id and tag properties, which is needed for PDF print formatting.
+// This is the only point at which we know we have the final
+// shaped.Lines for a text item.
+func (tx *Text) setAnchorFromProperties() {
+ anc := ""
if id, ok := tx.Properties["id"]; ok {
- tx.paintText.Anchor = id.(string)
+ anc = id.(string)
+ }
+ if t, ok := tx.Properties["tag"]; ok {
+ tag := t.(string)
+ if len(tag) > 1 && tag[0] == 'h' { // header
+ level := errors.Log1(strconv.Atoi(tag[1:]))
+ anc += fmt.Sprintf(";Header %d:%s", level, tx.Text)
+ }
}
+ tx.paintText.Anchor = anc
}
// configTextAlloc is used for determining how much space the text
@@ -455,9 +474,7 @@ func (tx *Text) configTextAlloc(sz math32.Vector2) math32.Vector2 {
rsz = tx.paintText.Bounds.Size().Ceil()
}
tx.paintText = tsh.WrapLines(tx.richText, sty, tsty, rsz)
- if id, ok := tx.Properties["id"]; ok {
- tx.paintText.Anchor = id.(string)
- }
+ tx.setAnchorFromProperties()
return tx.paintText.Bounds.Size().Ceil()
}
diff --git a/paint/pdf/links.go b/paint/pdf/links.go
index 453e1166fc..c7a61f1a9f 100644
--- a/paint/pdf/links.go
+++ b/paint/pdf/links.go
@@ -51,3 +51,87 @@ func (w *pdfPage) AddLink(uri string, rect math32.Box2) {
}
w.annots = append(w.annots, annot)
}
+
+//////// outline
+
+// pdfOutline is one outline entry
+type pdfOutline struct {
+ name string
+ level int
+ page int
+ ypos float32 // inverted y position
+
+ parent int // index into outline list
+ count int
+ prev int
+ next int
+ first int
+ last int
+}
+
+// AddOutline adds an outline element. level must be > 0
+func (w *pdfPage) AddOutline(name string, level int, ypos float32) {
+ ol := &pdfOutline{name: name, level: level, page: w.pageNo, ypos: w.height - w.pdf.globalScale*ypos, prev: -1, next: -1, first: -1, last: -1}
+ if len(w.pdf.outlines) == 0 {
+ or := &pdfOutline{name: "Contents", level: 0, page: 0, ypos: 0, prev: -1, next: -1, first: -1, last: -1}
+ w.pdf.outlines = append(w.pdf.outlines, or)
+ }
+ w.pdf.outlines = append(w.pdf.outlines, ol)
+}
+
+// writeOutlines outputs all the outline elements, and returns the ref to the first one.
+func (w *pdfWriter) writeOutlines() pdfRef {
+ lastLev := make(map[int]int)
+ level := 0
+ n := len(w.outlines)
+ // note: this logic from fpdf
+ for i, o := range w.outlines {
+ if o.level > 0 {
+ parent := lastLev[o.level-1]
+ o.parent = parent
+ w.outlines[parent].last = i
+ w.outlines[parent].count++
+ if o.level > level {
+ w.outlines[parent].first = i
+ }
+ } else {
+ o.parent = n
+ }
+ if o.level <= level && i > 0 {
+ prev := lastLev[o.level]
+ w.outlines[prev].next = i
+ o.prev = prev
+ }
+ lastLev[o.level] = i
+ level = o.level
+ }
+ var refs pdfArray
+ firstRef := pdfRef(len(w.objOffsets) + 1)
+ for i, o := range w.outlines {
+ od := pdfDict{
+ "Title": o.name,
+ "Parent": firstRef + pdfRef(o.parent),
+ "Dest": pdfArray{w.pages[o.page], pdfName("XYZ"), 0, o.ypos, 0},
+ "Count": o.count,
+ }
+ if o.prev != -1 {
+ od[pdfName("Prev")] = firstRef + pdfRef(o.prev)
+ }
+ if o.next != -1 {
+ od[pdfName("Next")] = firstRef + pdfRef(o.next)
+ }
+ if o.first != -1 {
+ od[pdfName("First")] = firstRef + pdfRef(o.first)
+ }
+ if o.last != -1 {
+ od[pdfName("Last")] = firstRef + pdfRef(o.last)
+ }
+ if i == 0 {
+ delete(od, "Parent")
+ delete(od, "Dest")
+ }
+ ref := w.writeObject(od)
+ refs = append(refs, ref)
+ }
+ return firstRef
+}
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
index 552c868425..77b2c82a4d 100644
--- a/paint/pdf/pdf_test.go
+++ b/paint/pdf/pdf_test.go
@@ -172,6 +172,34 @@ func TestLinks(t *testing.T) {
})
}
+func TestOutline(t *testing.T) {
+ RunTest(t, "outline", 300, 300, func(pd *PDF, sty *styles.Paint) {
+ prv := UseStandardFonts()
+ sh := shaped.NewShaper()
+ m := math32.Identity2()
+ rsty := &sty.Font
+ tsty := &sty.Text
+ sz := math32.Vec2(250, 250)
+
+ txt := func(src string, pos math32.Vector2) {
+ tx, err := htmltext.HTMLToRich([]byte(src), rsty, nil)
+ assert.NoError(t, err)
+ lns := sh.WrapLines(tx, rsty, tsty, sz)
+ pd.Text(sty, m, pos, lns)
+ }
+ txt("Heading 1", math32.Vec2(10, 10))
+ pd.w.AddOutline("Heading 1", 1, 10)
+ txt("Sub Heading", math32.Vec2(10, 30))
+ pd.w.AddOutline("Sub Heading 1", 2, 30)
+ txt("Another Sub Heading", math32.Vec2(10, 50))
+ pd.w.AddOutline("Sub Heading 2", 2, 50)
+ txt("Heading 2", math32.Vec2(10, 70))
+ pd.w.AddOutline("Heading 2", 1, 70)
+
+ RestorePreviousFonts(prv)
+ })
+}
+
func TestLayers(t *testing.T) {
RunTest(t, "layers", 300, 300, func(pd *PDF, sty *styles.Paint) {
prv := UseStandardFonts()
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
index 47215e3852..c714a7fb1e 100644
--- a/paint/pdf/text.go
+++ b/paint/pdf/text.go
@@ -10,6 +10,8 @@ package pdf
import (
"fmt"
"image"
+ "strconv"
+ "strings"
"cogentcore.org/core/base/errors"
"cogentcore.org/core/colors"
@@ -28,7 +30,20 @@ import (
// (the translation component specifies the starting offset)
func (r *PDF) Text(style *styles.Paint, m math32.Matrix2, pos math32.Vector2, lns *shaped.Lines) {
if lns.Anchor != "" {
- r.w.AddAnchor(lns.Anchor, pos)
+ anc := lns.Anchor
+ hidx := strings.Index(anc, ";Header ")
+ if hidx >= 0 {
+ if hidx > 0 {
+ r.w.AddAnchor(anc[:hidx], pos)
+ anc = anc[hidx:]
+ }
+ anc = anc[8:]
+ level := errors.Log1(strconv.Atoi(anc[0:1]))
+ anc = anc[2:]
+ r.w.AddOutline(anc, level, pos.Y)
+ } else {
+ r.w.AddAnchor(lns.Anchor, pos)
+ }
}
mt := m.Mul(math32.Translate2D(pos.X, pos.Y))
r.w.PushTransform(mt)
diff --git a/paint/pdf/writer.go b/paint/pdf/writer.go
index 25c9fe1845..33f0f8e38a 100644
--- a/paint/pdf/writer.go
+++ b/paint/pdf/writer.go
@@ -50,6 +50,7 @@ type pdfWriter struct {
images map[image.Image]pdfRef
layers pdfLayers
anchors pdfMap // things that can be linked to within doc
+ outlines []*pdfOutline
compress bool
subset bool
title string
@@ -342,6 +343,18 @@ func (w *pdfWriter) Close() error {
catalog[pdfName("Names")] = pdfDict{"Dests": nmref}
}
+ if len(w.outlines) > 0 {
+ firstRef := w.writeOutlines()
+ first := w.outlines[0]
+ cdict := w.writeObject(pdfDict{
+ "Type:": pdfName("Outlines"),
+ "First": firstRef,
+ "Last": firstRef + pdfRef(first.last),
+ "Count": len(w.outlines),
+ })
+ catalog[pdfName("Outlines")] = cdict
+ }
+
if len(w.layers.list) > 0 {
var refs, off pdfArray
for _, l := range w.layers.list {
diff --git a/text/paginate/runners.go b/text/paginate/runners.go
index ac74e41b73..ed4fb8e840 100644
--- a/text/paginate/runners.go
+++ b/text/paginate/runners.go
@@ -7,6 +7,7 @@ package paginate
import (
"strconv"
+ "cogentcore.org/core/base/errors"
"cogentcore.org/core/core"
"cogentcore.org/core/styles"
"cogentcore.org/core/text/printer"
@@ -146,27 +147,34 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
}
// APAHeaders is a TextStyler function that sets APA-style headers based
-// on the text.Type. The default material design header sizes used onscreen are
+// on the tag property. The default material design header sizes used onscreen are
// generally too large for print. This is designed for content, where the
// second level header ## is used for most top-level headers within a page.
func APAHeaders(tx *core.Text) {
+ headerLevel := 0
+ if t, ok := tx.Properties["tag"]; ok {
+ tag := t.(string)
+ if len(tag) > 1 && tag[0] == 'h' {
+ headerLevel = errors.Log1(strconv.Atoi(tag[1:]))
+ }
+ }
s := &tx.Styles
base := printer.Settings.FontSize
- switch tx.Type {
- case core.TextDisplaySmall: // h1, e.g., chapter level
+ switch headerLevel {
+ case 1: // e.g., chapter level
s.Font.Size = base
s.Font.Size.Value *= 16.0 / 11.0
s.Font.Weight = rich.Bold
- case core.TextHeadlineMedium: // h2
+ case 2:
s.Font.Size = base
s.Font.Size.Value *= 14.0 / 11.0
s.Font.Weight = rich.Bold
s.Align.Self = styles.Center
- case core.TextTitleLarge: // h3
+ case 3:
s.Font.Size = base
s.Font.Size.Value *= 12.0 / 11.0
s.Font.Weight = rich.Bold
- case core.TextTitleMedium: // h4
+ case 4:
s.Font.Size = base
s.Font.Weight = rich.Bold
s.Font.Slant = rich.Italic
From 1a8978c36f86f97882fae01fbd3522eb6a547d78 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sun, 12 Oct 2025 12:22:14 +0200
Subject: [PATCH 56/99] pdf: toc root points to start of doc
---
paint/pdf/links.go | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/paint/pdf/links.go b/paint/pdf/links.go
index c7a61f1a9f..74ee91ee4d 100644
--- a/paint/pdf/links.go
+++ b/paint/pdf/links.go
@@ -73,7 +73,7 @@ type pdfOutline struct {
func (w *pdfPage) AddOutline(name string, level int, ypos float32) {
ol := &pdfOutline{name: name, level: level, page: w.pageNo, ypos: w.height - w.pdf.globalScale*ypos, prev: -1, next: -1, first: -1, last: -1}
if len(w.pdf.outlines) == 0 {
- or := &pdfOutline{name: "Contents", level: 0, page: 0, ypos: 0, prev: -1, next: -1, first: -1, last: -1}
+ or := &pdfOutline{name: "Contents", level: 0, page: 0, ypos: w.height, prev: -1, next: -1, first: -1, last: -1}
w.pdf.outlines = append(w.pdf.outlines, or)
}
w.pdf.outlines = append(w.pdf.outlines, ol)
@@ -128,7 +128,6 @@ func (w *pdfWriter) writeOutlines() pdfRef {
}
if i == 0 {
delete(od, "Parent")
- delete(od, "Dest")
}
ref := w.writeObject(od)
refs = append(refs, ref)
From 591409667f3549e7e20365f998debcde16689e8f Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sun, 12 Oct 2025 14:29:05 +0200
Subject: [PATCH 57/99] pdf: tests identify that the nomial gradient
coordinates need to be updated by the full transform.
---
colors/gradient/linear.go | 11 +++++++++--
paint/painter.go | 18 ++++++++++--------
paint/pdf/context.go | 9 +++++++++
paint/pdf/paint.go | 24 ++++++++++++++++--------
paint/pdf/pdf.go | 20 +++++++++++++-------
paint/pdf/pdf_test.go | 20 +++++++++++++-------
paint/pdf/text.go | 8 ++++----
paint/renderers/pdfrender/pdfrender.go | 2 +-
8 files changed, 75 insertions(+), 37 deletions(-)
diff --git a/colors/gradient/linear.go b/colors/gradient/linear.go
index 285ab7700c..011005810f 100644
--- a/colors/gradient/linear.go
+++ b/colors/gradient/linear.go
@@ -14,11 +14,12 @@ import (
"cogentcore.org/core/math32"
)
-// Linear represents a linear gradient. It implements the [image.Image] interface.
+// Linear represents a linear gradient along linear axis from Start to End.
+// It implements the [image.Image] interface.
type Linear struct { //types:add -setters
Base
- // the starting point of the gradient (x1 and y1 in SVG)
+ // the starting point of the gradient axis (x1 and y1 in SVG).
Start math32.Vector2
// the ending point of the gradient (x2 and y2 in SVG)
@@ -74,6 +75,12 @@ func (l *Linear) Update(opacity float32, box math32.Box2, objTransform math32.Ma
l.distanceLengthSquared = l.distance.LengthSquared()
}
+// TransformedAxis returns the Start and End axis points as transformed
+// by the last Update call.
+func (l *Linear) TransformedAxis() (start, end math32.Vector2) {
+ return l.rStart, l.rEnd
+}
+
// At returns the color of the linear gradient at the given point
func (l *Linear) At(x, y int) color.Color {
switch len(l.Stops) {
diff --git a/paint/painter.go b/paint/painter.go
index 275635e08d..a22998bd72 100644
--- a/paint/painter.go
+++ b/paint/painter.go
@@ -57,7 +57,9 @@ func NewPainter(size math32.Vector2) *Painter {
return pc
}
-func (pc *Painter) Transform() math32.Matrix2 {
+// Cumulative returns the current cumulative transform function,
+// including the current transform.
+func (pc *Painter) Cumulative() math32.Matrix2 {
return pc.Context().Cumulative.Mul(pc.Paint.Transform)
}
@@ -446,8 +448,8 @@ func (pc *Painter) DrawBox(pos, size math32.Vector2, img image.Image, op draw.Op
if img == nil {
img = colors.Uniform(color.RGBA{})
}
- pos = pc.Transform().MulVector2AsPoint(pos)
- size = pc.Transform().MulVector2AsVector(size)
+ pos = pc.Cumulative().MulVector2AsPoint(pos)
+ size = pc.Cumulative().MulVector2AsVector(size)
br := math32.RectFromPosSizeMax(pos, size)
cb := pc.Context().Bounds.Rect.ToRect()
b := cb.Intersect(br)
@@ -455,7 +457,7 @@ func (pc *Painter) DrawBox(pos, size math32.Vector2, img image.Image, op draw.Op
return
}
if g, ok := img.(gradient.Gradient); ok {
- g.Update(pc.Fill.Opacity, math32.B2FromRect(b), pc.Transform())
+ g.Update(pc.Fill.Opacity, math32.B2FromRect(b), pc.Cumulative())
} else {
img = gradient.ApplyOpacity(img, pc.Fill.Opacity)
}
@@ -521,7 +523,7 @@ func (pc *Painter) DrawImageAnchored(src image.Image, x, y, ax, ay float32) *pim
s := src.Bounds().Size()
x -= ax * float32(s.X)
y -= ay * float32(s.Y)
- m := pc.Transform().Translate(x, y)
+ m := pc.Cumulative().Translate(x, y)
var pim *pimage.Params
if pc.Mask == nil {
pim = pimage.NewTransform(m, src.Bounds(), src, draw.Over)
@@ -540,7 +542,7 @@ func (pc *Painter) DrawImageScaled(src image.Image, x, y, w, h float32) *pimage.
isz := math32.FromPoint(s)
isc := math32.Vec2(w, h).Div(isz)
- m := pc.Transform().Translate(x, y).Scale(isc.X, isc.Y)
+ m := pc.Cumulative().Translate(x, y).Scale(isc.X, isc.Y)
var pim *pimage.Params
if pc.Mask == nil {
pim = pimage.NewTransform(m, src.Bounds(), src, draw.Over)
@@ -558,8 +560,8 @@ func (pc *Painter) BoundingBox(minX, minY, maxX, maxY float32) image.Rectangle {
// if pc.Stroke.Color != nil {// todo
// sw = 0.5 * pc.StrokeWidth()
// }
- tmin := pc.Transform().MulVector2AsPoint(math32.Vec2(minX, minY))
- tmax := pc.Transform().MulVector2AsPoint(math32.Vec2(maxX, maxY))
+ tmin := pc.Cumulative().MulVector2AsPoint(math32.Vec2(minX, minY))
+ tmax := pc.Cumulative().MulVector2AsPoint(math32.Vec2(maxX, maxY))
tp1 := math32.Vec2(tmin.X-sw, tmin.Y-sw).ToPointFloor()
tp2 := math32.Vec2(tmax.X+sw, tmax.Y+sw).ToPointCeil()
return image.Rect(tp1.X, tp1.Y, tp2.X, tp2.Y)
diff --git a/paint/pdf/context.go b/paint/pdf/context.go
index b48d152c0a..e60eea6e60 100644
--- a/paint/pdf/context.go
+++ b/paint/pdf/context.go
@@ -64,6 +64,15 @@ func (w *pdfPage) PushTransform(m math32.Matrix2) {
w.SetTransform(m)
}
+// Cumulative returns the current cumulative transform.
+func (w *pdfPage) Cumulative() math32.Matrix2 {
+ m := math32.Identity2()
+ for _, s := range w.stack {
+ m = m.Mul(s.Transform)
+ }
+ return m
+}
+
// style() returns the currently active style
func (w *pdfPage) style() *styles.Paint {
ctx := w.stack.Peek()
diff --git a/paint/pdf/paint.go b/paint/pdf/paint.go
index 4ccfa86535..4e2abe7aab 100644
--- a/paint/pdf/paint.go
+++ b/paint/pdf/paint.go
@@ -16,15 +16,18 @@ import (
"cogentcore.org/core/colors"
"cogentcore.org/core/colors/gradient"
+ "cogentcore.org/core/math32"
"cogentcore.org/core/paint/ppath"
"cogentcore.org/core/styles"
)
// SetFill sets the fill style values where different from current.
-func (w *pdfPage) SetFill(fill *styles.Fill) {
+// The bounds and matrix are required for gradient fills: pass identity
+// if not available.
+func (w *pdfPage) SetFill(fill *styles.Fill, bounds math32.Box2, m math32.Matrix2) {
csty := w.style()
if csty.Fill.Color != fill.Color || csty.Fill.Opacity != fill.Opacity {
- w.SetFillColor(fill)
+ w.SetFillColor(fill, bounds, m)
}
csty.Fill = *fill
}
@@ -51,13 +54,13 @@ func alphaNormColor(c color.RGBA, a float32) [3]dec {
}
// SetFillColor sets the filling color (image).
-func (w *pdfPage) SetFillColor(fill *styles.Fill) {
+func (w *pdfPage) SetFillColor(fill *styles.Fill, bounds math32.Box2, m math32.Matrix2) {
switch x := fill.Color.(type) {
// todo: image
case *gradient.Linear:
- fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(x))
+ fmt.Fprintf(w, " /Pattern cs /%v scn", w.gradientPattern(x, bounds, m))
case *gradient.Radial:
- fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(x))
+ fmt.Fprintf(w, " /Pattern cs /%v scn", w.gradientPattern(x, bounds, m))
case *image.Uniform:
var clr color.RGBA
if x != nil {
@@ -107,7 +110,7 @@ func (w *pdfPage) SetStrokeColor(stroke *styles.Stroke) {
case *gradient.Linear:
case *gradient.Radial:
// TODO: should we unset cs?
- // fmt.Fprintf(w, " /Pattern cs /%v scn", w.getPattern(stroke.Gradient))
+ // fmt.Fprintf(w, " /Pattern cs /%v scn", w.gradientPattern(stroke.Gradient))
case *image.Uniform:
clr := colors.ApplyOpacity(colors.AsRGBA(x), stroke.Opacity)
a := float32(clr.A) / 255.0
@@ -208,15 +211,20 @@ func (w *pdfPage) getOpacityGS(a float32) pdfName {
return name
}
-func (w *pdfPage) getPattern(gr gradient.Gradient) pdfName {
+func (w *pdfPage) gradientPattern(gr gradient.Gradient, bounds math32.Box2, m math32.Matrix2) pdfName {
+ // fbox := sc.GetPathExtent()
+ // lastRenderBBox := image.Rectangle{Min: image.Point{fbox.Min.X.Floor(), fbox.Min.Y.Floor()},
+ // Max: image.Point{fbox.Max.X.Ceil(), fbox.Max.Y.Ceil()}}
+ gr.Update(1, bounds, m)
// TODO: support patterns/gradients with alpha channel
shading := pdfDict{
"ColorSpace": pdfName("DeviceRGB"),
}
switch g := gr.(type) {
case *gradient.Linear:
+ s, e := g.TransformedAxis()
shading["ShadingType"] = 2
- shading["Coords"] = pdfArray{g.Start.X, g.Start.Y, g.End.X, g.End.Y}
+ shading["Coords"] = pdfArray{s.X, s.Y, e.X, e.Y}
shading["Function"] = patternStopsFunction(g.Stops)
shading["Extend"] = pdfArray{true, true}
case *gradient.Radial:
diff --git a/paint/pdf/pdf.go b/paint/pdf/pdf.go
index f1efff2347..da2099f805 100644
--- a/paint/pdf/pdf.go
+++ b/paint/pdf/pdf.go
@@ -160,8 +160,14 @@ func (r *PDF) PushTransform(m math32.Matrix2) {
r.SetTransform(m)
}
-// Path renders a path to the canvas using a style and a transformation matrix.
-func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
+// Cumulative returns the current cumulative transform.
+func (r *PDF) Cumulative() math32.Matrix2 {
+ return r.w.Cumulative()
+}
+
+// Path renders a path to the canvas using a style and an
+// individual and cumulative transformation matrix (needed for fill)
+func (r *PDF) Path(path ppath.Path, style *styles.Paint, bounds math32.Box2, tr, cum math32.Matrix2) {
// PDFs don't support the arcs joiner, miter joiner (not clipped),
// or miter joiner (clipped) with non-bevel fallback
strokeUnsupported := false
@@ -175,7 +181,7 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
// strokeUnsupported = true
// }
}
- scale := math32.Sqrt(math32.Abs(m.Det()))
+ scale := math32.Sqrt(math32.Abs(tr.Det()))
stk := style.Stroke
stk.Width.Dots *= scale
stk.DashOffset, stk.Dashes = ppath.ScaleDash(scale, stk.DashOffset, stk.Dashes)
@@ -188,7 +194,7 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
//}
closed := false
- data := path.Clone().Transform(m).ToPDF()
+ data := path.Clone().Transform(tr).ToPDF()
if 1 < len(data) && data[len(data)-1] == 'h' {
data = data[:len(data)-2]
closed = true
@@ -221,7 +227,7 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
// return
}
if style.HasFill() && !style.HasStroke() {
- r.w.SetFill(&style.Fill)
+ r.w.SetFill(&style.Fill, bounds, cum)
r.w.Write([]byte(" "))
r.w.Write([]byte(data))
r.w.Write([]byte(" f"))
@@ -245,7 +251,7 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
// todo:
sameAlpha := true
if sameAlpha {
- r.w.SetFill(&style.Fill)
+ r.w.SetFill(&style.Fill, bounds, cum)
r.w.SetStroke(&style.Stroke)
r.w.Write([]byte(" "))
r.w.Write([]byte(data))
@@ -258,7 +264,7 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, m math32.Matrix2) {
r.w.Write([]byte("*"))
}
} else {
- r.w.SetFill(&style.Fill)
+ r.w.SetFill(&style.Fill, bounds, cum)
r.w.Write([]byte(" "))
r.w.Write([]byte(data))
r.w.Write([]byte(" f"))
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
index 77b2c82a4d..20fecbd537 100644
--- a/paint/pdf/pdf_test.go
+++ b/paint/pdf/pdf_test.go
@@ -50,24 +50,28 @@ func TestPath(t *testing.T) {
sty.Stroke.Width.Px(2)
sty.ToDots()
- pd.Path(*p, sty, math32.Translate2D(10, 20))
+ tr := math32.Translate2D(10, 20)
+ pd.Path(*p, sty, math32.Box2{}, tr, tr)
})
}
func TestGradientLinear(t *testing.T) {
RunTest(t, "gradient-linear", 50, 50, func(pd *PDF, sty *styles.Paint) {
- // pd.PushTransform(math32.Translate2D(10, 5))
+ pd.PushTransform(math32.Translate2D(10, 5).Scale(.5, .5))
p := ppath.New().Rectangle(0, 0, 30, 20)
gg := gradient.NewLinear().AddStop(colors.White, 0).AddStop(colors.Red, 1)
- gg.Start.Set(10, 20)
- gg.End.Set(40, 20)
+ gg.Start.Set(0, 10)
+ gg.End.Set(30, 10)
+ gg.Units = gradient.UserSpaceOnUse
sty.Stroke.Color = colors.Uniform(colors.Blue)
sty.Fill.Color = gg
sty.Stroke.Width.Px(2)
sty.ToDots()
- pd.Path(*p, sty, math32.Translate2D(10, 20))
- // pd.PopStack()
+ tr := math32.Translate2D(10, 20)
+ cum := pd.Cumulative()
+ pd.Path(*p, sty, math32.B2(0, 0, 30, 20), tr, cum.Mul(tr))
+ pd.PopStack()
})
}
@@ -76,6 +80,7 @@ func TestGradientRadial(t *testing.T) {
p := ppath.New().Rectangle(0, 0, 30, 20)
// todo: this is a different definition than ours..
gg := gradient.NewRadial().AddStop(colors.White, 0).AddStop(colors.Red, 1)
+ gg.Units = gradient.UserSpaceOnUse
gg.Center.Set(25, 20)
gg.Focal = gg.Center
gg.Radius.Set(15, 5)
@@ -84,7 +89,8 @@ func TestGradientRadial(t *testing.T) {
sty.Stroke.Width.Px(2)
sty.ToDots()
- pd.Path(*p, sty, math32.Translate2D(10, 20))
+ tr := math32.Translate2D(10, 20)
+ pd.Path(*p, sty, math32.B2(0, 0, 30, 20), tr, tr)
})
}
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
index c714a7fb1e..e6a7ea4310 100644
--- a/paint/pdf/text.go
+++ b/paint/pdf/text.go
@@ -135,7 +135,7 @@ func (r *PDF) textRun(style *styles.Paint, m math32.Matrix2, run *shapedgt.Run,
psty := *style
psty.Stroke.Color = run.StrokeColor
psty.Fill.Color = fill
- r.Path(*run.Math.Path, &psty, math32.Identity2())
+ r.Path(*run.Math.Path, &psty, math32.B2FromFixed(run.Bounds()), math32.Identity2(), math32.Identity2())
r.w.PopStack()
return
}
@@ -184,7 +184,7 @@ func (r *PDF) setTextStrokeColor(clr image.Image) {
func (r *PDF) setTextFillColor(clr image.Image) {
fc := r.w.style().Fill
fc.Color = clr
- r.w.SetFill(&fc)
+ r.w.SetFill(&fc, math32.Box2{}, math32.Identity2())
}
// setTextStyle applies the given styles.
@@ -217,7 +217,7 @@ func (r *PDF) strokeTextLine(m math32.Matrix2, sp, ep math32.Vector2, width floa
sty.Stroke.Color = clr
sty.Stroke.Dashes = dash
p := ppath.New().Line(sp.X, sp.Y, ep.X, ep.Y)
- r.Path(*p, sty, m)
+ r.Path(*p, sty, math32.Box2{}, m, m)
}
// FillBox fills a box in the given color.
@@ -227,7 +227,7 @@ func (r *PDF) FillBox(m math32.Matrix2, bb math32.Box2, clr image.Image) {
sty.Fill.Color = clr
sz := bb.Size()
p := ppath.New().Rectangle(bb.Min.X, bb.Min.Y, sz.X, sz.Y)
- r.Path(*p, sty, m)
+ r.Path(*p, sty, bb, m, m)
}
func (r *PDF) links(lns *shaped.Lines, m math32.Matrix2, pos math32.Vector2) {
diff --git a/paint/renderers/pdfrender/pdfrender.go b/paint/renderers/pdfrender/pdfrender.go
index a029b83e0d..05920da982 100644
--- a/paint/renderers/pdfrender/pdfrender.go
+++ b/paint/renderers/pdfrender/pdfrender.go
@@ -130,7 +130,7 @@ func (rs *Renderer) PopStack() int {
func (rs *Renderer) RenderPath(pt *render.Path) {
p := pt.Path
pc := &pt.Context
- rs.PDF.Path(p, &pc.Style, pc.Transform)
+ rs.PDF.Path(p, &pc.Style, pc.Bounds.Rect, pc.Transform, pc.Cumulative)
}
func (rs *Renderer) PushContext(pt *render.ContextPush) {
From dcae805c89d357a4fc71e125221c7b59f6c08beb Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 13 Oct 2025 10:11:19 +0200
Subject: [PATCH 58/99] pdf: gradients mostly working
---
colors/gradient/radial.go | 6 ++
paint/pdf/context.go | 2 +-
paint/pdf/paint.go | 131 ++++++++++++++++++++++++-
paint/pdf/pdf.go | 130 ++----------------------
paint/pdf/pdf_test.go | 7 +-
paint/pdf/text.go | 6 +-
paint/pdf/writer.go | 1 +
paint/renderers/pdfrender/pdfrender.go | 2 +-
svg/svg_test.go | 2 +-
svg/testdata/svg/test.svg | 68 +++++++------
10 files changed, 191 insertions(+), 164 deletions(-)
diff --git a/colors/gradient/radial.go b/colors/gradient/radial.go
index d87c0b655c..86a33e7ebe 100644
--- a/colors/gradient/radial.go
+++ b/colors/gradient/radial.go
@@ -96,6 +96,12 @@ func (r *Radial) Update(opacity float32, box math32.Box2, objTransform math32.Ma
r.rCenter, r.rFocal, r.rRadius = c, f, rs
}
+// TransformedCoords returns the coordinate points as transformed
+// by the last Update call.
+func (r *Radial) TransformedCoords() (center, focal, radius math32.Vector2) {
+ return r.rCenter, r.rFocal, r.rRadius
+}
+
const epsilonF = 1e-5
// At returns the color of the radial gradient at the given point
diff --git a/paint/pdf/context.go b/paint/pdf/context.go
index e60eea6e60..50a1ff6312 100644
--- a/paint/pdf/context.go
+++ b/paint/pdf/context.go
@@ -54,7 +54,7 @@ func (w *pdfPage) SetTransform(m math32.Matrix2) {
}
fmt.Fprintf(w, " %s cm", mat2(m2))
ctx := w.stack.Peek()
- ctx.Transform = ctx.Transform.Mul(m2)
+ ctx.Transform = m2 // not cumulative!
}
// PushTransform adds a graphics stack push (q) and then
diff --git a/paint/pdf/paint.go b/paint/pdf/paint.go
index 4e2abe7aab..2cd8879a9c 100644
--- a/paint/pdf/paint.go
+++ b/paint/pdf/paint.go
@@ -21,6 +21,128 @@ import (
"cogentcore.org/core/styles"
)
+// Path renders a path to the canvas using a style and an
+// individual matrix (needed for fill)
+func (r *PDF) Path(path ppath.Path, style *styles.Paint, bounds math32.Box2, tr math32.Matrix2) {
+ // PDFs don't support the arcs joiner, miter joiner (not clipped),
+ // or miter joiner (clipped) with non-bevel fallback
+ strokeUnsupported := false
+ if style.Stroke.Join == ppath.JoinArcs {
+ strokeUnsupported = true
+ } else if style.Stroke.Join == ppath.JoinMiter {
+ if style.Stroke.MiterLimit == 0 {
+ strokeUnsupported = true
+ }
+ // } else if _, ok := miter.GapJoiner.(canvas.BevelJoiner); !ok {
+ // strokeUnsupported = true
+ // }
+ }
+ scale := math32.Sqrt(math32.Abs(tr.Det()))
+ stk := style.Stroke
+ stk.Width.Dots *= scale
+ stk.DashOffset, stk.Dashes = ppath.ScaleDash(scale, stk.DashOffset, stk.Dashes)
+
+ // PDFs don't support connecting first and last dashes if path is closed,
+ // so we move the start of the path if this is the case
+ // TODO: closing dashes
+ //if style.DashesClose {
+ // strokeUnsupported = true
+ //}
+
+ closed := false
+ data := path.Clone().Transform(tr).ToPDF()
+ if 1 < len(data) && data[len(data)-1] == 'h' {
+ data = data[:len(data)-2]
+ closed = true
+ }
+
+ if style.HasStroke() && strokeUnsupported {
+ // todo: handle with optional inclusion of stroke function as _ import
+ /* // style.HasStroke() && strokeUnsupported
+ if style.HasFill() {
+ r.w.SetFill(style.Fill)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ r.w.Write([]byte(" f"))
+ if style.Fill.Rule == canvas.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+ }
+
+ // stroke settings unsupported by PDF, draw stroke explicitly
+ if style.IsDashed() {
+ path = path.Dash(style.DashOffset, style.Dashes...)
+ }
+ path = path.Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
+
+ r.w.SetFill(style.Stroke)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(path.Transform(m).ToPDF()))
+ r.w.Write([]byte(" f"))
+ */
+ // return
+ }
+ if style.HasFill() && !style.HasStroke() {
+ r.w.SetFill(&style.Fill, bounds, tr)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ r.w.Write([]byte(" f"))
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+ } else if !style.HasFill() && style.HasStroke() {
+ r.w.SetStroke(&stk)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ if closed {
+ r.w.Write([]byte(" s"))
+ } else {
+ r.w.Write([]byte(" S"))
+ }
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+ } else if style.HasFill() && style.HasStroke() {
+ // sameAlpha := style.Fill.IsColor() && style.Stroke.IsColor() && style.Fill.Color.A == style.Stroke.Color.A
+ // todo:
+ sameAlpha := true
+ if sameAlpha {
+ r.w.SetFill(&style.Fill, bounds, tr)
+ r.w.SetStroke(&style.Stroke)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ if closed {
+ r.w.Write([]byte(" b"))
+ } else {
+ r.w.Write([]byte(" B"))
+ }
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+ } else {
+ r.w.SetFill(&style.Fill, bounds, tr)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ r.w.Write([]byte(" f"))
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+
+ r.w.SetStroke(&style.Stroke)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ if closed {
+ r.w.Write([]byte(" s"))
+ } else {
+ r.w.Write([]byte(" S"))
+ }
+ if style.Fill.Rule == ppath.EvenOdd {
+ r.w.Write([]byte("*"))
+ }
+ }
+ }
+}
+
// SetFill sets the fill style values where different from current.
// The bounds and matrix are required for gradient fills: pass identity
// if not available.
@@ -215,7 +337,8 @@ func (w *pdfPage) gradientPattern(gr gradient.Gradient, bounds math32.Box2, m ma
// fbox := sc.GetPathExtent()
// lastRenderBBox := image.Rectangle{Min: image.Point{fbox.Min.X.Floor(), fbox.Min.Y.Floor()},
// Max: image.Point{fbox.Max.X.Ceil(), fbox.Max.Y.Ceil()}}
- gr.Update(1, bounds, m)
+ cum := w.Cumulative().Mul(m)
+ gr.Update(1, bounds, cum)
// TODO: support patterns/gradients with alpha channel
shading := pdfDict{
"ColorSpace": pdfName("DeviceRGB"),
@@ -228,8 +351,12 @@ func (w *pdfPage) gradientPattern(gr gradient.Gradient, bounds math32.Box2, m ma
shading["Function"] = patternStopsFunction(g.Stops)
shading["Extend"] = pdfArray{true, true}
case *gradient.Radial:
+ c, f, rad := g.TransformedCoords()
+ // r := 0.5 * (math32.Abs(rad.X) + math32.Abs(rad.Y))
+ r := max(math32.Abs(rad.X), math32.Abs(rad.Y))
+ // fmt.Println("c:", c, "ctr:", g.Center)
shading["ShadingType"] = 3
- shading["Coords"] = pdfArray{g.Center.X, g.Center.Y, g.Radius.X, g.Focal.X, g.Focal.Y, g.Radius.Y}
+ shading["Coords"] = pdfArray{f.X, f.Y, 0, c.X, c.Y, r}
shading["Function"] = patternStopsFunction(g.Stops)
shading["Extend"] = pdfArray{true, true}
}
diff --git a/paint/pdf/pdf.go b/paint/pdf/pdf.go
index da2099f805..50528fcc71 100644
--- a/paint/pdf/pdf.go
+++ b/paint/pdf/pdf.go
@@ -12,12 +12,16 @@ import (
"io"
"cogentcore.org/core/math32"
- "cogentcore.org/core/paint/ppath"
- "cogentcore.org/core/styles"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/text/rich"
)
+// todo:
+// * needs to include the basic page xform which we neeed to install on pdf rendering stack.
+// if passed bounds are empty, use path bounds.
+// - compute cumulative from stack, don't pass -- only pass local
+// - add a convenience method with empty bounds and ID transform, for text cases?
+
// UseStandardFonts sets the [rich.Settings] default fonts to the
// corresponding PDF defaults, so that text layout works correctly
// for the PDF rendering. The current settings are returned,
@@ -165,128 +169,6 @@ func (r *PDF) Cumulative() math32.Matrix2 {
return r.w.Cumulative()
}
-// Path renders a path to the canvas using a style and an
-// individual and cumulative transformation matrix (needed for fill)
-func (r *PDF) Path(path ppath.Path, style *styles.Paint, bounds math32.Box2, tr, cum math32.Matrix2) {
- // PDFs don't support the arcs joiner, miter joiner (not clipped),
- // or miter joiner (clipped) with non-bevel fallback
- strokeUnsupported := false
- if style.Stroke.Join == ppath.JoinArcs {
- strokeUnsupported = true
- } else if style.Stroke.Join == ppath.JoinMiter {
- if style.Stroke.MiterLimit == 0 {
- strokeUnsupported = true
- }
- // } else if _, ok := miter.GapJoiner.(canvas.BevelJoiner); !ok {
- // strokeUnsupported = true
- // }
- }
- scale := math32.Sqrt(math32.Abs(tr.Det()))
- stk := style.Stroke
- stk.Width.Dots *= scale
- stk.DashOffset, stk.Dashes = ppath.ScaleDash(scale, stk.DashOffset, stk.Dashes)
-
- // PDFs don't support connecting first and last dashes if path is closed,
- // so we move the start of the path if this is the case
- // TODO: closing dashes
- //if style.DashesClose {
- // strokeUnsupported = true
- //}
-
- closed := false
- data := path.Clone().Transform(tr).ToPDF()
- if 1 < len(data) && data[len(data)-1] == 'h' {
- data = data[:len(data)-2]
- closed = true
- }
-
- if style.HasStroke() && strokeUnsupported {
- // todo: handle with optional inclusion of stroke function as _ import
- /* // style.HasStroke() && strokeUnsupported
- if style.HasFill() {
- r.w.SetFill(style.Fill)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- r.w.Write([]byte(" f"))
- if style.Fill.Rule == canvas.EvenOdd {
- r.w.Write([]byte("*"))
- }
- }
-
- // stroke settings unsupported by PDF, draw stroke explicitly
- if style.IsDashed() {
- path = path.Dash(style.DashOffset, style.Dashes...)
- }
- path = path.Stroke(style.StrokeWidth, style.StrokeCapper, style.StrokeJoiner, canvas.Tolerance)
-
- r.w.SetFill(style.Stroke)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(path.Transform(m).ToPDF()))
- r.w.Write([]byte(" f"))
- */
- // return
- }
- if style.HasFill() && !style.HasStroke() {
- r.w.SetFill(&style.Fill, bounds, cum)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- r.w.Write([]byte(" f"))
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
- } else if !style.HasFill() && style.HasStroke() {
- r.w.SetStroke(&stk)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- if closed {
- r.w.Write([]byte(" s"))
- } else {
- r.w.Write([]byte(" S"))
- }
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
- } else if style.HasFill() && style.HasStroke() {
- // sameAlpha := style.Fill.IsColor() && style.Stroke.IsColor() && style.Fill.Color.A == style.Stroke.Color.A
- // todo:
- sameAlpha := true
- if sameAlpha {
- r.w.SetFill(&style.Fill, bounds, cum)
- r.w.SetStroke(&style.Stroke)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- if closed {
- r.w.Write([]byte(" b"))
- } else {
- r.w.Write([]byte(" B"))
- }
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
- } else {
- r.w.SetFill(&style.Fill, bounds, cum)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- r.w.Write([]byte(" f"))
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
-
- r.w.SetStroke(&style.Stroke)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- if closed {
- r.w.Write([]byte(" s"))
- } else {
- r.w.Write([]byte(" S"))
- }
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
- }
- }
-}
-
// Image renders an image to the canvas using a transformation matrix.
func (r *PDF) Image(img image.Image, m math32.Matrix2) {
r.w.DrawImage(img, m)
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
index 20fecbd537..df6ae9ee51 100644
--- a/paint/pdf/pdf_test.go
+++ b/paint/pdf/pdf_test.go
@@ -51,7 +51,7 @@ func TestPath(t *testing.T) {
sty.ToDots()
tr := math32.Translate2D(10, 20)
- pd.Path(*p, sty, math32.Box2{}, tr, tr)
+ pd.Path(*p, sty, math32.Box2{}, tr)
})
}
@@ -69,8 +69,7 @@ func TestGradientLinear(t *testing.T) {
sty.ToDots()
tr := math32.Translate2D(10, 20)
- cum := pd.Cumulative()
- pd.Path(*p, sty, math32.B2(0, 0, 30, 20), tr, cum.Mul(tr))
+ pd.Path(*p, sty, math32.B2(0, 0, 30, 20), tr)
pd.PopStack()
})
}
@@ -90,7 +89,7 @@ func TestGradientRadial(t *testing.T) {
sty.ToDots()
tr := math32.Translate2D(10, 20)
- pd.Path(*p, sty, math32.B2(0, 0, 30, 20), tr, tr)
+ pd.Path(*p, sty, math32.B2(0, 0, 30, 20), tr)
})
}
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
index e6a7ea4310..3604583593 100644
--- a/paint/pdf/text.go
+++ b/paint/pdf/text.go
@@ -135,7 +135,7 @@ func (r *PDF) textRun(style *styles.Paint, m math32.Matrix2, run *shapedgt.Run,
psty := *style
psty.Stroke.Color = run.StrokeColor
psty.Fill.Color = fill
- r.Path(*run.Math.Path, &psty, math32.B2FromFixed(run.Bounds()), math32.Identity2(), math32.Identity2())
+ r.Path(*run.Math.Path, &psty, math32.B2FromFixed(run.Bounds()), math32.Identity2())
r.w.PopStack()
return
}
@@ -217,7 +217,7 @@ func (r *PDF) strokeTextLine(m math32.Matrix2, sp, ep math32.Vector2, width floa
sty.Stroke.Color = clr
sty.Stroke.Dashes = dash
p := ppath.New().Line(sp.X, sp.Y, ep.X, ep.Y)
- r.Path(*p, sty, math32.Box2{}, m, m)
+ r.Path(*p, sty, math32.Box2{}, m)
}
// FillBox fills a box in the given color.
@@ -227,7 +227,7 @@ func (r *PDF) FillBox(m math32.Matrix2, bb math32.Box2, clr image.Image) {
sty.Fill.Color = clr
sz := bb.Size()
p := ppath.New().Rectangle(bb.Min.X, bb.Min.Y, sz.X, sz.Y)
- r.Path(*p, sty, bb, m, m)
+ r.Path(*p, sty, bb, m)
}
func (r *PDF) links(lns *shaped.Lines, m math32.Matrix2, pos math32.Vector2) {
diff --git a/paint/pdf/writer.go b/paint/pdf/writer.go
index 33f0f8e38a..9f54e9aade 100644
--- a/paint/pdf/writer.go
+++ b/paint/pdf/writer.go
@@ -493,6 +493,7 @@ func (w *pdfPage) setTopTransform() {
w.SetTransform(m)
}
+// dec efficiently prints float values.
type dec float32
func (f dec) String() string {
diff --git a/paint/renderers/pdfrender/pdfrender.go b/paint/renderers/pdfrender/pdfrender.go
index 05920da982..c4e6969861 100644
--- a/paint/renderers/pdfrender/pdfrender.go
+++ b/paint/renderers/pdfrender/pdfrender.go
@@ -130,7 +130,7 @@ func (rs *Renderer) PopStack() int {
func (rs *Renderer) RenderPath(pt *render.Path) {
p := pt.Path
pc := &pt.Context
- rs.PDF.Path(p, &pc.Style, pc.Bounds.Rect, pc.Transform, pc.Cumulative)
+ rs.PDF.Path(p, &pc.Style, pc.Bounds.Rect, pc.Transform)
}
func (rs *Renderer) PushContext(pt *render.ContextPush) {
diff --git a/svg/svg_test.go b/svg/svg_test.go
index 9a7475c962..7f38a5b080 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -60,7 +60,7 @@ func TestSVG(t *testing.T) {
files := fsx.Filenames(filepath.Join("testdata", dir), ".svg")
for _, fn := range files {
- // if fn != "fig_neuron_as_detect.svg" {
+ // if fn != "fig_srn_time_predict.svg" { // "fig_cortex_lobes.svg" {
// continue
// }
RunTest(t, 640, 480, dir, fn)
diff --git a/svg/testdata/svg/test.svg b/svg/testdata/svg/test.svg
index c0d6c71ae4..1630246cc4 100644
--- a/svg/testdata/svg/test.svg
+++ b/svg/testdata/svg/test.svg
@@ -1,37 +1,49 @@
-
From 4d18c93c65b49d70d67867612d8714ea6a27e726 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 13 Oct 2025 11:53:12 +0200
Subject: [PATCH 59/99] pdf: key fixes to gradient function to handle case with
non- 0,1 gradients
---
paint/pdf/paint.go | 12 +++++++-----
svg/svg_test.go | 6 +++---
2 files changed, 10 insertions(+), 8 deletions(-)
diff --git a/paint/pdf/paint.go b/paint/pdf/paint.go
index 2cd8879a9c..107a5ff4b5 100644
--- a/paint/pdf/paint.go
+++ b/paint/pdf/paint.go
@@ -380,11 +380,12 @@ func (w *pdfPage) gradientPattern(gr gradient.Gradient, bounds math32.Box2, m ma
}
func patternStopsFunction(stops []gradient.Stop) pdfDict {
+ n := len(stops)
if len(stops) < 2 {
return pdfDict{}
}
- fs := []pdfDict{}
+ fs := pdfArray{}
encode := pdfArray{}
bounds := pdfArray{}
if !ppath.Equal(stops[0].Pos, 0.0) {
@@ -392,19 +393,20 @@ func patternStopsFunction(stops []gradient.Stop) pdfDict {
encode = append(encode, 0, 1)
bounds = append(bounds, stops[0].Pos)
}
- for i := 0; i < len(stops)-1; i++ {
+ for i := range n - 1 {
fs = append(fs, patternStopFunction(stops[i], stops[i+1]))
encode = append(encode, 0, 1)
if i != 0 {
bounds = append(bounds, stops[1].Pos)
}
}
- if !ppath.Equal(stops[len(stops)-1].Pos, 1.0) {
- fs = append(fs, patternStopFunction(stops[len(stops)-1], stops[len(stops)-1]))
+ if !ppath.Equal(stops[n-1].Pos, 1.0) {
+ fs = append(fs, patternStopFunction(stops[n-1], stops[n-1]))
encode = append(encode, 0, 1)
+ bounds = append(bounds, stops[n-1].Pos)
}
if len(fs) == 1 {
- return fs[0]
+ return fs[0].(pdfDict)
}
return pdfDict{
"FunctionType": 3,
diff --git a/svg/svg_test.go b/svg/svg_test.go
index 7f38a5b080..94caf2b017 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -60,9 +60,9 @@ func TestSVG(t *testing.T) {
files := fsx.Filenames(filepath.Join("testdata", dir), ".svg")
for _, fn := range files {
- // if fn != "fig_srn_time_predict.svg" { // "fig_cortex_lobes.svg" {
- // continue
- // }
+ if fn != "test2.svg" {
+ continue
+ }
RunTest(t, 640, 480, dir, fn)
}
}
From 972fae6296967110b0fa458d34e7c1aaf89b41c7 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 13 Oct 2025 21:16:13 +0200
Subject: [PATCH 60/99] pdf: rendering fixes and svg io marshal fix to not
output duplicate transforms in groups
---
paint/pdf/paint.go | 17 ++-
paint/pdf/text.go | 6 +-
paint/renderers/pdfrender/pdfrender.go | 2 +-
svg/io.go | 7 +-
svg/svg_test.go | 8 +-
svg/testdata/svg/fig_bp_compute_delta.svg | 126 ++++++++++------------
6 files changed, 85 insertions(+), 81 deletions(-)
diff --git a/paint/pdf/paint.go b/paint/pdf/paint.go
index 107a5ff4b5..2e735e8a48 100644
--- a/paint/pdf/paint.go
+++ b/paint/pdf/paint.go
@@ -23,7 +23,7 @@ import (
// Path renders a path to the canvas using a style and an
// individual matrix (needed for fill)
-func (r *PDF) Path(path ppath.Path, style *styles.Paint, bounds math32.Box2, tr math32.Matrix2) {
+func (r *PDF) Path(path ppath.Path, style *styles.Paint, tr math32.Matrix2) {
// PDFs don't support the arcs joiner, miter joiner (not clipped),
// or miter joiner (clipped) with non-bevel fallback
strokeUnsupported := false
@@ -50,12 +50,22 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, bounds math32.Box2, tr
//}
closed := false
- data := path.Clone().Transform(tr).ToPDF()
+ trpath := path.Clone().Transform(tr)
+ data := trpath.ToPDF()
if 1 < len(data) && data[len(data)-1] == 'h' {
data = data[:len(data)-2]
closed = true
}
+ var bounds math32.Box2
+ if style.HasFill() {
+ if _, ok := style.Fill.Color.(gradient.Gradient); ok {
+ fbox := trpath.FastBounds()
+ cum := r.Cumulative()
+ bounds = fbox.MulMatrix2(cum)
+ }
+ }
+
if style.HasStroke() && strokeUnsupported {
// todo: handle with optional inclusion of stroke function as _ import
/* // style.HasStroke() && strokeUnsupported
@@ -334,9 +344,6 @@ func (w *pdfPage) getOpacityGS(a float32) pdfName {
}
func (w *pdfPage) gradientPattern(gr gradient.Gradient, bounds math32.Box2, m math32.Matrix2) pdfName {
- // fbox := sc.GetPathExtent()
- // lastRenderBBox := image.Rectangle{Min: image.Point{fbox.Min.X.Floor(), fbox.Min.Y.Floor()},
- // Max: image.Point{fbox.Max.X.Ceil(), fbox.Max.Y.Ceil()}}
cum := w.Cumulative().Mul(m)
gr.Update(1, bounds, cum)
// TODO: support patterns/gradients with alpha channel
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
index 3604583593..a2fe929299 100644
--- a/paint/pdf/text.go
+++ b/paint/pdf/text.go
@@ -135,7 +135,7 @@ func (r *PDF) textRun(style *styles.Paint, m math32.Matrix2, run *shapedgt.Run,
psty := *style
psty.Stroke.Color = run.StrokeColor
psty.Fill.Color = fill
- r.Path(*run.Math.Path, &psty, math32.B2FromFixed(run.Bounds()), math32.Identity2())
+ r.Path(*run.Math.Path, &psty, math32.Identity2())
r.w.PopStack()
return
}
@@ -217,7 +217,7 @@ func (r *PDF) strokeTextLine(m math32.Matrix2, sp, ep math32.Vector2, width floa
sty.Stroke.Color = clr
sty.Stroke.Dashes = dash
p := ppath.New().Line(sp.X, sp.Y, ep.X, ep.Y)
- r.Path(*p, sty, math32.Box2{}, m)
+ r.Path(*p, sty, m)
}
// FillBox fills a box in the given color.
@@ -227,7 +227,7 @@ func (r *PDF) FillBox(m math32.Matrix2, bb math32.Box2, clr image.Image) {
sty.Fill.Color = clr
sz := bb.Size()
p := ppath.New().Rectangle(bb.Min.X, bb.Min.Y, sz.X, sz.Y)
- r.Path(*p, sty, bb, m)
+ r.Path(*p, sty, m)
}
func (r *PDF) links(lns *shaped.Lines, m math32.Matrix2, pos math32.Vector2) {
diff --git a/paint/renderers/pdfrender/pdfrender.go b/paint/renderers/pdfrender/pdfrender.go
index c4e6969861..a029b83e0d 100644
--- a/paint/renderers/pdfrender/pdfrender.go
+++ b/paint/renderers/pdfrender/pdfrender.go
@@ -130,7 +130,7 @@ func (rs *Renderer) PopStack() int {
func (rs *Renderer) RenderPath(pt *render.Path) {
p := pt.Path
pc := &pt.Context
- rs.PDF.Path(p, &pc.Style, pc.Bounds.Rect, pc.Transform)
+ rs.PDF.Path(p, &pc.Style, pc.Transform)
}
func (rs *Renderer) PushContext(pt *render.ContextPush) {
diff --git a/svg/io.go b/svg/io.go
index 54e4a28234..f4bbb2bb10 100644
--- a/svg/io.go
+++ b/svg/io.go
@@ -18,6 +18,7 @@ import (
"io/fs"
"log"
"os"
+ "slices"
"strings"
"cogentcore.org/core/base/iox/imagex"
@@ -974,8 +975,12 @@ func MarshalXML(n tree.Node, enc *XMLEncoder, setName string) string {
for k, v := range properties {
sv := reflectx.ToString(v)
switch k {
- case "opacity", "transform":
+ case "opacity":
XMLAddAttr(&se.Attr, k, sv)
+ case "transform":
+ if slices.IndexFunc(se.Attr, func(a xml.Attr) bool { return a.Name.Local == k }) < 0 {
+ XMLAddAttr(&se.Attr, k, sv)
+ }
case "groupmode":
XMLAddAttr(&se.Attr, "inkscape:groupmode", sv)
if st, has := properties["style"]; has {
diff --git a/svg/svg_test.go b/svg/svg_test.go
index 94caf2b017..4d3956a5be 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -59,10 +59,12 @@ func TestSVG(t *testing.T) {
dir := "svg"
files := fsx.Filenames(filepath.Join("testdata", dir), ".svg")
+ // PDF currently failing: TestShapes4, 6, fig_bp_compute_delta.svg
+ // rect_trans.svg
for _, fn := range files {
- if fn != "test2.svg" {
- continue
- }
+ // if fn != "test2.svg" {
+ // continue
+ // }
RunTest(t, 640, 480, dir, fn)
}
}
diff --git a/svg/testdata/svg/fig_bp_compute_delta.svg b/svg/testdata/svg/fig_bp_compute_delta.svg
index 4e0bb1eebd..6dc1a7b295 100644
--- a/svg/testdata/svg/fig_bp_compute_delta.svg
+++ b/svg/testdata/svg/fig_bp_compute_delta.svg
@@ -8,23 +8,37 @@
xmlns="http://www.w3.org/2000/svg">
+
+
+
+
@@ -34,20 +48,6 @@
transform="scale(0.4) rotate(180) translate(10,0)"
d="M0 0L5 -5L-12.5 0L5 5z" />
-
-
-
-
@@ -239,10 +239,10 @@
+ style="inkscape:zoom:1;inkscape:document-units:px;inkscape:current-layer:layer1;inkscape:cx:0;inkscape:cy:0;">
+ style="spacingx:32;spacingy:32;type:xygrid;units:px;" />
@@ -678,94 +673,89 @@
@@ -782,7 +772,7 @@
Date: Mon, 13 Oct 2025 21:55:02 +0200
Subject: [PATCH 61/99] pdf: fix for bp_delta case: always push the transform..
---
paint/pdf/pdf_test.go | 6 +++---
paint/renderers/pdfrender/pdfrender.go | 6 +-----
svg/svg_test.go | 4 ++--
3 files changed, 6 insertions(+), 10 deletions(-)
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
index df6ae9ee51..41b15b4761 100644
--- a/paint/pdf/pdf_test.go
+++ b/paint/pdf/pdf_test.go
@@ -51,7 +51,7 @@ func TestPath(t *testing.T) {
sty.ToDots()
tr := math32.Translate2D(10, 20)
- pd.Path(*p, sty, math32.Box2{}, tr)
+ pd.Path(*p, sty, tr)
})
}
@@ -69,7 +69,7 @@ func TestGradientLinear(t *testing.T) {
sty.ToDots()
tr := math32.Translate2D(10, 20)
- pd.Path(*p, sty, math32.B2(0, 0, 30, 20), tr)
+ pd.Path(*p, sty, tr)
pd.PopStack()
})
}
@@ -89,7 +89,7 @@ func TestGradientRadial(t *testing.T) {
sty.ToDots()
tr := math32.Translate2D(10, 20)
- pd.Path(*p, sty, math32.B2(0, 0, 30, 20), tr)
+ pd.Path(*p, sty, tr)
})
}
diff --git a/paint/renderers/pdfrender/pdfrender.go b/paint/renderers/pdfrender/pdfrender.go
index a029b83e0d..eefd1ecd17 100644
--- a/paint/renderers/pdfrender/pdfrender.go
+++ b/paint/renderers/pdfrender/pdfrender.go
@@ -112,11 +112,7 @@ func (rs *Renderer) RenderPage(r render.Render) {
func (rs *Renderer) PushTransform(m math32.Matrix2) int {
cg := rs.gStack.Peek()
g := cg + 1
- if m.IsIdentity() {
- rs.PDF.PushStack()
- } else {
- rs.PDF.PushTransform(m)
- }
+ rs.PDF.PushTransform(m)
rs.gStack.Push(g)
return g
}
diff --git a/svg/svg_test.go b/svg/svg_test.go
index 4d3956a5be..0d233f2dc9 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -59,10 +59,10 @@ func TestSVG(t *testing.T) {
dir := "svg"
files := fsx.Filenames(filepath.Join("testdata", dir), ".svg")
- // PDF currently failing: TestShapes4, 6, fig_bp_compute_delta.svg
+ // PDF currently failing: TestShapes4, 6, rect_trans, arrows
// rect_trans.svg
for _, fn := range files {
- // if fn != "test2.svg" {
+ // if fn != "fig_bp_compute_delta.svg" {
// continue
// }
RunTest(t, 640, 480, dir, fn)
From 454ea3a01cbea54f7c3f58cd24d341e5f73582a9 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 13 Oct 2025 23:17:59 +0200
Subject: [PATCH 62/99] pdf: pdf passing most svg tests -- good enough for now!
---
paint/pdf/paint.go | 48 ++++++++++++----------------------------------
svg/svg_test.go | 5 ++---
2 files changed, 14 insertions(+), 39 deletions(-)
diff --git a/paint/pdf/paint.go b/paint/pdf/paint.go
index 2e735e8a48..29281bacd2 100644
--- a/paint/pdf/paint.go
+++ b/paint/pdf/paint.go
@@ -109,46 +109,22 @@ func (r *PDF) Path(path ppath.Path, style *styles.Paint, tr math32.Matrix2) {
} else {
r.w.Write([]byte(" S"))
}
+ } else if style.HasFill() && style.HasStroke() {
+ r.w.SetFill(&style.Fill, bounds, tr)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ r.w.Write([]byte(" f"))
if style.Fill.Rule == ppath.EvenOdd {
r.w.Write([]byte("*"))
}
- } else if style.HasFill() && style.HasStroke() {
- // sameAlpha := style.Fill.IsColor() && style.Stroke.IsColor() && style.Fill.Color.A == style.Stroke.Color.A
- // todo:
- sameAlpha := true
- if sameAlpha {
- r.w.SetFill(&style.Fill, bounds, tr)
- r.w.SetStroke(&style.Stroke)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- if closed {
- r.w.Write([]byte(" b"))
- } else {
- r.w.Write([]byte(" B"))
- }
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
- } else {
- r.w.SetFill(&style.Fill, bounds, tr)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- r.w.Write([]byte(" f"))
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
- r.w.SetStroke(&style.Stroke)
- r.w.Write([]byte(" "))
- r.w.Write([]byte(data))
- if closed {
- r.w.Write([]byte(" s"))
- } else {
- r.w.Write([]byte(" S"))
- }
- if style.Fill.Rule == ppath.EvenOdd {
- r.w.Write([]byte("*"))
- }
+ r.w.SetStroke(&stk)
+ r.w.Write([]byte(" "))
+ r.w.Write([]byte(data))
+ if closed {
+ r.w.Write([]byte(" s"))
+ } else {
+ r.w.Write([]byte(" S"))
}
}
}
diff --git a/svg/svg_test.go b/svg/svg_test.go
index 0d233f2dc9..e47f8de2c6 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -59,10 +59,9 @@ func TestSVG(t *testing.T) {
dir := "svg"
files := fsx.Filenames(filepath.Join("testdata", dir), ".svg")
- // PDF currently failing: TestShapes4, 6, rect_trans, arrows
- // rect_trans.svg
+ // PDF currently failing: TestShapes4, 6,
for _, fn := range files {
- // if fn != "fig_bp_compute_delta.svg" {
+ // if fn != "fig_neuron_as_detect_test.svg" {
// continue
// }
RunTest(t, 640, 480, dir, fn)
From d3a3e8427d36272fc6965ef5e02dd732b718e646 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 14 Oct 2025 08:32:22 +0200
Subject: [PATCH 63/99] pdf: svg to pdf and svg clone methods
---
paint/pdf/pdf_test.go | 2 +-
styles/units/context.go | 2 +-
svg/svg.go | 28 +
svg/testdata/svg/fig_neuron_as_detect.svg | 855 +++++++++++-----------
4 files changed, 471 insertions(+), 416 deletions(-)
diff --git a/paint/pdf/pdf_test.go b/paint/pdf/pdf_test.go
index 41b15b4761..0999b5d93e 100644
--- a/paint/pdf/pdf_test.go
+++ b/paint/pdf/pdf_test.go
@@ -80,7 +80,7 @@ func TestGradientRadial(t *testing.T) {
// todo: this is a different definition than ours..
gg := gradient.NewRadial().AddStop(colors.White, 0).AddStop(colors.Red, 1)
gg.Units = gradient.UserSpaceOnUse
- gg.Center.Set(25, 20)
+ gg.Center.Set(15, 10)
gg.Focal = gg.Center
gg.Radius.Set(15, 5)
sty.Stroke.Color = colors.Uniform(colors.Blue)
diff --git a/styles/units/context.go b/styles/units/context.go
index 8653688e31..8f242104c3 100644
--- a/styles/units/context.go
+++ b/styles/units/context.go
@@ -130,7 +130,7 @@ func (uc *Context) SetFont(em float32) {
uc.FontRem = math32.Round(uc.Dp(16))
}
-// ToDotsFact returns factor needed to convert given unit into raw pixels (dots in DPI)
+// Dots returns factor needed to convert given unit into raw pixels (dots in DPI)
func (uc *Context) Dots(un Units) float32 {
if uc.DPI == 0 {
// log.Printf("gi/units Context was not initialized -- falling back on defaults\n")
diff --git a/svg/svg.go b/svg/svg.go
index e7f3ca0026..c18d720885 100644
--- a/svg/svg.go
+++ b/svg/svg.go
@@ -10,9 +10,11 @@ import (
"bytes"
"image"
"image/color"
+ "os"
"strings"
"sync"
+ "cogentcore.org/core/base/errors"
"cogentcore.org/core/base/iox/imagex"
"cogentcore.org/core/colors"
"cogentcore.org/core/math32"
@@ -281,6 +283,32 @@ func (sv *SVG) SaveImageSize(fname string, width, height float32) error {
return err
}
+// SavePDF renders the SVG to a PDF and saves it to given filename.
+func (sv *SVG) SavePDF(fname string) error {
+ pos := sv.Geom.Pos
+ sv.Geom.Pos = image.Point{}
+ rend := sv.Render(nil).RenderDone()
+
+ ctx := units.NewContext()
+ vsz := math32.FromPoint(sv.Geom.Size)
+ pd := paint.NewPDFRenderer(vsz, ctx)
+ pd.Render(rend)
+ err := os.WriteFile(fname, pd.Source(), 0666)
+
+ sv.Geom.Pos = pos
+ return err
+}
+
+// CloneSVG returns a copy of this SVG, with given size,
+// by writing and reading the XML source.
+func (sv *SVG) CloneSVG(size math32.Vector2) *SVG {
+ var b bytes.Buffer
+ errors.Log(sv.WriteXML(&b, false))
+ nsv := NewSVG(size)
+ errors.Log(nsv.ReadXML(&b))
+ return nsv
+}
+
func (sv *SVG) FillViewport() {
sty := styles.NewPaint() // has no transform
pc := &paint.Painter{sv.painter.State, sty}
diff --git a/svg/testdata/svg/fig_neuron_as_detect.svg b/svg/testdata/svg/fig_neuron_as_detect.svg
index 2605305ace..c83e700e91 100644
--- a/svg/testdata/svg/fig_neuron_as_detect.svg
+++ b/svg/testdata/svg/fig_neuron_as_detect.svg
@@ -1,415 +1,442 @@
-
-
-
\ No newline at end of file
+ width="353.75"
+ height="196.25"
+ viewBox="0 0 384 224"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:xlink="http://www.w3.org/1999/xlink"
+ xmlns="http://www.w3.org/2000/svg">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Integration
+
+
+ Output
+
+
+ Inputs
+
+
+ Detector
+
+
+ Neuron
+
+
+ Synapses
+
+
+ Dendrites
+
+
+ Axon
+
+
+ Cell body
+ (membrane
+ potential)
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
From d7a86f601d32c423a870f55a424b46bd7fa999b5 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 14 Oct 2025 09:54:19 +0200
Subject: [PATCH 64/99] pdf: fix scene handle layout issues introduced by
earlier LayoutFrame function
---
core/render.go | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/core/render.go b/core/render.go
index 2775fb7267..f8e6d7c5c1 100644
--- a/core/render.go
+++ b/core/render.go
@@ -127,11 +127,12 @@ func (sc *Scene) LayoutScene() {
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nLayoutScene SizeUp start:", sc)
}
- sc.LayoutFrame(math32.FromPoint(sc.SceneGeom.Size))
+ sc.layoutFrame(math32.FromPoint(sc.SceneGeom.Size))
+ sc.ApplyScenePos()
}
-// LayoutFrame does a layout on the given Frame using given size.
-func (fr *Frame) LayoutFrame(size math32.Vector2) {
+// layoutFrame does the frame layout core functionality
+func (fr *Frame) layoutFrame(size math32.Vector2) {
fr.SizeUp()
sz := &fr.Geom.Size
sz.Alloc.Total = size
@@ -161,6 +162,11 @@ func (fr *Frame) LayoutFrame(size math32.Vector2) {
if DebugSettings.LayoutTrace {
fmt.Println("\n############################\nScenePos start:", fr)
}
+}
+
+// layoutFrame does a layout on the given Frame using given size.
+func (fr *Frame) LayoutFrame(size math32.Vector2) {
+ fr.layoutFrame(size)
fr.ApplyScenePos()
}
From 2744144215b7973bab0beb748697f2b61b70ce6e Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 14 Oct 2025 13:43:01 +0200
Subject: [PATCH 65/99] pdf: finally fix the svg testdata directory structure
so pdf and png are outer-level, not under svg
---
svg/svg_test.go | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/svg/svg_test.go b/svg/svg_test.go
index e47f8de2c6..ea86fc50cb 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -39,16 +39,16 @@ func RunTest(t *testing.T, width, height int, dir, fname string) {
rd := paint.NewImageRenderer(size)
img := rd.Render(rend).Image()
bnm := strings.TrimSuffix(fname, ".svg")
- imfn := filepath.Join(dir, "png", bnm)
+ imfn := filepath.Join("png", dir, bnm)
// fmt.Println(svfn, imfn)
imagex.Assert(t, img, imfn)
- pddir := filepath.Join("testdata", dir, "pdf")
- pdfn := filepath.Join(pddir, bnm+".pdf")
+ pddir := filepath.Join("testdata", "pdf")
+ pdfn := filepath.Join(pddir, dir, bnm+".pdf")
ctx := units.NewContext()
pd := paint.NewPDFRenderer(size, ctx)
pd.Render(rend)
- os.MkdirAll(pddir, 0777)
+ os.MkdirAll(filepath.Join(pddir, dir), 0777)
err = os.WriteFile(pdfn, pd.Source(), 0666)
assert.NoError(t, err)
@@ -86,7 +86,7 @@ func TestViewBox(t *testing.T) {
img := sv.RenderImage()
fnm := fmt.Sprintf("%s_%s", fpre, ts)
- imfn := filepath.Join("svg", "png", "viewbox", fnm)
+ imfn := filepath.Join("png", "viewbox", fnm)
// fmt.Println(imfn)
imagex.Assert(t, img, imfn)
}
@@ -156,7 +156,7 @@ func TestEmoji(t *testing.T) {
}
func TestFontEmoji(t *testing.T) {
- t.Skip("special-case testing -- requires noto-emoji file")
+ // t.Skip("special-case testing -- requires noto-emoji file")
// dir := filepath.Join("testdata", "noto-emoji")
os.MkdirAll("testdata/font-emoji-src", 0777)
fname := "/Library/Fonts/NotoColorEmoji-Regular.ttf"
@@ -189,7 +189,7 @@ func TestFontEmoji(t *testing.T) {
err := sv.ReadXML(b)
assert.NoError(t, err)
img := sv.RenderImage()
- imfn := filepath.Join("svg", "png", "font-emoji", strings.TrimSuffix(fn, ".svg"))
+ imfn := filepath.Join("png", "font-emoji", strings.TrimSuffix(fn, ".svg"))
imagex.Assert(t, img, imfn)
// sv.SaveXML(sfn)
// os.WriteFile(sfn, gd.Source, 0666)
From 3ccc95b33e666c7d13be6949b8acb6db65c945a5 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 14 Oct 2025 15:12:48 +0200
Subject: [PATCH 66/99] pdf: convenience RenderToPDF func
---
paint/state.go | 8 ++++++++
1 file changed, 8 insertions(+)
diff --git a/paint/state.go b/paint/state.go
index f97d6575e9..513dcc62c6 100644
--- a/paint/state.go
+++ b/paint/state.go
@@ -52,6 +52,14 @@ func RenderToSVG(pc *Painter) []byte {
return rd.Render(pc.RenderDone()).Source()
}
+// RenderToPDF is a convenience function that renders the current
+// accumulated painter actions to a PDF document using a [NewPDFRenderer]
+func RenderToPDF(pc *Painter) []byte {
+ p := pc.RenderDone()
+ rd := NewPDFRenderer(pc.Size, &pc.Context().Style.UnitContext)
+ return rd.Render(p).Source()
+}
+
// The State holds all the current rendering state information used
// while painting. The [Paint] embeds a pointer to this.
type State struct {
From a81b0793eb81ffc7f6883bd91458f1302fd20445 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 14 Oct 2025 19:30:45 +0200
Subject: [PATCH 67/99] pdf: minor cleanup of addlink
---
paint/pdf/links.go | 10 +++-------
1 file changed, 3 insertions(+), 7 deletions(-)
diff --git a/paint/pdf/links.go b/paint/pdf/links.go
index 74ee91ee4d..2a877bccfc 100644
--- a/paint/pdf/links.go
+++ b/paint/pdf/links.go
@@ -30,20 +30,16 @@ func (w *pdfPage) AddAnchor(name string, pos math32.Vector2) {
func (w *pdfPage) AddLink(uri string, rect math32.Box2) {
ms := math32.Scale2D(w.pdf.globalScale, w.pdf.globalScale)
rect = rect.MulMatrix2(ms)
- isLocal := false
- if uri[0] == '#' { // local anchor actions
- uri = uri[1:]
- isLocal = true
- }
annot := pdfDict{
"Type": pdfName("Annot"),
"Subtype": pdfName("Link"),
"Border": pdfArray{0, 0, 0},
"Rect": pdfArray{rect.Min.X, w.height - rect.Max.Y, rect.Max.X, w.height - rect.Min.Y},
}
- if isLocal {
- annot["Dest"] = uri
+ if len(uri) > 0 && uri[0] == '#' { // local anchor actions
+ annot["Dest"] = uri[1:]
} else {
+ annot["Contents"] = uri
annot["A"] = pdfDict{
"S": pdfName("URI"),
pdfName("URI"): uri,
From bcc3cac9905962028b4fa14faf695fe9ddedba86 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 15 Oct 2025 09:24:18 +0200
Subject: [PATCH 68/99] pdf: add OnlineURL for docs
---
docs/docs.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/docs/docs.go b/docs/docs.go
index 45ff203216..e421b55d22 100644
--- a/docs/docs.go
+++ b/docs/docs.go
@@ -59,6 +59,7 @@ func main() {
b := core.NewBody("Cogent Core Docs")
ct := content.NewContent(b).SetContent(econtent)
ctx := ct.Context
+ content.OfflineURL = "https://cogentcore.org/core"
ctx.AddWikilinkHandler(htmlcore.GoDocWikilink("doc", "cogentcore.org/core"))
b.AddTopBar(func(bar *core.Frame) {
tb := core.NewToolbar(bar)
From 63dcaa54186933df9c9fed28f2baccb280060439 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 21 Oct 2025 09:54:33 +0200
Subject: [PATCH 69/99] pdf: don't run TestFontEmoji
---
svg/svg_test.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/svg/svg_test.go b/svg/svg_test.go
index ea86fc50cb..5c0776794e 100644
--- a/svg/svg_test.go
+++ b/svg/svg_test.go
@@ -156,7 +156,7 @@ func TestEmoji(t *testing.T) {
}
func TestFontEmoji(t *testing.T) {
- // t.Skip("special-case testing -- requires noto-emoji file")
+ t.Skip("special-case testing -- requires noto-emoji file")
// dir := filepath.Join("testdata", "noto-emoji")
os.MkdirAll("testdata/font-emoji-src", 0777)
fname := "/Library/Fonts/NotoColorEmoji-Regular.ttf"
From c8d48bf1d549654025a30caf9ecb86df8b7fab51 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 4 Nov 2025 16:18:31 +0100
Subject: [PATCH 70/99] pdf: paginate ensures font is black and use light mode
so links are the correct blue color.
---
text/paginate/pdf.go | 6 ++++++
text/paginate/runners.go | 30 ++++++++++++++++++++----------
2 files changed, 26 insertions(+), 10 deletions(-)
diff --git a/text/paginate/pdf.go b/text/paginate/pdf.go
index 167b4b0c58..c051a614ef 100644
--- a/text/paginate/pdf.go
+++ b/text/paginate/pdf.go
@@ -7,6 +7,8 @@ package paginate
import (
"io"
+ "cogentcore.org/core/colors"
+ "cogentcore.org/core/colors/matcolor"
"cogentcore.org/core/core"
"cogentcore.org/core/paint"
"cogentcore.org/core/paint/pdf"
@@ -27,6 +29,8 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
if len(ins) == 0 {
return
}
+ cmode := matcolor.SchemeIsDark
+ colors.SetScheme(false)
cset := pdf.UseStandardFonts()
p := pager{opts: &opts, ins: ins}
@@ -67,6 +71,7 @@ func PDF(w io.Writer, opts Options, ins ...core.Widget) {
sc.DeleteChildren()
}
pdr.EndRender()
+ colors.SetScheme(cmode)
pdf.RestorePreviousFonts(cset)
}
@@ -108,6 +113,7 @@ func (p *pager) assemble() {
s.Text.LineHeight = printer.Settings.LineHeight
}
s.Font.Size.Value *= fsc
+ s.Color = colors.Uniform(colors.Black) // in case dark mode
if p.opts.TextStyler != nil {
p.opts.TextStyler(tx)
}
diff --git a/text/paginate/runners.go b/text/paginate/runners.go
index ed4fb8e840..10981d7180 100644
--- a/text/paginate/runners.go
+++ b/text/paginate/runners.go
@@ -8,6 +8,7 @@ import (
"strconv"
"cogentcore.org/core/base/errors"
+ "cogentcore.org/core/colors"
"cogentcore.org/core/core"
"cogentcore.org/core/styles"
"cogentcore.org/core/text/printer"
@@ -15,6 +16,13 @@ import (
"cogentcore.org/core/text/text"
)
+// TextStyler does standard text styling for printout:
+// FontFamily and Black Color (e.g., in case user is in Dark mode).
+func TextStyler(s *styles.Style) {
+ s.Font.Family = printer.Settings.FontFamily
+ s.Color = colors.Uniform(colors.Black)
+}
+
// CenteredPageNumber generates a page number cenetered in the frame
// with a 1.5em space above it.
func CenteredPageNumber(frame *core.Frame, opts *Options, pageNo int) {
@@ -28,7 +36,9 @@ func CenteredPageNumber(frame *core.Frame, opts *Options, pageNo int) {
s.Grow.Set(1, 0)
s.Justify.Content = styles.Center
})
- core.NewText(fr).SetText(strconv.Itoa(pageNo))
+ core.NewText(fr).SetText(strconv.Itoa(pageNo)).Styler(func(s *styles.Style) {
+ TextStyler(s)
+ })
}
// NoFirst excludes the first page for any runner
@@ -51,15 +61,15 @@ func HeaderLeftPageNumber(header string) func(frame *core.Frame, opts *Options,
s.Grow.Set(1, 0)
})
core.NewText(fr).SetText(header).Styler(func(s *styles.Style) {
+ TextStyler(s)
s.SetTextWrap(false)
- s.Font.Family = printer.Settings.FontFamily
s.Font.Slant = rich.Italic
s.Font.Size = printer.Settings.FontSize
})
core.NewStretch(fr)
core.NewText(fr).SetText(strconv.Itoa(pageNo)).Styler(func(s *styles.Style) {
+ TextStyler(s)
s.SetTextWrap(false)
- s.Font.Family = printer.Settings.FontFamily
s.Font.Size = printer.Settings.FontSize
})
core.NewSpace(frame).Styler(func(s *styles.Style) { // space after
@@ -86,7 +96,7 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
s.Min.Y.Em(.1)
})
core.NewText(fr).SetText(title).Styler(func(s *styles.Style) {
- s.Font.Family = printer.Settings.FontFamily
+ TextStyler(s)
s.Font.Size = printer.Settings.FontSize
s.Font.Size.Value *= 16.0 / 11
s.Text.Align = text.Center
@@ -94,7 +104,7 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
if authors != "" {
core.NewText(fr).SetText(authors).Styler(func(s *styles.Style) {
- s.Font.Family = printer.Settings.FontFamily
+ TextStyler(s)
s.Font.Size = printer.Settings.FontSize
s.Font.Size.Value *= 12.0 / 11
s.Text.Align = text.Center
@@ -103,7 +113,7 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
if affiliations != "" {
core.NewText(fr).SetText(affiliations).Styler(func(s *styles.Style) {
- s.Font.Family = printer.Settings.FontFamily
+ TextStyler(s)
s.Font.Size = printer.Settings.FontSize
s.Text.Align = text.Center
s.Text.LineHeight = 1.1
@@ -113,7 +123,7 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
if date != "" {
core.NewText(fr).SetText(date).Styler(func(s *styles.Style) {
- s.Font.Family = printer.Settings.FontFamily
+ TextStyler(s)
s.Font.Size = printer.Settings.FontSize
s.Text.Align = text.Center
})
@@ -121,7 +131,7 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
if url != "" {
core.NewText(fr).SetText(url).Styler(func(s *styles.Style) {
- s.Font.Family = printer.Settings.FontFamily
+ TextStyler(s)
s.Font.Size = printer.Settings.FontSize
s.Text.Align = text.Center
})
@@ -129,14 +139,14 @@ func CenteredTitle(title, authors, affiliations, url, date, abstract string) fun
if abstract != "" {
core.NewText(fr).SetText("Abstract:").Styler(func(s *styles.Style) {
- s.Font.Family = printer.Settings.FontFamily
+ TextStyler(s)
s.Font.Size = printer.Settings.FontSize
s.Font.Size.Value *= 12.0 / 11
s.Font.Weight = rich.Bold
s.Align.Self = styles.Start
})
core.NewText(fr).SetText(abstract).Styler(func(s *styles.Style) {
- s.Font.Family = printer.Settings.FontFamily
+ TextStyler(s)
s.Font.Size = printer.Settings.FontSize
s.Text.LineHeight = printer.Settings.LineHeight
s.Align.Self = styles.Start
From 80f098a17456f01ac8f1a12a846a7ae80f7ced9c Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 20 Nov 2025 10:07:53 +0100
Subject: [PATCH 71/99] pdf: tables have less space between rows -- standard
formatting for tables in print.
---
content/handlers.go | 23 +++++++++++++----------
1 file changed, 13 insertions(+), 10 deletions(-)
diff --git a/content/handlers.go b/content/handlers.go
index 0d83d010cc..a08213e405 100644
--- a/content/handlers.go
+++ b/content/handlers.go
@@ -154,7 +154,11 @@ func (ct *Content) widgetHandler(w core.Widget) {
case *core.Text:
hdr := len(tag) > 0 && tag[0] == 'h'
x.Styler(func(s *styles.Style) {
- s.Margin.SetVertical(units.Em(core.ConstantSpacing(0.25)))
+ if tag == "td" {
+ s.Margin.SetVertical(units.Em(0)) // use gap
+ } else {
+ s.Margin.SetVertical(units.Em(core.ConstantSpacing(0.25)))
+ }
s.Font.Size.Value *= core.AppearanceSettings.DocsFontSize / 100
s.Max.X.In(8) // big enough to not constrain PDF render
if hdr {
@@ -179,18 +183,17 @@ func (ct *Content) widgetHandler(w core.Widget) {
case *core.Frame:
switch tag {
case "table":
+ if id != "" {
+ lbl := ct.currentPage.SpecialLabel(id)
+ cp := "" + lbl + ":"
+ if title != "" {
+ cp += " " + title
+ }
+ ct.moveToBlockFrame(w, id, cp, true)
+ }
x.Styler(func(s *styles.Style) {
s.Align.Self = styles.Center
})
- if id == "" {
- break
- }
- lbl := ct.currentPage.SpecialLabel(id)
- cp := "" + lbl + ":"
- if title != "" {
- cp += " " + title
- }
- ct.moveToBlockFrame(w, id, cp, true)
}
}
}
From 59071c03153e23473d4257ba3bb2cdd947324df0 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sat, 22 Nov 2025 08:44:49 +0100
Subject: [PATCH 72/99] pdf: change matcolor scheme to use 100 and 0 for
Background: the almost-white-but-kinda-faded-and-weird background is just not
good, and I don't care what the material 3 specs say. if we need to, we can
have an option to enable it, but this has always bugged me and I think this
looks 100% better.
---
colors/matcolor/scheme.go | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/colors/matcolor/scheme.go b/colors/matcolor/scheme.go
index 61ad40b2db..d89a2670f6 100644
--- a/colors/matcolor/scheme.go
+++ b/colors/matcolor/scheme.go
@@ -118,7 +118,7 @@ func NewLightScheme(p *Palette) Scheme {
SurfaceDim: p.Neutral.AbsToneUniform(87),
Surface: p.Neutral.AbsToneUniform(98),
- SurfaceBright: p.Neutral.AbsToneUniform(98),
+ SurfaceBright: p.Neutral.AbsToneUniform(99),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(100),
SurfaceContainerLow: p.Neutral.AbsToneUniform(96),
@@ -134,7 +134,7 @@ func NewLightScheme(p *Palette) Scheme {
InverseOnSurface: p.Neutral.AbsToneUniform(95),
InversePrimary: p.Primary.AbsToneUniform(80),
- Background: p.Neutral.AbsToneUniform(98),
+ Background: p.Neutral.AbsToneUniform(100),
OnBackground: p.Neutral.AbsToneUniform(10),
Outline: p.NeutralVariant.AbsToneUniform(50),
@@ -182,7 +182,7 @@ func NewDarkScheme(p *Palette) Scheme {
InverseOnSurface: p.Neutral.AbsToneUniform(20),
InversePrimary: p.Primary.AbsToneUniform(40),
- Background: p.Neutral.AbsToneUniform(6),
+ Background: p.Neutral.AbsToneUniform(0),
OnBackground: p.Neutral.AbsToneUniform(90),
Outline: p.NeutralVariant.AbsToneUniform(60),
From 5b9ee06409b0d337e364b9cdc2cb2fad08efe274 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sat, 22 Nov 2025 09:05:12 +0100
Subject: [PATCH 73/99] pdf: matcolor scheme also needs to brighten the surface
values a notch to match brighter background.
---
colors/matcolor/scheme.go | 10 +++++-----
1 file changed, 5 insertions(+), 5 deletions(-)
diff --git a/colors/matcolor/scheme.go b/colors/matcolor/scheme.go
index d89a2670f6..30f0c2905a 100644
--- a/colors/matcolor/scheme.go
+++ b/colors/matcolor/scheme.go
@@ -117,14 +117,14 @@ func NewLightScheme(p *Palette) Scheme {
Custom: map[string]Accent{},
SurfaceDim: p.Neutral.AbsToneUniform(87),
- Surface: p.Neutral.AbsToneUniform(98),
+ Surface: p.Neutral.AbsToneUniform(99),
SurfaceBright: p.Neutral.AbsToneUniform(99),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(100),
- SurfaceContainerLow: p.Neutral.AbsToneUniform(96),
- SurfaceContainer: p.Neutral.AbsToneUniform(94),
- SurfaceContainerHigh: p.Neutral.AbsToneUniform(92),
- SurfaceContainerHighest: p.Neutral.AbsToneUniform(90),
+ SurfaceContainerLow: p.Neutral.AbsToneUniform(98),
+ SurfaceContainer: p.Neutral.AbsToneUniform(96),
+ SurfaceContainerHigh: p.Neutral.AbsToneUniform(94),
+ SurfaceContainerHighest: p.Neutral.AbsToneUniform(92),
SurfaceVariant: p.NeutralVariant.AbsToneUniform(90),
OnSurface: p.NeutralVariant.AbsToneUniform(10),
From 2bd13fd91c9640d6635361fcb161f44c6ce1a7d2 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sun, 30 Nov 2025 21:29:47 +0100
Subject: [PATCH 74/99] pdf: minor math32 vector reordering and doc
---
gpu/doc.go | 2 +-
math32/vector2.go | 360 +++++++++++++++++++++++----------------------
math32/vector2i.go | 4 +-
3 files changed, 187 insertions(+), 179 deletions(-)
diff --git a/gpu/doc.go b/gpu/doc.go
index 8097babee6..fcb240abcc 100644
--- a/gpu/doc.go
+++ b/gpu/doc.go
@@ -5,7 +5,7 @@
/*
Package gpu implements a convenient interface to the WebGPU
graphics and compute framework, in Go, using the
-... bindings
+[github.com/cogentcore/webgpu] bindings.
The Cogent Core GUI framework runs on top of this.
*/
diff --git a/math32/vector2.go b/math32/vector2.go
index 8dc6c84d70..3fdbe1d138 100644
--- a/math32/vector2.go
+++ b/math32/vector2.go
@@ -72,128 +72,11 @@ func (v *Vector2) SetFromVector2i(vi Vector2i) {
v.Y = float32(vi.Y)
}
-// SetDim sets the given vector component value by its dimension index.
-func (v *Vector2) SetDim(dim Dims, value float32) {
- switch dim {
- case X:
- v.X = value
- case Y:
- v.Y = value
- default:
- panic("dim is out of range")
- }
-}
-
-// Dim returns the given vector component.
-func (v Vector2) Dim(dim Dims) float32 {
- switch dim {
- case X:
- return v.X
- case Y:
- return v.Y
- default:
- panic("dim is out of range")
- }
-}
-
-// SetPointDim sets the given dimension of the given [image.Point] to the given value.
-func SetPointDim(pt *image.Point, dim Dims, value int) {
- switch dim {
- case X:
- pt.X = value
- case Y:
- pt.Y = value
- default:
- panic("dim is out of range")
- }
-}
-
-// PointDim returns the given dimension of the given [image.Point].
-func PointDim(pt image.Point, dim Dims) int {
- switch dim {
- case X:
- return pt.X
- case Y:
- return pt.Y
- default:
- panic("dim is out of range")
- }
-}
-
-func (a Vector2) String() string {
- return fmt.Sprintf("(%v, %v)", a.X, a.Y)
-}
-
-// SetPoint sets the vector from the given [image.Point].
-func (a *Vector2) SetPoint(pt image.Point) {
- a.X = float32(pt.X)
- a.Y = float32(pt.Y)
-}
-
-// SetFixed sets the vector from the given [fixed.Point26_6].
-func (a *Vector2) SetFixed(pt fixed.Point26_6) {
- a.X = FromFixed(pt.X)
- a.Y = FromFixed(pt.Y)
-}
-
-// ToPoint returns the vector as an [image.Point].
-func (a Vector2) ToPoint() image.Point {
- return image.Point{int(a.X), int(a.Y)}
-}
-
-// ToPointFloor returns the vector as an [image.Point] with all values [Floor]ed.
-func (a Vector2) ToPointFloor() image.Point {
- return image.Point{int(Floor(a.X)), int(Floor(a.Y))}
-}
-
-// ToPointCeil returns the vector as an [image.Point] with all values [Ceil]ed.
-func (a Vector2) ToPointCeil() image.Point {
- return image.Point{int(Ceil(a.X)), int(Ceil(a.Y))}
-}
-
-// ToPointRound returns the vector as an [image.Point] with all values [Round]ed.
-func (a Vector2) ToPointRound() image.Point {
- return image.Point{int(Round(a.X)), int(Round(a.Y))}
-}
-
-// ToFixed returns the vector as a [fixed.Point26_6].
-func (a Vector2) ToFixed() fixed.Point26_6 {
- return ToFixedPoint(a.X, a.Y)
-}
-
-// RectFromPosSizeMax returns an [image.Rectangle] from the floor of pos
-// and ceil of size.
-func RectFromPosSizeMax(pos, size Vector2) image.Rectangle {
- tp := pos.ToPointFloor()
- ts := size.ToPointCeil()
- return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
-}
-
-// RectFromPosSizeMin returns an [image.Rectangle] from the ceil of pos
-// and floor of size.
-func RectFromPosSizeMin(pos, size Vector2) image.Rectangle {
- tp := pos.ToPointCeil()
- ts := size.ToPointFloor()
- return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
-}
-
// SetZero sets all of the vector's components to zero.
func (v *Vector2) SetZero() {
v.SetScalar(0)
}
-// FromSlice sets this vector's components from the given slice, starting at offset.
-func (v *Vector2) FromSlice(slice []float32, offset int) {
- v.X = slice[offset]
- v.Y = slice[offset+1]
-}
-
-// ToSlice copies this vector's components to the given slice, starting at offset.
-func (v Vector2) ToSlice(slice []float32, offset int) {
- slice[offset] = v.X
- slice[offset+1] = v.Y
-}
-
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
@@ -336,70 +219,11 @@ func (v *Vector2) Clamp(min, max Vector2) {
}
}
-// Floor returns this vector with [Floor] applied to each of its components.
-func (v Vector2) Floor() Vector2 {
- return Vec2(Floor(v.X), Floor(v.Y))
-}
-
-// Ceil returns this vector with [Ceil] applied to each of its components.
-func (v Vector2) Ceil() Vector2 {
- return Vec2(Ceil(v.X), Ceil(v.Y))
-}
-
-// Round returns this vector with [Round] applied to each of its components.
-func (v Vector2) Round() Vector2 {
- return Vec2(Round(v.X), Round(v.Y))
-}
-
// Negate returns the vector with each component negated.
func (v Vector2) Negate() Vector2 {
return Vec2(-v.X, -v.Y)
}
-// AddDim returns the vector with the given value added on the given dimension.
-func (a Vector2) AddDim(d Dims, value float32) Vector2 {
- switch d {
- case X:
- a.X += value
- case Y:
- a.Y += value
- }
- return a
-}
-
-// SubDim returns the vector with the given value subtracted on the given dimension.
-func (a Vector2) SubDim(d Dims, value float32) Vector2 {
- switch d {
- case X:
- a.X -= value
- case Y:
- a.Y -= value
- }
- return a
-}
-
-// MulDim returns the vector with the given value multiplied by on the given dimension.
-func (a Vector2) MulDim(d Dims, value float32) Vector2 {
- switch d {
- case X:
- a.X *= value
- case Y:
- a.Y *= value
- }
- return a
-}
-
-// DivDim returns the vector with the given value divided by on the given dimension.
-func (a Vector2) DivDim(d Dims, value float32) Vector2 {
- switch d {
- case X:
- a.X /= value
- case Y:
- a.Y /= value
- }
- return a
-}
-
// Distance, Normal:
// Dot returns the dot product of this vector with the given other vector.
@@ -498,3 +322,187 @@ func (v Vector2) Rot(phi float32, p0 Vector2) Vector2 {
p0.Y + sinphi*(v.X-p0.X) + cosphi*(v.Y-p0.Y),
}
}
+
+func (a Vector2) String() string {
+ return fmt.Sprintf("(%v, %v)", a.X, a.Y)
+}
+
+// SetDim sets the given vector component value by its dimension index.
+func (v *Vector2) SetDim(dim Dims, value float32) {
+ switch dim {
+ case X:
+ v.X = value
+ case Y:
+ v.Y = value
+ default:
+ panic("dim is out of range")
+ }
+}
+
+// Dim returns the given vector component.
+func (v Vector2) Dim(dim Dims) float32 {
+ switch dim {
+ case X:
+ return v.X
+ case Y:
+ return v.Y
+ default:
+ panic("dim is out of range")
+ }
+}
+
+// SetPointDim sets the given dimension of the given [image.Point] to the given value.
+func SetPointDim(pt *image.Point, dim Dims, value int) {
+ switch dim {
+ case X:
+ pt.X = value
+ case Y:
+ pt.Y = value
+ default:
+ panic("dim is out of range")
+ }
+}
+
+// PointDim returns the given dimension of the given [image.Point].
+func PointDim(pt image.Point, dim Dims) int {
+ switch dim {
+ case X:
+ return pt.X
+ case Y:
+ return pt.Y
+ default:
+ panic("dim is out of range")
+ }
+}
+
+// AddDim returns the vector with the given value added on the given dimension.
+func (a Vector2) AddDim(d Dims, value float32) Vector2 {
+ switch d {
+ case X:
+ a.X += value
+ case Y:
+ a.Y += value
+ default:
+ panic("dim is out of range")
+ }
+ return a
+}
+
+// SubDim returns the vector with the given value subtracted on the given dimension.
+func (a Vector2) SubDim(d Dims, value float32) Vector2 {
+ switch d {
+ case X:
+ a.X -= value
+ case Y:
+ a.Y -= value
+ default:
+ panic("dim is out of range")
+ }
+ return a
+}
+
+// MulDim returns the vector with the given value multiplied by on the given dimension.
+func (a Vector2) MulDim(d Dims, value float32) Vector2 {
+ switch d {
+ case X:
+ a.X *= value
+ case Y:
+ a.Y *= value
+ default:
+ panic("dim is out of range")
+ }
+ return a
+}
+
+// DivDim returns the vector with the given value divided by on the given dimension.
+func (a Vector2) DivDim(d Dims, value float32) Vector2 {
+ switch d {
+ case X:
+ a.X /= value
+ case Y:
+ a.Y /= value
+ default:
+ panic("dim is out of range")
+ }
+ return a
+}
+
+// SetPoint sets the vector from the given [image.Point].
+func (a *Vector2) SetPoint(pt image.Point) {
+ a.X = float32(pt.X)
+ a.Y = float32(pt.Y)
+}
+
+// SetFixed sets the vector from the given [fixed.Point26_6].
+func (a *Vector2) SetFixed(pt fixed.Point26_6) {
+ a.X = FromFixed(pt.X)
+ a.Y = FromFixed(pt.Y)
+}
+
+// ToPoint returns the vector as an [image.Point].
+func (a Vector2) ToPoint() image.Point {
+ return image.Point{int(a.X), int(a.Y)}
+}
+
+// ToPointFloor returns the vector as an [image.Point] with all values [Floor]ed.
+func (a Vector2) ToPointFloor() image.Point {
+ return image.Point{int(Floor(a.X)), int(Floor(a.Y))}
+}
+
+// ToPointCeil returns the vector as an [image.Point] with all values [Ceil]ed.
+func (a Vector2) ToPointCeil() image.Point {
+ return image.Point{int(Ceil(a.X)), int(Ceil(a.Y))}
+}
+
+// ToPointRound returns the vector as an [image.Point] with all values [Round]ed.
+func (a Vector2) ToPointRound() image.Point {
+ return image.Point{int(Round(a.X)), int(Round(a.Y))}
+}
+
+// ToFixed returns the vector as a [fixed.Point26_6].
+func (a Vector2) ToFixed() fixed.Point26_6 {
+ return ToFixedPoint(a.X, a.Y)
+}
+
+// RectFromPosSizeMax returns an [image.Rectangle] from the floor of pos
+// and ceil of size.
+func RectFromPosSizeMax(pos, size Vector2) image.Rectangle {
+ tp := pos.ToPointFloor()
+ ts := size.ToPointCeil()
+ return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
+}
+
+// RectFromPosSizeMin returns an [image.Rectangle] from the ceil of pos
+// and floor of size.
+func RectFromPosSizeMin(pos, size Vector2) image.Rectangle {
+ tp := pos.ToPointCeil()
+ ts := size.ToPointFloor()
+ return image.Rect(tp.X, tp.Y, tp.X+ts.X, tp.Y+ts.Y)
+}
+
+// FromSlice sets this vector's components from the given slice, starting at offset.
+func (v *Vector2) FromSlice(slice []float32, offset int) {
+ v.X = slice[offset]
+ v.Y = slice[offset+1]
+}
+
+// ToSlice copies this vector's components to the given slice, starting at offset.
+func (v Vector2) ToSlice(slice []float32, offset int) {
+ slice[offset] = v.X
+ slice[offset+1] = v.Y
+}
+
+// Floor returns this vector with [Floor] applied to each of its components.
+func (v Vector2) Floor() Vector2 {
+ return Vec2(Floor(v.X), Floor(v.Y))
+}
+
+// Ceil returns this vector with [Ceil] applied to each of its components.
+func (v Vector2) Ceil() Vector2 {
+ return Vec2(Ceil(v.X), Ceil(v.Y))
+}
+
+// Round returns this vector with [Round] applied to each of its components.
+func (v Vector2) Round() Vector2 {
+ return Vec2(Round(v.X), Round(v.Y))
+}
diff --git a/math32/vector2i.go b/math32/vector2i.go
index 9fa8e2cfc9..ffe60df6b9 100644
--- a/math32/vector2i.go
+++ b/math32/vector2i.go
@@ -17,8 +17,8 @@ type Vector2i struct {
}
// Vec2i returns a new [Vector2i] with the given x and y components.
-func Vec2i(x, y int32) Vector2i {
- return Vector2i{X: x, Y: y}
+func Vec2i(x, y int) Vector2i {
+ return Vector2i{X: int32(x), Y: int32(y)}
}
// Vector2iScalar returns a new [Vector2i] with all components set to the given scalar value.
From a352e4501a714c4d01ab18914ea3a05f070517f8 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 15 Dec 2025 06:57:39 +0100
Subject: [PATCH 75/99] pdf: rename the awkward MulVector2As methods to simpler
MulVector and MulPoint
---
colors/gradient/linear.go | 10 +++----
colors/gradient/radial.go | 12 ++++----
gpu/drawmatrix/matrix.go | 4 +--
math32/box2.go | 8 +++---
math32/box3.go | 7 +++++
math32/matrix2.go | 10 ++++---
math32/matrix2_test.go | 44 ++++++++++++++---------------
math32/matrix3.go | 11 ++++----
math32/matrix3_test.go | 26 ++++++++---------
math32/quaternion.go | 25 ++++++----------
math32/quaternion_test.go | 3 +-
math32/vector3.go | 18 +++++-------
paint/painter.go | 8 +++---
paint/pdf/links.go | 2 +-
paint/pdf/page.go | 8 +++---
paint/ppath/transform.go | 14 ++++-----
paint/renderers/rasterx/renderer.go | 8 +++---
paint/renderers/rasterx/shapes.go | 2 +-
paint/renderers/rasterx/text.go | 28 +++++++++---------
svg/circle.go | 2 +-
svg/ellipse.go | 4 +--
svg/image.go | 4 +--
svg/line.go | 4 +--
svg/node.go | 4 +--
svg/polyline.go | 2 +-
svg/rect.go | 4 +--
svg/text.go | 4 +--
svg/zoom.go | 4 +--
28 files changed, 138 insertions(+), 142 deletions(-)
diff --git a/colors/gradient/linear.go b/colors/gradient/linear.go
index 011005810f..d12b8dd937 100644
--- a/colors/gradient/linear.go
+++ b/colors/gradient/linear.go
@@ -65,10 +65,10 @@ func (l *Linear) Update(opacity float32, box math32.Box2, objTransform math32.Ma
l.rStart = l.Box.Min.Add(sz.Mul(l.Start))
l.rEnd = l.Box.Min.Add(sz.Mul(l.End))
} else {
- l.rStart = l.Transform.MulVector2AsPoint(l.Start)
- l.rEnd = l.Transform.MulVector2AsPoint(l.End)
- l.rStart = objTransform.MulVector2AsPoint(l.rStart)
- l.rEnd = objTransform.MulVector2AsPoint(l.rEnd)
+ l.rStart = l.Transform.MulPoint(l.Start)
+ l.rEnd = l.Transform.MulPoint(l.End)
+ l.rStart = objTransform.MulPoint(l.rStart)
+ l.rEnd = objTransform.MulPoint(l.rEnd)
}
l.distance = l.rEnd.Sub(l.rStart)
@@ -92,7 +92,7 @@ func (l *Linear) At(x, y int) color.Color {
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if l.Units == ObjectBoundingBox {
- pt = l.boxTransform.MulVector2AsPoint(pt)
+ pt = l.boxTransform.MulPoint(pt)
}
df := pt.Sub(l.rStart)
pos := (l.distance.X*df.X + l.distance.Y*df.Y) / l.distanceLengthSquared
diff --git a/colors/gradient/radial.go b/colors/gradient/radial.go
index 86a33e7ebe..8326ad8777 100644
--- a/colors/gradient/radial.go
+++ b/colors/gradient/radial.go
@@ -74,8 +74,8 @@ func (r *Radial) Update(opacity float32, box math32.Box2, objTransform math32.Ma
rs.SetMul(sz)
} else {
ct := objTransform.Mul(r.Transform)
- c = ct.MulVector2AsPoint(c)
- f = ct.MulVector2AsPoint(f)
+ c = ct.MulPoint(c)
+ f = ct.MulPoint(f)
_, _, phi, sx, sy, _ := ct.Decompose()
r.rotTrans = math32.Rotate2D(phi)
rs.SetMul(math32.Vec2(sx, sy))
@@ -118,9 +118,9 @@ func (r *Radial) At(x, y int) color.Color {
// pos is just distance from center scaled by radius
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if r.Units == ObjectBoundingBox {
- pt = r.boxTransform.MulVector2AsPoint(pt)
+ pt = r.boxTransform.MulPoint(pt)
}
- d := r.rotTrans.MulVector2AsVector(pt.Sub(r.rCenter))
+ d := r.rotTrans.MulVector(pt.Sub(r.rCenter))
pos := math32.Sqrt(d.X*d.X/(r.rRadius.X*r.rRadius.X) + (d.Y*d.Y)/(r.rRadius.Y*r.rRadius.Y))
return r.getColor(pos)
}
@@ -130,9 +130,9 @@ func (r *Radial) At(x, y int) color.Color {
pt := math32.Vec2(float32(x)+0.5, float32(y)+0.5)
if r.Units == ObjectBoundingBox {
- pt = r.boxTransform.MulVector2AsPoint(pt)
+ pt = r.boxTransform.MulPoint(pt)
}
- pt = r.rotTrans.MulVector2AsVector(pt)
+ pt = r.rotTrans.MulVector(pt)
e := pt.Div(r.rRadius)
t1, intersects := rayCircleIntersectionF(e, r.rFocal, r.rCenter, 1)
diff --git a/gpu/drawmatrix/matrix.go b/gpu/drawmatrix/matrix.go
index 675c2cad22..92487d8cb0 100644
--- a/gpu/drawmatrix/matrix.go
+++ b/gpu/drawmatrix/matrix.go
@@ -159,8 +159,8 @@ func Transform(dr image.Rectangle, sr image.Rectangle, rotDeg float32) math32.Ma
dsz := math32.FromPoint(dr.Size())
rmat := math32.Rotate2D(rad)
- dmnr := rmat.MulVector2AsPoint(math32.FromPoint(dr.Min))
- dmxr := rmat.MulVector2AsPoint(math32.FromPoint(dr.Max))
+ dmnr := rmat.MulPoint(math32.FromPoint(dr.Min))
+ dmxr := rmat.MulPoint(math32.FromPoint(dr.Max))
sx = math32.Abs(dmxr.X-dmnr.X) / float32(sr.Dx())
sy = math32.Abs(dmxr.Y-dmnr.Y) / float32(sr.Dy())
tx = dmnr.X - sx*float32(sr.Min.X)
diff --git a/math32/box2.go b/math32/box2.go
index b52230c042..ae1c004142 100644
--- a/math32/box2.go
+++ b/math32/box2.go
@@ -157,10 +157,10 @@ func (b *Box2) ExpandByBox(box Box2) {
// and computes the resulting spanning Box2 of the transformed points
func (b Box2) MulMatrix2(m Matrix2) Box2 {
var cs [4]Vector2
- cs[0] = m.MulVector2AsPoint(Vec2(b.Min.X, b.Min.Y))
- cs[1] = m.MulVector2AsPoint(Vec2(b.Min.X, b.Max.Y))
- cs[2] = m.MulVector2AsPoint(Vec2(b.Max.X, b.Min.Y))
- cs[3] = m.MulVector2AsPoint(Vec2(b.Max.X, b.Max.Y))
+ cs[0] = m.MulPoint(Vec2(b.Min.X, b.Min.Y))
+ cs[1] = m.MulPoint(Vec2(b.Min.X, b.Max.Y))
+ cs[2] = m.MulPoint(Vec2(b.Max.X, b.Min.Y))
+ cs[3] = m.MulPoint(Vec2(b.Max.X, b.Max.Y))
nb := B2Empty()
for i := 0; i < 4; i++ {
diff --git a/math32/box3.go b/math32/box3.go
index e989ae19a9..86092360c8 100644
--- a/math32/box3.go
+++ b/math32/box3.go
@@ -55,6 +55,13 @@ func (b *Box3) Set(min, max *Vector3) {
}
}
+// SetMinMax sets this bounding box minimum and maximum coordinates
+// using specific values (see also Set)
+func (b *Box3) SetMinMax(min, max Vector3) {
+ b.Min = min
+ b.Max = max
+}
+
// SetFromPoints sets this bounding box from the specified array of points.
func (b *Box3) SetFromPoints(points []Vector3) {
b.SetEmpty()
diff --git a/math32/matrix2.go b/math32/matrix2.go
index 34a7e456fa..0a5468cb8c 100644
--- a/math32/matrix2.go
+++ b/math32/matrix2.go
@@ -133,16 +133,18 @@ func (a *Matrix2) SetMul(b Matrix2) {
*a = a.Mul(b)
}
-// MulVector2AsVector multiplies the Vector2 as a vector without adding translations.
+// todo: rename to MulVector, MulPoint
+
+// MulVector multiplies the Vector2 as a vector without adding translations.
// This is for directional vectors and not points.
-func (a Matrix2) MulVector2AsVector(v Vector2) Vector2 {
+func (a Matrix2) MulVector(v Vector2) Vector2 {
tx := a.XX*v.X + a.XY*v.Y
ty := a.YX*v.X + a.YY*v.Y
return Vec2(tx, ty)
}
-// MulVector2AsPoint multiplies the Vector2 as a point, including adding translations.
-func (a Matrix2) MulVector2AsPoint(v Vector2) Vector2 {
+// MulPoint multiplies the Vector2 as a point, including adding translations.
+func (a Matrix2) MulPoint(v Vector2) Vector2 {
tx := a.XX*v.X + a.XY*v.Y + a.X0
ty := a.YX*v.X + a.YY*v.Y + a.Y0
return Vec2(tx, ty)
diff --git a/math32/matrix2_test.go b/math32/matrix2_test.go
index b519457e00..a78bfcb70c 100644
--- a/math32/matrix2_test.go
+++ b/math32/matrix2_test.go
@@ -45,24 +45,24 @@ func TestMatrix2(t *testing.T) {
rot90 := DegToRad(90)
rot45 := DegToRad(45)
- assert.Equal(t, vx, Identity3().MulVector2AsPoint(vx))
- assert.Equal(t, vy, Identity3().MulVector2AsPoint(vy))
- assert.Equal(t, vxy, Identity3().MulVector2AsPoint(vxy))
+ assert.Equal(t, vx, Identity3().MulPoint(vx))
+ assert.Equal(t, vy, Identity3().MulPoint(vy))
+ assert.Equal(t, vxy, Identity3().MulPoint(vxy))
- assert.Equal(t, vxy, Translate2D(1, 1).MulVector2AsPoint(v0))
+ assert.Equal(t, vxy, Translate2D(1, 1).MulPoint(v0))
- assert.Equal(t, vxy.MulScalar(2), Scale2D(2, 2).MulVector2AsPoint(vxy))
+ assert.Equal(t, vxy.MulScalar(2), Scale2D(2, 2).MulPoint(vxy))
- tolAssertEqualVector(t, vy, Rotate2D(rot90).MulVector2AsPoint(vx)) // left, CCW
- tolAssertEqualVector(t, vx, Rotate2D(-rot90).MulVector2AsPoint(vy)) // right, CW
- tolAssertEqualVector(t, vxy.Normal(), Rotate2D(rot45).MulVector2AsPoint(vx))
- tolAssertEqualVector(t, vxy.Normal(), Rotate2D(-rot45).MulVector2AsPoint(vy))
+ tolAssertEqualVector(t, vy, Rotate2D(rot90).MulPoint(vx)) // left, CCW
+ tolAssertEqualVector(t, vx, Rotate2D(-rot90).MulPoint(vy)) // right, CW
+ tolAssertEqualVector(t, vxy.Normal(), Rotate2D(rot45).MulPoint(vx))
+ tolAssertEqualVector(t, vxy.Normal(), Rotate2D(-rot45).MulPoint(vy))
- tolAssertEqualVector(t, vy, Rotate2D(-rot90).Inverse().MulVector2AsPoint(vx))
- tolAssertEqualVector(t, vx, Rotate2D(rot90).Inverse().MulVector2AsPoint(vy))
+ tolAssertEqualVector(t, vy, Rotate2D(-rot90).Inverse().MulPoint(vx))
+ tolAssertEqualVector(t, vx, Rotate2D(rot90).Inverse().MulPoint(vy))
- tolAssertEqualVector(t, vxy, Rotate2D(-rot45).Mul(Rotate2D(rot45)).MulVector2AsPoint(vxy))
- tolAssertEqualVector(t, vxy, Rotate2D(-rot45).Mul(Rotate2D(-rot45).Inverse()).MulVector2AsPoint(vxy))
+ tolAssertEqualVector(t, vxy, Rotate2D(-rot45).Mul(Rotate2D(rot45)).MulPoint(vxy))
+ tolAssertEqualVector(t, vxy, Rotate2D(-rot45).Mul(Rotate2D(-rot45).Inverse()).MulPoint(vxy))
tolassert.EqualTol(t, -rot90, Rotate2D(-rot90).ExtractRot(), standardTol)
tolassert.EqualTol(t, -rot45, Rotate2D(-rot45).ExtractRot(), standardTol)
@@ -71,7 +71,7 @@ func TestMatrix2(t *testing.T) {
// 1,0 -> scale(2) = 2,0 -> rotate 90 = 0,2 -> trans 1,1 -> 1,3
// multiplication order is *reverse* of "logical" order:
- tolAssertEqualVector(t, Vec2(1, 3), Translate2D(1, 1).Mul(Rotate2D(rot90)).Mul(Scale2D(2, 2)).MulVector2AsPoint(vx))
+ tolAssertEqualVector(t, Vec2(1, 3), Translate2D(1, 1).Mul(Rotate2D(rot90)).Mul(Scale2D(2, 2)).MulPoint(vx))
}
@@ -153,14 +153,14 @@ func TestMatrix2Canvas(t *testing.T) {
p := Vector2{3, 4}
rot90 := DegToRad(90)
rot45 := DegToRad(45)
- tolAssertEqualVector(t, Identity2().Translate(2.0, 2.0).MulVector2AsPoint(p), Vector2{5.0, 6.0})
- tolAssertEqualVector(t, Identity2().Scale(2.0, 2.0).MulVector2AsPoint(p), Vector2{6.0, 8.0})
- tolAssertEqualVector(t, Identity2().Scale(1.0, -1.0).MulVector2AsPoint(p), Vector2{3.0, -4.0})
- tolAssertEqualVector(t, Identity2().ScaleAbout(2.0, -1.0, 2.0, 2.0).MulVector2AsPoint(p), Vector2{4.0, 0.0})
- tolAssertEqualVector(t, Identity2().Shear(1.0, 0.0).MulVector2AsPoint(p), Vector2{7.0, 4.0})
- tolAssertEqualVector(t, Identity2().Rotate(rot90).MulVector2AsPoint(p), p.Rot90CCW())
- tolAssertEqualVector(t, Identity2().RotateAbout(rot90, 5.0, 5.0).MulVector2AsPoint(p), p.Rot(90.0*Pi/180.0, Vector2{5.0, 5.0}))
- tolAssertEqualVector(t, Identity2().Rotate(rot90).Transpose().MulVector2AsPoint(p), p.Rot90CW())
+ tolAssertEqualVector(t, Identity2().Translate(2.0, 2.0).MulPoint(p), Vector2{5.0, 6.0})
+ tolAssertEqualVector(t, Identity2().Scale(2.0, 2.0).MulPoint(p), Vector2{6.0, 8.0})
+ tolAssertEqualVector(t, Identity2().Scale(1.0, -1.0).MulPoint(p), Vector2{3.0, -4.0})
+ tolAssertEqualVector(t, Identity2().ScaleAbout(2.0, -1.0, 2.0, 2.0).MulPoint(p), Vector2{4.0, 0.0})
+ tolAssertEqualVector(t, Identity2().Shear(1.0, 0.0).MulPoint(p), Vector2{7.0, 4.0})
+ tolAssertEqualVector(t, Identity2().Rotate(rot90).MulPoint(p), p.Rot90CCW())
+ tolAssertEqualVector(t, Identity2().RotateAbout(rot90, 5.0, 5.0).MulPoint(p), p.Rot(90.0*Pi/180.0, Vector2{5.0, 5.0}))
+ tolAssertEqualVector(t, Identity2().Rotate(rot90).Transpose().MulPoint(p), p.Rot90CW())
tolAssertEqualMatrix2(t, Identity2().Scale(2.0, 4.0).Inverse(), Identity2().Scale(0.5, 0.25))
tolAssertEqualMatrix2(t, Identity2().Rotate(rot90).Inverse(), Identity2().Rotate(-rot90))
tolAssertEqualMatrix2(t, Identity2().Rotate(rot90).Scale(2.0, 1.0), Identity2().Scale(1.0, 2.0).Rotate(rot90))
diff --git a/math32/matrix3.go b/math32/matrix3.go
index 00ccd0ae95..65fbbde3e6 100644
--- a/math32/matrix3.go
+++ b/math32/matrix3.go
@@ -186,16 +186,16 @@ func (m *Matrix3) SetMulScalar(s float32) {
m[8] *= s
}
-// MulVector2AsVector multiplies the Vector2 as a vector without adding translations.
+// MulVector multiplies the Vector2 as a vector without adding translations.
// This is for directional vectors and not points.
-func (a Matrix3) MulVector2AsVector(v Vector2) Vector2 {
+func (a Matrix3) MulVector(v Vector2) Vector2 {
tx := a[0]*v.X + a[1]*v.Y
ty := a[3]*v.X + a[4]*v.Y
return Vec2(tx, ty)
}
-// MulVector2AsPoint multiplies the Vector2 as a point, including adding translations.
-func (a Matrix3) MulVector2AsPoint(v Vector2) Vector2 {
+// MulPoint multiplies the Vector2 as a point, including adding translations.
+func (a Matrix3) MulPoint(v Vector2) Vector2 {
tx := a[0]*v.X + a[1]*v.Y + a[2]
ty := a[3]*v.X + a[4]*v.Y + a[5]
return Vec2(tx, ty)
@@ -323,8 +323,7 @@ func (m *Matrix3) SetScaleCols(v Vector3) {
m[8] *= v.Z
}
-/////////////////////////////////////////////////////////////////////////////
-// Special functions
+//////// Special functions
// SetNormalMatrix set this matrix to the matrix that can transform the normal vectors
// from the src matrix which is used transform the vertices (e.g., a ModelView matrix).
diff --git a/math32/matrix3_test.go b/math32/matrix3_test.go
index af8c22d933..9f8bafeb74 100644
--- a/math32/matrix3_test.go
+++ b/math32/matrix3_test.go
@@ -16,27 +16,27 @@ func TestMatrix3(t *testing.T) {
vy := Vec2(0, 1)
vxy := Vec2(1, 1)
- assert.Equal(t, vx, Identity3().MulVector2AsPoint(vx))
- assert.Equal(t, vy, Identity3().MulVector2AsPoint(vy))
- assert.Equal(t, vxy, Identity3().MulVector2AsPoint(vxy))
+ assert.Equal(t, vx, Identity3().MulPoint(vx))
+ assert.Equal(t, vy, Identity3().MulPoint(vy))
+ assert.Equal(t, vxy, Identity3().MulPoint(vxy))
- assert.Equal(t, vxy, Matrix3FromMatrix2(Translate2D(1, 1)).MulVector2AsPoint(v0))
+ assert.Equal(t, vxy, Matrix3FromMatrix2(Translate2D(1, 1)).MulPoint(v0))
- assert.Equal(t, vxy.MulScalar(2), Matrix3FromMatrix2(Scale2D(2, 2)).MulVector2AsPoint(vxy))
+ assert.Equal(t, vxy.MulScalar(2), Matrix3FromMatrix2(Scale2D(2, 2)).MulPoint(vxy))
- tolAssertEqualVector(t, vy, Matrix3FromMatrix2(Rotate2D(DegToRad(90))).MulVector2AsPoint(vx)) // left
- tolAssertEqualVector(t, vx, Matrix3FromMatrix2(Rotate2D(DegToRad(-90))).MulVector2AsPoint(vy)) // right
- tolAssertEqualVector(t, vxy.Normal(), Matrix3FromMatrix2(Rotate2D(DegToRad(45))).MulVector2AsPoint(vx))
- tolAssertEqualVector(t, vxy.Normal(), Matrix3FromMatrix2(Rotate2D(DegToRad(-45))).MulVector2AsPoint(vy))
+ tolAssertEqualVector(t, vy, Matrix3FromMatrix2(Rotate2D(DegToRad(90))).MulPoint(vx)) // left
+ tolAssertEqualVector(t, vx, Matrix3FromMatrix2(Rotate2D(DegToRad(-90))).MulPoint(vy)) // right
+ tolAssertEqualVector(t, vxy.Normal(), Matrix3FromMatrix2(Rotate2D(DegToRad(45))).MulPoint(vx))
+ tolAssertEqualVector(t, vxy.Normal(), Matrix3FromMatrix2(Rotate2D(DegToRad(-45))).MulPoint(vy))
- tolAssertEqualVector(t, vy, Matrix3FromMatrix2(Rotate2D(DegToRad(-90))).Inverse().MulVector2AsPoint(vx)) // left
- tolAssertEqualVector(t, vx, Matrix3FromMatrix2(Rotate2D(DegToRad(90))).Inverse().MulVector2AsPoint(vy)) // right
+ tolAssertEqualVector(t, vy, Matrix3FromMatrix2(Rotate2D(DegToRad(-90))).Inverse().MulPoint(vx)) // left
+ tolAssertEqualVector(t, vx, Matrix3FromMatrix2(Rotate2D(DegToRad(90))).Inverse().MulPoint(vy)) // right
// 1,0 -> scale(2) = 2,0 -> rotate 90 = 0,2 -> trans 1,1 -> 1,3
// multiplication order is *reverse* of "logical" order:
- tolAssertEqualVector(t, Vec2(1, 3), Matrix3Translate2D(1, 1).Mul(Matrix3Rotate2D(DegToRad(90))).Mul(Matrix3Scale2D(2, 2)).MulVector2AsPoint(vx))
+ tolAssertEqualVector(t, Vec2(1, 3), Matrix3Translate2D(1, 1).Mul(Matrix3Rotate2D(DegToRad(90))).Mul(Matrix3Scale2D(2, 2)).MulPoint(vx))
- // xmat := Matrix3Translate2D(1, 1).Mul(Matrix3Rotate2D(DegToRad(90))).Mul(Matrix3Scale2D(2, 2)).MulVector2AsPoint(vx))
+ // xmat := Matrix3Translate2D(1, 1).Mul(Matrix3Rotate2D(DegToRad(90))).Mul(Matrix3Scale2D(2, 2)).MulPoint(vx))
}
func TestMatrix3SetFromMatrix4(t *testing.T) {
diff --git a/math32/quaternion.go b/math32/quaternion.go
index 4f822b2bf8..96bf844ecb 100644
--- a/math32/quaternion.go
+++ b/math32/quaternion.go
@@ -282,27 +282,20 @@ func (q *Quat) NormalizeFast() {
}
}
-// MulQuats set this quaternion to the multiplication of a by b.
-func (q *Quat) MulQuats(a, b Quat) {
+// MulQuats returns a * b quaternion multiplication.
+func MulQuats(a, b Quat) Quat {
// from http://www.euclideanspace.com/maths/algebra/realNormedAlgebra/quaternions/code/index.htm
- qax := a.X
- qay := a.Y
- qaz := a.Z
- qaw := a.W
- qbx := b.X
- qby := b.Y
- qbz := b.Z
- qbw := b.W
-
- q.X = qax*qbw + qaw*qbx + qay*qbz - qaz*qby
- q.Y = qay*qbw + qaw*qby + qaz*qbx - qax*qbz
- q.Z = qaz*qbw + qaw*qbz + qax*qby - qay*qbx
- q.W = qaw*qbw - qax*qbx - qay*qby - qaz*qbz
+ var q Quat
+ q.X = a.X*b.W + a.W*b.X + a.Y*b.Z - a.Z*b.Y
+ q.Y = a.Y*b.W + a.W*b.Y + a.Z*b.X - a.X*b.Z
+ q.Z = a.Z*b.W + a.W*b.Z + a.X*b.Y - a.Y*b.X
+ q.W = a.W*b.W - a.X*b.X - a.Y*b.Y - a.Z*b.Z
+ return q
}
// SetMul sets this quaternion to the multiplication of itself by other.
func (q *Quat) SetMul(other Quat) {
- q.MulQuats(*q, other)
+ *q = MulQuats(*q, other)
}
// Mul returns returns multiplication of this quaternion with other
diff --git a/math32/quaternion_test.go b/math32/quaternion_test.go
index 00615805ba..67f268383e 100644
--- a/math32/quaternion_test.go
+++ b/math32/quaternion_test.go
@@ -104,8 +104,7 @@ func TestQuatMulQuats(t *testing.T) {
q1 := Quat{X: 1, Y: 2, Z: 3, W: 4}
q2 := Quat{X: 5, Y: 6, Z: 7, W: 8}
- q := Quat{}
- q.MulQuats(q1, q2)
+ q := MulQuats(q1, q2)
expected := Quat{X: 24, Y: 48, Z: 48, W: -6}
diff --git a/math32/vector3.go b/math32/vector3.go
index aec01dc91b..0dfff9391c 100644
--- a/math32/vector3.go
+++ b/math32/vector3.go
@@ -416,19 +416,15 @@ func (v Vector3) MulProjection(m *Matrix4) Vector3 {
// then by the quaternion inverse.
// It basically applies the rotation encoded in the quaternion to this vector.
func (v Vector3) MulQuat(q Quat) Vector3 {
- qx := q.X
- qy := q.Y
- qz := q.Z
- qw := q.W
// calculate quat * vector
- ix := qw*v.X + qy*v.Z - qz*v.Y
- iy := qw*v.Y + qz*v.X - qx*v.Z
- iz := qw*v.Z + qx*v.Y - qy*v.X
- iw := -qx*v.X - qy*v.Y - qz*v.Z
+ ix := q.W*v.X + q.Y*v.Z - q.Z*v.Y
+ iy := q.W*v.Y + q.Z*v.X - q.X*v.Z
+ iz := q.W*v.Z + q.X*v.Y - q.Y*v.X
+ iw := -q.X*v.X - q.Y*v.Y - q.Z*v.Z
// calculate result * inverse quat
- return Vector3{ix*qw + iw*-qx + iy*-qz - iz*-qy,
- iy*qw + iw*-qy + iz*-qx - ix*-qz,
- iz*qw + iw*-qz + ix*-qy - iy*-qx}
+ return Vec3(ix*q.W+iw*-q.X+iy*-q.Z-iz*-q.Y,
+ iy*q.W+iw*-q.Y+iz*-q.X-ix*-q.Z,
+ iz*q.W+iw*-q.Z+ix*-q.Y-iy*-q.X)
}
// Cross returns the cross product of this vector with other.
diff --git a/paint/painter.go b/paint/painter.go
index a22998bd72..486eb71362 100644
--- a/paint/painter.go
+++ b/paint/painter.go
@@ -448,8 +448,8 @@ func (pc *Painter) DrawBox(pos, size math32.Vector2, img image.Image, op draw.Op
if img == nil {
img = colors.Uniform(color.RGBA{})
}
- pos = pc.Cumulative().MulVector2AsPoint(pos)
- size = pc.Cumulative().MulVector2AsVector(size)
+ pos = pc.Cumulative().MulPoint(pos)
+ size = pc.Cumulative().MulVector(size)
br := math32.RectFromPosSizeMax(pos, size)
cb := pc.Context().Bounds.Rect.ToRect()
b := cb.Intersect(br)
@@ -560,8 +560,8 @@ func (pc *Painter) BoundingBox(minX, minY, maxX, maxY float32) image.Rectangle {
// if pc.Stroke.Color != nil {// todo
// sw = 0.5 * pc.StrokeWidth()
// }
- tmin := pc.Cumulative().MulVector2AsPoint(math32.Vec2(minX, minY))
- tmax := pc.Cumulative().MulVector2AsPoint(math32.Vec2(maxX, maxY))
+ tmin := pc.Cumulative().MulPoint(math32.Vec2(minX, minY))
+ tmax := pc.Cumulative().MulPoint(math32.Vec2(maxX, maxY))
tp1 := math32.Vec2(tmin.X-sw, tmin.Y-sw).ToPointFloor()
tp2 := math32.Vec2(tmax.X+sw, tmax.Y+sw).ToPointCeil()
return image.Rect(tp1.X, tp1.Y, tp2.X, tp2.Y)
diff --git a/paint/pdf/links.go b/paint/pdf/links.go
index 2a877bccfc..5f57cd5c03 100644
--- a/paint/pdf/links.go
+++ b/paint/pdf/links.go
@@ -14,7 +14,7 @@ import (
// transform for scaling and flipping of coordinates to top-left system.
func (w *pdfPage) AddAnchor(name string, pos math32.Vector2) {
ms := math32.Scale2D(w.pdf.globalScale, w.pdf.globalScale)
- pos = ms.MulVector2AsPoint(pos)
+ pos = ms.MulPoint(pos)
if w.pdf.anchors == nil {
w.pdf.anchors = make(pdfMap)
}
diff --git a/paint/pdf/page.go b/paint/pdf/page.go
index 6271bf9571..5d69e00449 100644
--- a/paint/pdf/page.go
+++ b/paint/pdf/page.go
@@ -70,10 +70,10 @@ func (w *pdfPage) DrawImage(img image.Image, m math32.Matrix2) {
// add clipping path around image for smooth edges when rotating
outerRect := math32.B2(0.0, 0.0, float32(size.X), float32(size.Y)).MulMatrix2(m)
- bl := m.MulVector2AsPoint(math32.Vector2{0, 0})
- br := m.MulVector2AsPoint(math32.Vector2{float32(size.X), 0})
- tl := m.MulVector2AsPoint(math32.Vector2{0, float32(size.Y)})
- tr := m.MulVector2AsPoint(math32.Vector2{float32(size.X), float32(size.Y)})
+ bl := m.MulPoint(math32.Vector2{0, 0})
+ br := m.MulPoint(math32.Vector2{float32(size.X), 0})
+ tl := m.MulPoint(math32.Vector2{0, float32(size.Y)})
+ tr := m.MulPoint(math32.Vector2{float32(size.X), float32(size.Y)})
fmt.Fprintf(w, " q %v %v %v %v re W n", dec(outerRect.Min.X), dec(outerRect.Min.Y), dec(outerRect.Size().X), dec(outerRect.Size().Y))
fmt.Fprintf(w, " %v %v m %v %v l %v %v l %v %v l h W n", dec(bl.X), dec(bl.Y), dec(tl.X), dec(tl.Y), dec(tr.X), dec(tr.Y), dec(br.X), dec(br.Y))
diff --git a/paint/ppath/transform.go b/paint/ppath/transform.go
index 401921b51d..fafb2541e9 100644
--- a/paint/ppath/transform.go
+++ b/paint/ppath/transform.go
@@ -25,20 +25,20 @@ func (p Path) Transform(m math32.Matrix2) Path {
fmt.Println("path length error:", len(p), i, p)
return p
}
- end := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2]))
+ end := m.MulPoint(math32.Vec2(p[i+1], p[i+2]))
p[i+1] = end.X
p[i+2] = end.Y
case QuadTo:
- cp := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2]))
- end := m.MulVector2AsPoint(math32.Vec2(p[i+3], p[i+4]))
+ cp := m.MulPoint(math32.Vec2(p[i+1], p[i+2]))
+ end := m.MulPoint(math32.Vec2(p[i+3], p[i+4]))
p[i+1] = cp.X
p[i+2] = cp.Y
p[i+3] = end.X
p[i+4] = end.Y
case CubeTo:
- cp1 := m.MulVector2AsPoint(math32.Vec2(p[i+1], p[i+2]))
- cp2 := m.MulVector2AsPoint(math32.Vec2(p[i+3], p[i+4]))
- end := m.MulVector2AsPoint(math32.Vec2(p[i+5], p[i+6]))
+ cp1 := m.MulPoint(math32.Vec2(p[i+1], p[i+2]))
+ cp2 := m.MulPoint(math32.Vec2(p[i+3], p[i+4]))
+ end := m.MulPoint(math32.Vec2(p[i+5], p[i+6]))
p[i+1] = cp1.X
p[i+2] = cp1.Y
p[i+3] = cp2.X
@@ -77,7 +77,7 @@ func (p Path) Transform(m math32.Matrix2) Path {
if xscale*yscale < 0.0 { // flip x or y axis needs flipping of the sweep
sweep = !sweep
}
- end = m.MulVector2AsPoint(end)
+ end = m.MulPoint(end)
p[i+1] = rx
p[i+2] = ry
diff --git a/paint/renderers/rasterx/renderer.go b/paint/renderers/rasterx/renderer.go
index 647af02c3b..c6a270c82b 100644
--- a/paint/renderers/rasterx/renderer.go
+++ b/paint/renderers/rasterx/renderer.go
@@ -92,18 +92,18 @@ func (rs *Renderer) RenderPath(pt *render.Path) {
func PathToRasterx(rs Adder, p ppath.Path, m math32.Matrix2, off math32.Vector2) {
for s := p.Scanner(); s.Scan(); {
cmd := s.Cmd()
- end := m.MulVector2AsPoint(s.End()).Add(off)
+ end := m.MulPoint(s.End()).Add(off)
switch cmd {
case ppath.MoveTo:
rs.Start(end.ToFixed())
case ppath.LineTo:
rs.Line(end.ToFixed())
case ppath.QuadTo:
- cp1 := m.MulVector2AsPoint(s.CP1()).Add(off)
+ cp1 := m.MulPoint(s.CP1()).Add(off)
rs.QuadBezier(cp1.ToFixed(), end.ToFixed())
case ppath.CubeTo:
- cp1 := m.MulVector2AsPoint(s.CP1()).Add(off)
- cp2 := m.MulVector2AsPoint(s.CP2()).Add(off)
+ cp1 := m.MulPoint(s.CP1()).Add(off)
+ cp2 := m.MulPoint(s.CP2()).Add(off)
rs.CubeBezier(cp1.ToFixed(), cp2.ToFixed(), end.ToFixed())
case ppath.Close:
rs.Stop(true)
diff --git a/paint/renderers/rasterx/shapes.go b/paint/renderers/rasterx/shapes.go
index 31bd4f5294..2586e6a81b 100644
--- a/paint/renderers/rasterx/shapes.go
+++ b/paint/renderers/rasterx/shapes.go
@@ -33,7 +33,7 @@ func AddCircle(cx, cy, r float32, p Adder) {
// x and y radius, (rx, ry), rotated around the center by rot degrees.
func AddEllipse(cx, cy, rx, ry, rot float32, p Adder) {
rotRads := rot * math32.Pi / 180
- pt := math32.Identity2().Translate(cx, cy).Rotate(rotRads).Translate(-cx, -cy).MulVector2AsPoint(math32.Vec2(cx+rx, cy))
+ pt := math32.Identity2().Translate(cx, cy).Rotate(rotRads).Translate(-cx, -cy).MulPoint(math32.Vec2(cx+rx, cy))
points := []float32{rx, ry, rot, 1.0, 0.0, pt.X, pt.Y}
p.Start(pt.ToFixed())
AddArc(points, cx, cy, pt.X, pt.Y, p)
diff --git a/paint/renderers/rasterx/text.go b/paint/renderers/rasterx/text.go
index 36f895e0c8..8ebac88fa9 100644
--- a/paint/renderers/rasterx/text.go
+++ b/paint/renderers/rasterx/text.go
@@ -206,18 +206,18 @@ func (rs *Renderer) GlyphOutline(ctx *render.Context, run *shapedgt.Run, g *shap
rs.Path.Clear()
m := ctx.Cumulative
for _, s := range outline.Segments {
- p0 := m.MulVector2AsPoint(math32.Vec2(s.Args[0].X*scale+x, -s.Args[0].Y*scale+y))
+ p0 := m.MulPoint(math32.Vec2(s.Args[0].X*scale+x, -s.Args[0].Y*scale+y))
switch s.Op {
case opentype.SegmentOpMoveTo:
rs.Path.Start(p0.ToFixed())
case opentype.SegmentOpLineTo:
rs.Path.Line(p0.ToFixed())
case opentype.SegmentOpQuadTo:
- p1 := m.MulVector2AsPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y))
+ p1 := m.MulPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y))
rs.Path.QuadBezier(p0.ToFixed(), p1.ToFixed())
case opentype.SegmentOpCubeTo:
- p1 := m.MulVector2AsPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y))
- p2 := m.MulVector2AsPoint(math32.Vec2(s.Args[2].X*scale+x, -s.Args[2].Y*scale+y))
+ p1 := m.MulPoint(math32.Vec2(s.Args[1].X*scale+x, -s.Args[1].Y*scale+y))
+ p2 := m.MulPoint(math32.Vec2(s.Args[2].X*scale+x, -s.Args[2].Y*scale+y))
rs.Path.CubeBezier(p0.ToFixed(), p1.ToFixed(), p2.ToFixed())
}
}
@@ -266,10 +266,10 @@ func (rs *Renderer) StrokeBounds(ctx *render.Context, bb math32.Box2, clr color.
nil, 0)
rs.Raster.SetColor(colors.Uniform(clr))
m := ctx.Cumulative
- rs.Raster.Start(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed())
- rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed())
- rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed())
- rs.Raster.Line(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed())
+ rs.Raster.Start(m.MulPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed())
+ rs.Raster.Line(m.MulPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed())
+ rs.Raster.Line(m.MulPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed())
+ rs.Raster.Line(m.MulPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed())
rs.Raster.Stop(true)
rs.Raster.Draw()
rs.Raster.Clear()
@@ -278,8 +278,8 @@ func (rs *Renderer) StrokeBounds(ctx *render.Context, bb math32.Box2, clr color.
// StrokeTextLine strokes a line for text decoration.
func (rs *Renderer) StrokeTextLine(ctx *render.Context, sp, ep math32.Vector2, width float32, clr image.Image, dash []float32) {
m := ctx.Cumulative
- sp = m.MulVector2AsPoint(sp)
- ep = m.MulVector2AsPoint(ep)
+ sp = m.MulPoint(sp)
+ ep = m.MulPoint(ep)
width *= MeanScale(m)
rs.Raster.SetStroke(
math32.ToFixed(width),
@@ -299,10 +299,10 @@ func (rs *Renderer) FillBounds(ctx *render.Context, bb math32.Box2, clr image.Im
rf := &rs.Raster.Filler
rf.SetColor(clr)
m := ctx.Cumulative
- rf.Start(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed())
- rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed())
- rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed())
- rf.Line(m.MulVector2AsPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed())
+ rf.Start(m.MulPoint(math32.Vec2(bb.Min.X, bb.Min.Y)).ToFixed())
+ rf.Line(m.MulPoint(math32.Vec2(bb.Max.X, bb.Min.Y)).ToFixed())
+ rf.Line(m.MulPoint(math32.Vec2(bb.Max.X, bb.Max.Y)).ToFixed())
+ rf.Line(m.MulPoint(math32.Vec2(bb.Min.X, bb.Max.Y)).ToFixed())
rf.Stop(true)
rf.Draw()
rf.Clear()
diff --git a/svg/circle.go b/svg/circle.go
index 6c230def5b..52c44450f8 100644
--- a/svg/circle.go
+++ b/svg/circle.go
@@ -59,7 +59,7 @@ func (g *Circle) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Pos = xf.MulVector2AsPoint(g.Pos)
+ g.Pos = xf.MulPoint(g.Pos)
scx, scy := xf.ExtractScale()
g.Radius *= 0.5 * (scx + scy)
g.GradientApplyTransform(sv, xf)
diff --git a/svg/ellipse.go b/svg/ellipse.go
index 74826dd920..dc3f354020 100644
--- a/svg/ellipse.go
+++ b/svg/ellipse.go
@@ -60,8 +60,8 @@ func (g *Ellipse) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.SetTransformProperty()
} else {
// todo: this is not the correct transform:
- g.Pos = xf.MulVector2AsPoint(g.Pos)
- g.Radii = xf.MulVector2AsVector(g.Radii)
+ g.Pos = xf.MulPoint(g.Pos)
+ g.Radii = xf.MulVector(g.Radii)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/image.go b/svg/image.go
index 345d88c3a2..1d391b1d5f 100644
--- a/svg/image.go
+++ b/svg/image.go
@@ -130,8 +130,8 @@ func (g *Image) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Pos = xf.MulVector2AsPoint(g.Pos)
- g.Size = xf.MulVector2AsVector(g.Size)
+ g.Pos = xf.MulPoint(g.Pos)
+ g.Size = xf.MulVector(g.Size)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/line.go b/svg/line.go
index 9d5ce2a953..f58f20324a 100644
--- a/svg/line.go
+++ b/svg/line.go
@@ -72,8 +72,8 @@ func (g *Line) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Start = xf.MulVector2AsPoint(g.Start)
- g.End = xf.MulVector2AsPoint(g.End)
+ g.Start = xf.MulPoint(g.Start)
+ g.End = xf.MulPoint(g.End)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/node.go b/svg/node.go
index ce7294efc1..882534f948 100644
--- a/svg/node.go
+++ b/svg/node.go
@@ -177,8 +177,8 @@ func (g *NodeBase) ApplyTransform(sv *SVG, xf math32.Matrix2) {
// operating around given reference point which serves as the effective origin for rotation.
func (g *NodeBase) DeltaTransform(trans math32.Vector2, scale math32.Vector2, rot float32, pt math32.Vector2) math32.Matrix2 {
mxi := g.ParentTransform(true).Inverse()
- lpt := mxi.MulVector2AsPoint(pt)
- ltr := mxi.MulVector2AsVector(trans)
+ lpt := mxi.MulPoint(pt)
+ ltr := mxi.MulVector(trans)
xf := math32.Translate2D(lpt.X, lpt.Y).Scale(scale.X, scale.Y).Rotate(rot).Translate(ltr.X, ltr.Y).Translate(-lpt.X, -lpt.Y)
return xf
}
diff --git a/svg/polyline.go b/svg/polyline.go
index 13f6fe1c23..5918c972f7 100644
--- a/svg/polyline.go
+++ b/svg/polyline.go
@@ -80,7 +80,7 @@ func (g *Polyline) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.SetTransformProperty()
} else {
for i, p := range g.Points {
- p = xf.MulVector2AsPoint(p)
+ p = xf.MulPoint(p)
g.Points[i] = p
}
g.GradientApplyTransform(sv, xf)
diff --git a/svg/rect.go b/svg/rect.go
index 322027a5ad..b5b89562cc 100644
--- a/svg/rect.go
+++ b/svg/rect.go
@@ -70,8 +70,8 @@ func (g *Rect) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Pos = xf.MulVector2AsPoint(g.Pos)
- g.Size = xf.MulVector2AsVector(g.Size)
+ g.Pos = xf.MulPoint(g.Pos)
+ g.Size = xf.MulVector(g.Size)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/text.go b/svg/text.go
index d1306d6535..4b44c9c4f8 100644
--- a/svg/text.go
+++ b/svg/text.go
@@ -74,7 +74,7 @@ func (g *Text) SetNodePos(pos math32.Vector2) {
g.Pos = pos
for _, kii := range g.Children {
kt := kii.(*Text)
- kt.Pos = g.Paint.Transform.MulVector2AsPoint(pos)
+ kt.Pos = g.Paint.Transform.MulPoint(pos)
}
}
@@ -193,7 +193,7 @@ func (g *Text) ApplyTransform(sv *SVG, xf math32.Matrix2) {
g.Paint.Transform.SetMul(xf)
g.SetTransformProperty()
} else {
- g.Pos = xf.MulVector2AsPoint(g.Pos)
+ g.Pos = xf.MulPoint(g.Pos)
g.GradientApplyTransform(sv, xf)
}
}
diff --git a/svg/zoom.go b/svg/zoom.go
index 1669618786..338d598417 100644
--- a/svg/zoom.go
+++ b/svg/zoom.go
@@ -112,12 +112,12 @@ func (sv *SVG) ScaleAt(pt image.Point, sc float32) {
sv.setRootTransform()
rxf := sv.Root.Paint.Transform.Inverse()
mpt := math32.FromPoint(pt)
- xpt := rxf.MulVector2AsPoint(mpt)
+ xpt := rxf.MulPoint(mpt)
sv.Scale = sc
sv.setRootTransform()
rxf = sv.Root.Paint.Transform
- npt := rxf.MulVector2AsPoint(xpt) // original point back to screen
+ npt := rxf.MulPoint(xpt) // original point back to screen
dpt := mpt.Sub(npt)
sv.Translate.SetAdd(dpt)
}
From d66b2085f266c692f7b8c48dfdfaad2db39e014f Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 15 Dec 2025 07:00:44 +0100
Subject: [PATCH 76/99] pdf: more cleanup
---
math32/matrix2.go | 6 ++----
paint/renderers/rasterx/geom.go | 8 ++++----
2 files changed, 6 insertions(+), 8 deletions(-)
diff --git a/math32/matrix2.go b/math32/matrix2.go
index 0a5468cb8c..c0f00a3033 100644
--- a/math32/matrix2.go
+++ b/math32/matrix2.go
@@ -133,8 +133,6 @@ func (a *Matrix2) SetMul(b Matrix2) {
*a = a.Mul(b)
}
-// todo: rename to MulVector, MulPoint
-
// MulVector multiplies the Vector2 as a vector without adding translations.
// This is for directional vectors and not points.
func (a Matrix2) MulVector(v Vector2) Vector2 {
@@ -150,8 +148,8 @@ func (a Matrix2) MulPoint(v Vector2) Vector2 {
return Vec2(tx, ty)
}
-// MulFixedAsPoint multiplies the fixed point as a point, including adding translations.
-func (a Matrix2) MulFixedAsPoint(fp fixed.Point26_6) fixed.Point26_6 {
+// MulFixedPoint multiplies the fixed point as a point, including adding translations.
+func (a Matrix2) MulFixedPoint(fp fixed.Point26_6) fixed.Point26_6 {
x := fixed.Int26_6((float32(fp.X)*a.XX + float32(fp.Y)*a.XY) + a.X0*32)
y := fixed.Int26_6((float32(fp.X)*a.YX + float32(fp.Y)*a.YY) + a.Y0*32)
return fixed.Point26_6{x, y}
diff --git a/paint/renderers/rasterx/geom.go b/paint/renderers/rasterx/geom.go
index 655a8e69b9..840ebf1177 100644
--- a/paint/renderers/rasterx/geom.go
+++ b/paint/renderers/rasterx/geom.go
@@ -322,20 +322,20 @@ func (t *MatrixAdder) Reset() {
// Start starts a new path
func (t *MatrixAdder) Start(a fixed.Point26_6) {
- t.Adder.Start(t.M.MulFixedAsPoint(a))
+ t.Adder.Start(t.M.MulFixedPoint(a))
}
// Line adds a linear segment to the current curve.
func (t *MatrixAdder) Line(b fixed.Point26_6) {
- t.Adder.Line(t.M.MulFixedAsPoint(b))
+ t.Adder.Line(t.M.MulFixedPoint(b))
}
// QuadBezier adds a quadratic segment to the current curve.
func (t *MatrixAdder) QuadBezier(b, c fixed.Point26_6) {
- t.Adder.QuadBezier(t.M.MulFixedAsPoint(b), t.M.MulFixedAsPoint(c))
+ t.Adder.QuadBezier(t.M.MulFixedPoint(b), t.M.MulFixedPoint(c))
}
// CubeBezier adds a cubic segment to the current curve.
func (t *MatrixAdder) CubeBezier(b, c, d fixed.Point26_6) {
- t.Adder.CubeBezier(t.M.MulFixedAsPoint(b), t.M.MulFixedAsPoint(c), t.M.MulFixedAsPoint(d))
+ t.Adder.CubeBezier(t.M.MulFixedPoint(b), t.M.MulFixedPoint(c), t.M.MulFixedPoint(d))
}
From 9ed2447ed1620127724987ed48643d2e80fac919 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 15 Dec 2025 09:47:34 +0100
Subject: [PATCH 77/99] pdf: Matrix3 constructor
---
math32/matrix2.go | 2 +-
math32/matrix3.go | 39 ++++++++++++++++++++++++++++++---------
2 files changed, 31 insertions(+), 10 deletions(-)
diff --git a/math32/matrix2.go b/math32/matrix2.go
index c0f00a3033..533c495b32 100644
--- a/math32/matrix2.go
+++ b/math32/matrix2.go
@@ -39,7 +39,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-// Matrix2 is a 3x2 matrix.
+// Matrix2 is a 2x3 matrix.
// [XX YX]
// [XY YY]
// [X0 Y0]
diff --git a/math32/matrix3.go b/math32/matrix3.go
index 65fbbde3e6..90b59ea9e0 100644
--- a/math32/matrix3.go
+++ b/math32/matrix3.go
@@ -15,6 +15,13 @@ import "errors"
// Matrix3 is 3x3 matrix organized internally as column matrix.
type Matrix3 [9]float32
+// Mat3 constructs a new Matrix3 from given values, in column-wise order.
+func Mat3(n11, n21, n31, n12, n22, n32, n13, n23, n33 float32) Matrix3 {
+ m := Matrix3{}
+ m.SetCols(n11, n21, n31, n12, n22, n32, n13, n23, n33)
+ return m
+}
+
// Identity3 returns a new identity [Matrix3] matrix.
func Identity3() Matrix3 {
m := Matrix3{}
@@ -49,23 +56,37 @@ func Matrix3Rotate2D(angle float32) Matrix3 {
return Matrix3FromMatrix2(Rotate2D(angle))
}
-// Set sets all the elements of the matrix row by row starting at row1, column1,
-// row1, column2, row1, column3 and so forth.
-func (m *Matrix3) Set(n11, n12, n13, n21, n22, n23, n31, n32, n33 float32) {
+// SetCols sets all the elements of the matrix col by col starting at row1, column1,
+// row2, column1, row3, column1 and so forth.
+func (m *Matrix3) SetCols(n11, n21, n31, n12, n22, n32, n13, n23, n33 float32) {
m[0] = n11
- m[3] = n12
- m[6] = n13
m[1] = n21
+ m[2] = n31
+ m[3] = n12
m[4] = n22
+ m[5] = n32
+ m[6] = n13
m[7] = n23
+ m[8] = n33
+}
+
+// SetRows sets all the elements of the matrix row by row starting at row1, column1,
+// row1, column2, row1, column3 and so forth.
+func (m *Matrix3) SetRows(n11, n12, n13, n21, n22, n23, n31, n32, n33 float32) {
+ m[0] = n11
+ m[1] = n21
m[2] = n31
+ m[3] = n12
+ m[4] = n22
m[5] = n32
+ m[6] = n13
+ m[7] = n23
m[8] = n33
}
// SetFromMatrix4 sets the matrix elements based on a Matrix4.
func (m *Matrix3) SetFromMatrix4(src *Matrix4) {
- m.Set(
+ m.SetRows(
src[0], src[4], src[8],
src[1], src[5], src[9],
src[2], src[6], src[10],
@@ -78,7 +99,7 @@ func (m *Matrix3) SetFromMatrix4(src *Matrix4) {
// SetFromMatrix2 sets the matrix elements based on a Matrix2.
func (m *Matrix3) SetFromMatrix2(src Matrix2) {
- m.Set(
+ m.SetRows(
src.XX, src.YX, src.X0,
src.XY, src.YY, src.Y0,
src.X0, src.Y0, 1,
@@ -97,7 +118,7 @@ func (m Matrix3) ToArray(array []float32, offset int) {
// SetIdentity sets this matrix as the identity matrix.
func (m *Matrix3) SetIdentity() {
- m.Set(
+ m.SetRows(
1, 0, 0,
0, 1, 0,
0, 0, 1,
@@ -106,7 +127,7 @@ func (m *Matrix3) SetIdentity() {
// SetZero sets this matrix as the zero matrix.
func (m *Matrix3) SetZero() {
- m.Set(
+ m.SetRows(
0, 0, 0,
0, 0, 0,
0, 0, 0,
From 95b6a48d720d16264076a6a216d9ffd61ee030fc Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 15 Dec 2025 12:34:43 +0100
Subject: [PATCH 78/99] pdf: move Vector3.MulQuat to Quat.MulVector
---
math32/box3.go | 18 ++++----
math32/matrix3.go | 41 +++++++-----------
math32/quaternion.go | 88 ++++++++++++++++++++++++++++-----------
math32/quaternion_test.go | 14 +++++++
math32/vector3.go | 15 -------
xyz/camera.go | 10 ++---
xyz/pose.go | 2 +-
7 files changed, 108 insertions(+), 80 deletions(-)
diff --git a/math32/box3.go b/math32/box3.go
index 86092360c8..aa080cc150 100644
--- a/math32/box3.go
+++ b/math32/box3.go
@@ -215,15 +215,15 @@ func (b Box3) MulMatrix4(m *Matrix4) Box3 {
// and computes the resulting spanning Box3 of the transformed points
func (b Box3) MulQuat(q Quat) Box3 {
var cs [8]Vector3
- cs[0] = Vec3(b.Min.X, b.Min.Y, b.Min.Z).MulQuat(q)
- cs[1] = Vec3(b.Min.X, b.Min.Y, b.Max.Z).MulQuat(q)
- cs[2] = Vec3(b.Min.X, b.Max.Y, b.Min.Z).MulQuat(q)
- cs[3] = Vec3(b.Max.X, b.Min.Y, b.Min.Z).MulQuat(q)
-
- cs[4] = Vec3(b.Max.X, b.Max.Y, b.Max.Z).MulQuat(q)
- cs[5] = Vec3(b.Max.X, b.Max.Y, b.Min.Z).MulQuat(q)
- cs[6] = Vec3(b.Max.X, b.Min.Y, b.Max.Z).MulQuat(q)
- cs[7] = Vec3(b.Min.X, b.Max.Y, b.Max.Z).MulQuat(q)
+ cs[0] = q.MulVector(Vec3(b.Min.X, b.Min.Y, b.Min.Z))
+ cs[1] = q.MulVector(Vec3(b.Min.X, b.Min.Y, b.Max.Z))
+ cs[2] = q.MulVector(Vec3(b.Min.X, b.Max.Y, b.Min.Z))
+ cs[3] = q.MulVector(Vec3(b.Max.X, b.Min.Y, b.Min.Z))
+
+ cs[4] = q.MulVector(Vec3(b.Max.X, b.Max.Y, b.Max.Z))
+ cs[5] = q.MulVector(Vec3(b.Max.X, b.Max.Y, b.Min.Z))
+ cs[6] = q.MulVector(Vec3(b.Max.X, b.Min.Y, b.Max.Z))
+ cs[7] = q.MulVector(Vec3(b.Min.X, b.Max.Y, b.Max.Z))
nb := B3Empty()
for i := 0; i < 8; i++ {
diff --git a/math32/matrix3.go b/math32/matrix3.go
index 90b59ea9e0..8ac6e576ce 100644
--- a/math32/matrix3.go
+++ b/math32/matrix3.go
@@ -15,10 +15,13 @@ import "errors"
// Matrix3 is 3x3 matrix organized internally as column matrix.
type Matrix3 [9]float32
+// note: matrix indexes are row,column. matrix dimensions are CxR.
+// nothing like consistency...
+
// Mat3 constructs a new Matrix3 from given values, in column-wise order.
func Mat3(n11, n21, n31, n12, n22, n32, n13, n23, n33 float32) Matrix3 {
m := Matrix3{}
- m.SetCols(n11, n21, n31, n12, n22, n32, n13, n23, n33)
+ m.Set(n11, n21, n31, n12, n22, n32, n13, n23, n33)
return m
}
@@ -56,23 +59,9 @@ func Matrix3Rotate2D(angle float32) Matrix3 {
return Matrix3FromMatrix2(Rotate2D(angle))
}
-// SetCols sets all the elements of the matrix col by col starting at row1, column1,
+// Set sets all the elements of the matrix col by col starting at row1, column1,
// row2, column1, row3, column1 and so forth.
-func (m *Matrix3) SetCols(n11, n21, n31, n12, n22, n32, n13, n23, n33 float32) {
- m[0] = n11
- m[1] = n21
- m[2] = n31
- m[3] = n12
- m[4] = n22
- m[5] = n32
- m[6] = n13
- m[7] = n23
- m[8] = n33
-}
-
-// SetRows sets all the elements of the matrix row by row starting at row1, column1,
-// row1, column2, row1, column3 and so forth.
-func (m *Matrix3) SetRows(n11, n12, n13, n21, n22, n23, n31, n32, n33 float32) {
+func (m *Matrix3) Set(n11, n21, n31, n12, n22, n32, n13, n23, n33 float32) {
m[0] = n11
m[1] = n21
m[2] = n31
@@ -86,10 +75,10 @@ func (m *Matrix3) SetRows(n11, n12, n13, n21, n22, n23, n31, n32, n33 float32) {
// SetFromMatrix4 sets the matrix elements based on a Matrix4.
func (m *Matrix3) SetFromMatrix4(src *Matrix4) {
- m.SetRows(
- src[0], src[4], src[8],
- src[1], src[5], src[9],
- src[2], src[6], src[10],
+ m.Set(
+ src[0], src[1], src[2],
+ src[4], src[5], src[6],
+ src[8], src[9], src[10],
)
}
@@ -99,9 +88,9 @@ func (m *Matrix3) SetFromMatrix4(src *Matrix4) {
// SetFromMatrix2 sets the matrix elements based on a Matrix2.
func (m *Matrix3) SetFromMatrix2(src Matrix2) {
- m.SetRows(
- src.XX, src.YX, src.X0,
- src.XY, src.YY, src.Y0,
+ m.Set(
+ src.XX, src.XY, src.X0,
+ src.YX, src.YY, src.Y0,
src.X0, src.Y0, 1,
)
}
@@ -118,7 +107,7 @@ func (m Matrix3) ToArray(array []float32, offset int) {
// SetIdentity sets this matrix as the identity matrix.
func (m *Matrix3) SetIdentity() {
- m.SetRows(
+ m.Set(
1, 0, 0,
0, 1, 0,
0, 0, 1,
@@ -127,7 +116,7 @@ func (m *Matrix3) SetIdentity() {
// SetZero sets this matrix as the zero matrix.
func (m *Matrix3) SetZero() {
- m.SetRows(
+ m.Set(
0, 0, 0,
0, 0, 0,
0, 0, 0,
diff --git a/math32/quaternion.go b/math32/quaternion.go
index 96bf844ecb..8dd8b14477 100644
--- a/math32/quaternion.go
+++ b/math32/quaternion.go
@@ -56,7 +56,7 @@ func (q *Quat) FromArray(array []float32, offset int) {
}
// ToArray copies this quaternions's components to array starting at offset.
-func (q *Quat) ToArray(array []float32, offset int) {
+func (q Quat) ToArray(array []float32, offset int) {
array[offset] = q.X
array[offset+1] = q.Y
array[offset+2] = q.Z
@@ -72,12 +72,12 @@ func (q *Quat) SetIdentity() {
}
// IsIdentity returns if this is an identity quaternion.
-func (q *Quat) IsIdentity() bool {
+func (q Quat) IsIdentity() bool {
return q.X == 0 && q.Y == 0 && q.Z == 0 && q.W == 1
}
// IsNil returns true if all values are 0 (uninitialized).
-func (q *Quat) IsNil() bool {
+func (q Quat) IsNil() bool {
return q.X == 0 && q.Y == 0 && q.Z == 0 && q.W == 0
}
@@ -104,9 +104,9 @@ func (q *Quat) SetFromEuler(euler Vector3) {
// ToEuler returns a Vector3 with components as the Euler angles
// from the given quaternion.
-func (q *Quat) ToEuler() Vector3 {
+func (q Quat) ToEuler() Vector3 {
rot := Vector3{}
- rot.SetEulerAnglesFromQuat(*q)
+ rot.SetEulerAnglesFromQuat(q)
return rot
}
@@ -122,20 +122,20 @@ func (q *Quat) SetFromAxisAngle(axis Vector3, angle float32) {
}
// ToAxisAngle returns the Vector4 holding axis and angle of this Quaternion
-func (q *Quat) ToAxisAngle() Vector4 {
+func (q Quat) ToAxisAngle() Vector4 {
aa := Vector4{}
- aa.SetAxisAngleFromQuat(*q)
+ aa.SetAxisAngleFromQuat(q)
return aa
}
// GenGoSet returns code to set values in object at given path (var.member etc)
-func (q *Quat) GenGoSet(path string) string {
+func (q Quat) GenGoSet(path string) string {
aa := q.ToAxisAngle()
return fmt.Sprintf("%s.SetFromAxisAngle(math32.Vec3(%g, %g, %g), %g)", path, aa.X, aa.Y, aa.Z, aa.W)
}
// GenGoNew returns code to create new
-func (q *Quat) GenGoNew() string {
+func (q Quat) GenGoNew() string {
return fmt.Sprintf("math32.Quat{%g, %g, %g, %g}", q.X, q.Y, q.Z, q.W)
}
@@ -213,10 +213,9 @@ func (q *Quat) SetInverse() {
}
// Inverse returns the inverse of this quaternion.
-func (q *Quat) Inverse() Quat {
- nq := *q
- nq.SetInverse()
- return nq
+func (q Quat) Inverse() Quat {
+ q.SetInverse()
+ return q
}
// SetConjugate sets this quaternion to its conjugate.
@@ -227,14 +226,13 @@ func (q *Quat) SetConjugate() {
}
// Conjugate returns the conjugate of this quaternion.
-func (q *Quat) Conjugate() Quat {
- nq := *q
- nq.SetConjugate()
- return nq
+func (q Quat) Conjugate() Quat {
+ q.SetConjugate()
+ return q
}
// Dot returns the dot products of this quaternion with other.
-func (q *Quat) Dot(other Quat) float32 {
+func (q Quat) Dot(other Quat) float32 {
return q.X*other.X + q.Y*other.Y + q.Z*other.Z + q.W*other.W
}
@@ -299,14 +297,56 @@ func (q *Quat) SetMul(other Quat) {
}
// Mul returns returns multiplication of this quaternion with other
-func (q *Quat) Mul(other Quat) Quat {
- nq := *q
- nq.SetMul(other)
- return nq
+func (q Quat) Mul(other Quat) Quat {
+ q.SetMul(other)
+ return q
}
-// Slerp sets this quaternion to another quaternion which is the spherically linear interpolation
-// from this quaternion to other using t.
+// MulVector applies the rotation encoded in the quaternion to the [Vector3].
+func (q Quat) MulVector(v Vector3) Vector3 {
+ // note: these each produce the same results when q is normalized
+ // but produce very different results for non-normalized,
+ // which is presumably not well defined anyway.
+ // According to IsaacLab, the cross-product version is faster,
+ // but the old version that it replaced is pretty weird.
+ // In any case, it is much simpler!
+ // https://github.com/isaac-sim/IsaacLab/issues/1711
+ // https://github.com/isaac-sim/IsaacLab/pull/2129/files
+
+ // original version:
+ // // calculate quat * vector
+ // ix := q.W*v.X + q.Y*v.Z - q.Z*v.Y
+ // iy := q.W*v.Y + q.Z*v.X - q.X*v.Z
+ // iz := q.W*v.Z + q.X*v.Y - q.Y*v.X
+ // iw := -q.X*v.X - q.Y*v.Y - q.Z*v.Z
+ // // calculate result * inverse quat
+ // return Vec3(ix*q.W+iw*-q.X+iy*-q.Z-iz*-q.Y,
+ // iy*q.W+iw*-q.Y+iz*-q.X-ix*-q.Z,
+ // iz*q.W+iw*-q.Z+ix*-q.Y-iy*-q.X)
+
+ // warp version:
+ // c := 2*q.W*q.W - 1
+ // d := 2 * (q.X*v.X + q.Y*v.Y + q.Z*v.Z)
+ // return Vec3(v.X*c+q.X*d+(q.Y*v.Z-q.Z*v.Y)*q.W*2,
+ // v.Y*c+q.Y*d+(q.Z*v.X-q.X*v.Z)*q.W*2,
+ // v.Z*c+q.Z*d+(q.X*v.Y-q.Y*v.X)*q.W*2)
+
+ // isaacLab
+ xyz := Vec3(q.X, q.Y, q.Z)
+ t := xyz.Cross(v).MulScalar(2)
+ return v.Add(t.MulScalar(q.W)).Add(xyz.Cross(t))
+}
+
+// MulVectorInverse applies the inverse of the rotation encoded in the quaternion
+// to the [Vector3].
+func (q Quat) MulVectorInverse(v Vector3) Vector3 {
+ xyz := Vec3(q.X, q.Y, q.Z)
+ t := xyz.Cross(v).MulScalar(2)
+ return v.Sub(t.MulScalar(q.W)).Add(xyz.Cross(t))
+}
+
+// Slerp sets this quaternion to another quaternion which is the
+// spherically linear interpolation from this quaternion to other using t.
func (q *Quat) Slerp(other Quat, t float32) {
if t == 0 {
return
diff --git a/math32/quaternion_test.go b/math32/quaternion_test.go
index 67f268383e..ae17e49969 100644
--- a/math32/quaternion_test.go
+++ b/math32/quaternion_test.go
@@ -111,6 +111,20 @@ func TestQuatMulQuats(t *testing.T) {
assert.Equal(t, expected, q)
}
+func TestQuatMulVector(t *testing.T) {
+ q1 := Quat{X: 1, Y: 2, Z: 3, W: 4}
+ q1.Normalize() // note: critical
+ v := Vector3{X: 5, Y: 6, Z: 7}
+
+ vr := q1.MulVector(v)
+
+ expected := Vector3{X: 1.7999997, Y: 7.6, Z: 7}
+ assert.Equal(t, expected, vr)
+
+ viv := q1.MulVectorInverse(q1.MulVector(v))
+ assert.Equal(t, v, viv)
+}
+
func TestQuatSlerp(t *testing.T) {
q1 := Quat{X: 1, Y: 2, Z: 3, W: 4}
q2 := Quat{X: 5, Y: 6, Z: 7, W: 8}
diff --git a/math32/vector3.go b/math32/vector3.go
index 0dfff9391c..ca5255a2a5 100644
--- a/math32/vector3.go
+++ b/math32/vector3.go
@@ -412,21 +412,6 @@ func (v Vector3) MulProjection(m *Matrix4) Vector3 {
(m[2]*v.X + m[6]*v.Y + m[10]*v.Z + m[14]) * d}
}
-// MulQuat returns vector multiplied by specified quaternion and
-// then by the quaternion inverse.
-// It basically applies the rotation encoded in the quaternion to this vector.
-func (v Vector3) MulQuat(q Quat) Vector3 {
- // calculate quat * vector
- ix := q.W*v.X + q.Y*v.Z - q.Z*v.Y
- iy := q.W*v.Y + q.Z*v.X - q.X*v.Z
- iz := q.W*v.Z + q.X*v.Y - q.Y*v.X
- iw := -q.X*v.X - q.Y*v.Y - q.Z*v.Z
- // calculate result * inverse quat
- return Vec3(ix*q.W+iw*-q.X+iy*-q.Z-iz*-q.Y,
- iy*q.W+iw*-q.Y+iz*-q.X-ix*-q.Z,
- iz*q.W+iw*-q.Z+ix*-q.Y-iy*-q.X)
-}
-
// Cross returns the cross product of this vector with other.
func (v Vector3) Cross(other Vector3) Vector3 {
return Vec3(v.Y*other.Z-v.Z*other.Y, v.Z*other.X-v.X*other.Z, v.X*other.Y-v.Y*other.X)
diff --git a/xyz/camera.go b/xyz/camera.go
index 0b508666ae..f6d6e6ff64 100644
--- a/xyz/camera.go
+++ b/xyz/camera.go
@@ -155,13 +155,13 @@ func (cm *Camera) Orbit(delX, delY float32) {
// delX rotates around the up vector
dxq := math32.NewQuatAxisAngle(up, math32.DegToRad(delX))
- dx := ctdir.MulQuat(dxq).Sub(ctdir)
+ dx := dxq.MulVector(ctdir).Sub(ctdir)
// delY rotates around the right vector
dyq := math32.NewQuatAxisAngle(right, math32.DegToRad(delY))
- dy := ctdir.MulQuat(dyq).Sub(ctdir)
+ dy := dyq.MulVector(ctdir).Sub(ctdir)
cm.Pose.Pos = cm.Pose.Pos.Add(dx).Add(dy)
- cm.UpDir = cm.UpDir.MulQuat(dyq) // this is only one that affects up
+ cm.UpDir = dyq.MulVector(cm.UpDir) // this is only one that affects up
cm.LookAtTarget()
}
@@ -171,8 +171,8 @@ func (cm *Camera) Orbit(delX, delY float32) {
// current window view)
// and it moves the target by the same increment, changing the target position.
func (cm *Camera) Pan(delX, delY float32) {
- dx := math32.Vec3(-delX, 0, 0).MulQuat(cm.Pose.Quat)
- dy := math32.Vec3(0, -delY, 0).MulQuat(cm.Pose.Quat)
+ dx := cm.Pose.Quat.MulVector(math32.Vec3(-delX, 0, 0))
+ dy := cm.Pose.Quat.MulVector(math32.Vec3(0, -delY, 0))
td := dx.Add(dy)
cm.Pose.Pos.SetAdd(td)
cm.Target.SetAdd(td)
diff --git a/xyz/pose.go b/xyz/pose.go
index b12d45872c..be7cb36988 100644
--- a/xyz/pose.go
+++ b/xyz/pose.go
@@ -117,7 +117,7 @@ func (ps *Pose) UpdateMVPMatrix(viewMat, projectionMat *math32.Matrix4) {
// MoveOnAxis moves (translates) the specified distance on the specified local axis,
// relative to the current rotation orientation.
func (ps *Pose) MoveOnAxis(x, y, z, dist float32) {
- ps.Pos.SetAdd(math32.Vec3(x, y, z).Normal().MulQuat(ps.Quat).MulScalar(dist))
+ ps.Pos.SetAdd(ps.Quat.MulVector(math32.Vec3(x, y, z).Normal()).MulScalar(dist))
}
// MoveOnAxisAbs moves (translates) the specified distance on the specified local axis,
From 04eecbb66cf86b339fa17047f89c12700ed0731b Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 15 Dec 2025 12:40:38 +0100
Subject: [PATCH 79/99] pdf: previous had simpler implementation of MulVector
-- this removes the commentary about changes.
---
math32/quaternion.go | 28 ----------------------------
1 file changed, 28 deletions(-)
diff --git a/math32/quaternion.go b/math32/quaternion.go
index 8dd8b14477..5b3943b7af 100644
--- a/math32/quaternion.go
+++ b/math32/quaternion.go
@@ -304,34 +304,6 @@ func (q Quat) Mul(other Quat) Quat {
// MulVector applies the rotation encoded in the quaternion to the [Vector3].
func (q Quat) MulVector(v Vector3) Vector3 {
- // note: these each produce the same results when q is normalized
- // but produce very different results for non-normalized,
- // which is presumably not well defined anyway.
- // According to IsaacLab, the cross-product version is faster,
- // but the old version that it replaced is pretty weird.
- // In any case, it is much simpler!
- // https://github.com/isaac-sim/IsaacLab/issues/1711
- // https://github.com/isaac-sim/IsaacLab/pull/2129/files
-
- // original version:
- // // calculate quat * vector
- // ix := q.W*v.X + q.Y*v.Z - q.Z*v.Y
- // iy := q.W*v.Y + q.Z*v.X - q.X*v.Z
- // iz := q.W*v.Z + q.X*v.Y - q.Y*v.X
- // iw := -q.X*v.X - q.Y*v.Y - q.Z*v.Z
- // // calculate result * inverse quat
- // return Vec3(ix*q.W+iw*-q.X+iy*-q.Z-iz*-q.Y,
- // iy*q.W+iw*-q.Y+iz*-q.X-ix*-q.Z,
- // iz*q.W+iw*-q.Z+ix*-q.Y-iy*-q.X)
-
- // warp version:
- // c := 2*q.W*q.W - 1
- // d := 2 * (q.X*v.X + q.Y*v.Y + q.Z*v.Z)
- // return Vec3(v.X*c+q.X*d+(q.Y*v.Z-q.Z*v.Y)*q.W*2,
- // v.Y*c+q.Y*d+(q.Z*v.X-q.X*v.Z)*q.W*2,
- // v.Z*c+q.Z*d+(q.X*v.Y-q.Y*v.X)*q.W*2)
-
- // isaacLab
xyz := Vec3(q.X, q.Y, q.Z)
t := xyz.Cross(v).MulScalar(2)
return v.Add(t.MulScalar(q.W)).Add(xyz.Cross(t))
From 56630f76cc497a97ef66b2a7a98844c7be85c198 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Fri, 19 Dec 2025 08:41:17 +0100
Subject: [PATCH 80/99] pdf: math32 minor cleanup
---
math32/matrix3.go | 46 ++++++++++++++++++++++++++------------------
math32/quaternion.go | 38 ++++++++++++++++++++++--------------
2 files changed, 51 insertions(+), 33 deletions(-)
diff --git a/math32/matrix3.go b/math32/matrix3.go
index 8ac6e576ce..94e36322c8 100644
--- a/math32/matrix3.go
+++ b/math32/matrix3.go
@@ -12,7 +12,8 @@ package math32
import "errors"
-// Matrix3 is 3x3 matrix organized internally as column matrix.
+// Matrix3 is 3x3 matrix organized internally as column matrix,
+// with columns as the inner dimension.
type Matrix3 [9]float32
// note: matrix indexes are row,column. matrix dimensions are CxR.
@@ -132,35 +133,35 @@ func (m *Matrix3) CopyFrom(src Matrix3) {
// MulMatrices sets ths matrix as matrix multiplication a by b (i.e., a*b).
func (m *Matrix3) MulMatrices(a, b Matrix3) {
a11 := a[0]
- a12 := a[3]
- a13 := a[6]
a21 := a[1]
- a22 := a[4]
- a23 := a[7]
a31 := a[2]
+ a12 := a[3]
+ a22 := a[4]
a32 := a[5]
+ a13 := a[6]
+ a23 := a[7]
a33 := a[8]
b11 := b[0]
- b12 := b[3]
- b13 := b[6]
b21 := b[1]
- b22 := b[4]
- b23 := b[7]
b31 := b[2]
+ b12 := b[3]
+ b22 := b[4]
b32 := b[5]
+ b13 := b[6]
+ b23 := b[7]
b33 := b[8]
m[0] = b11*a11 + b12*a21 + b13*a31
- m[3] = b11*a12 + b12*a22 + b13*a32
- m[6] = b11*a13 + b12*a23 + b13*a33
-
m[1] = b21*a11 + b22*a21 + b23*a31
- m[4] = b21*a12 + b22*a22 + b23*a32
- m[7] = b21*a13 + b22*a23 + b23*a33
-
m[2] = b31*a11 + b32*a21 + b33*a31
+
+ m[3] = b11*a12 + b12*a22 + b13*a32
+ m[4] = b21*a12 + b22*a22 + b23*a32
m[5] = b31*a12 + b32*a22 + b33*a32
+
+ m[6] = b11*a13 + b12*a23 + b13*a33
+ m[7] = b21*a13 + b22*a23 + b23*a33
m[8] = b31*a13 + b32*a23 + b33*a33
}
@@ -186,13 +187,13 @@ func (m Matrix3) MulScalar(s float32) Matrix3 {
// SetMulScalar multiplies each of this matrix's components by the specified scalar.
func (m *Matrix3) SetMulScalar(s float32) {
m[0] *= s
- m[3] *= s
- m[6] *= s
m[1] *= s
- m[4] *= s
- m[7] *= s
m[2] *= s
+ m[3] *= s
+ m[4] *= s
m[5] *= s
+ m[6] *= s
+ m[7] *= s
m[8] *= s
}
@@ -211,6 +212,13 @@ func (a Matrix3) MulPoint(v Vector2) Vector2 {
return Vec2(tx, ty)
}
+// MulVector3 multiplies the Vector3 on the right, as a standard matrix multiply.
+func (a Matrix3) MulVector3(v Vector3) Vector3 {
+ return Vec3(a[0]*v.X+a[3]*v.Y+a[6]*v.Z,
+ a[1]*v.X+a[4]*v.Y+a[7]*v.Z,
+ a[2]*v.X+a[5]*v.Y+a[8]*v.Z)
+}
+
// MulVector3Array multiplies count vectors (i.e., 3 sequential array values per each increment in count)
// in the array starting at start index by this matrix.
func (m *Matrix3) MulVector3Array(array []float32, start, count int) {
diff --git a/math32/quaternion.go b/math32/quaternion.go
index 5b3943b7af..97131593d9 100644
--- a/math32/quaternion.go
+++ b/math32/quaternion.go
@@ -232,8 +232,8 @@ func (q Quat) Conjugate() Quat {
}
// Dot returns the dot products of this quaternion with other.
-func (q Quat) Dot(other Quat) float32 {
- return q.X*other.X + q.Y*other.Y + q.Z*other.Z + q.W*other.W
+func (q Quat) Dot(o Quat) float32 {
+ return q.X*o.X + q.Y*o.Y + q.Z*o.Z + q.W*o.W
}
// LengthSq returns this quanternion's length squared
@@ -292,16 +292,26 @@ func MulQuats(a, b Quat) Quat {
}
// SetMul sets this quaternion to the multiplication of itself by other.
-func (q *Quat) SetMul(other Quat) {
- *q = MulQuats(*q, other)
+func (q *Quat) SetMul(o Quat) {
+ *q = MulQuats(*q, o)
}
// Mul returns returns multiplication of this quaternion with other
-func (q Quat) Mul(other Quat) Quat {
- q.SetMul(other)
+func (q Quat) Mul(o Quat) Quat {
+ q.SetMul(o)
return q
}
+// MulScalar returns values multiplied by scalar
+func (q Quat) MulScalar(s float32) Quat {
+ return NewQuat(q.X*s, q.Y*s, q.Z*s, q.W*s)
+}
+
+// Add returns values added
+func (q Quat) Add(o Quat) Quat {
+ return NewQuat(q.X+o.X, q.Y+o.Y, q.Z+o.Z, q.W+o.W)
+}
+
// MulVector applies the rotation encoded in the quaternion to the [Vector3].
func (q Quat) MulVector(v Vector3) Vector3 {
xyz := Vec3(q.X, q.Y, q.Z)
@@ -319,12 +329,12 @@ func (q Quat) MulVectorInverse(v Vector3) Vector3 {
// Slerp sets this quaternion to another quaternion which is the
// spherically linear interpolation from this quaternion to other using t.
-func (q *Quat) Slerp(other Quat, t float32) {
+func (q *Quat) Slerp(o Quat, t float32) {
if t == 0 {
return
}
if t == 1 {
- *q = other
+ *q = o
return
}
@@ -333,16 +343,16 @@ func (q *Quat) Slerp(other Quat, t float32) {
z := q.Z
w := q.W
- cosHalfTheta := w*other.W + x*other.X + y*other.Y + z*other.Z
+ cosHalfTheta := w*o.W + x*o.X + y*o.Y + z*o.Z
if cosHalfTheta < 0 {
- q.W = -other.W
- q.X = -other.X
- q.Y = -other.Y
- q.Z = -other.Z
+ q.W = -o.W
+ q.X = -o.X
+ q.Y = -o.Y
+ q.Z = -o.Z
cosHalfTheta = -cosHalfTheta
} else {
- *q = other
+ *q = o
}
if cosHalfTheta >= 1.0 {
From 8cffdc08afa5324bd3fb38a0535960b46db6abbc Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Fri, 19 Dec 2025 08:44:23 +0100
Subject: [PATCH 81/99] pdf: yaegicore update
---
yaegicore/basesymbols/cogentcore_org-core-math32.go | 2 ++
yaegicore/coresymbols/cogentcore_org-core-base-iox-imagex.go | 1 +
yaegicore/coresymbols/cogentcore_org-core-core.go | 2 ++
yaegicore/coresymbols/cogentcore_org-core-htmlcore.go | 1 +
yaegicore/coresymbols/cogentcore_org-core-paint.go | 1 +
5 files changed, 7 insertions(+)
diff --git a/yaegicore/basesymbols/cogentcore_org-core-math32.go b/yaegicore/basesymbols/cogentcore_org-core-math32.go
index 0cc87cab56..645aa5d4e3 100644
--- a/yaegicore/basesymbols/cogentcore_org-core-math32.go
+++ b/yaegicore/basesymbols/cogentcore_org-core-math32.go
@@ -82,6 +82,7 @@ func init() {
"Log2": reflect.ValueOf(math32.Log2),
"Log2E": reflect.ValueOf(constant.MakeFromLiteral("1.44269504088896340735992468100189213742664595415298593413544940772066427768997545329060870636212628972710992130324953463427359402479619301286929040235571747101382214539290471666532766903401352465152740478515625", token.FLOAT, 0)),
"Logb": reflect.ValueOf(math32.Logb),
+ "Mat3": reflect.ValueOf(math32.Mat3),
"Matrix3FromMatrix2": reflect.ValueOf(math32.Matrix3FromMatrix2),
"Matrix3FromMatrix4": reflect.ValueOf(math32.Matrix3FromMatrix4),
"Matrix3Rotate2D": reflect.ValueOf(math32.Matrix3Rotate2D),
@@ -94,6 +95,7 @@ func init() {
"MinPos": reflect.ValueOf(math32.MinPos),
"Mod": reflect.ValueOf(math32.Mod),
"Modf": reflect.ValueOf(math32.Modf),
+ "MulQuats": reflect.ValueOf(math32.MulQuats),
"NaN": reflect.ValueOf(math32.NaN),
"NewArrayF32": reflect.ValueOf(math32.NewArrayF32),
"NewArrayU32": reflect.ValueOf(math32.NewArrayU32),
diff --git a/yaegicore/coresymbols/cogentcore_org-core-base-iox-imagex.go b/yaegicore/coresymbols/cogentcore_org-core-base-iox-imagex.go
index e7a527e758..825a464a24 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-base-iox-imagex.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-base-iox-imagex.go
@@ -49,6 +49,7 @@ func init() {
"Formats": reflect.ValueOf((*imagex.Formats)(nil)),
"JSON": reflect.ValueOf((*imagex.JSON)(nil)),
"JSONEncoded": reflect.ValueOf((*imagex.JSONEncoded)(nil)),
+ "JSRGBA": reflect.ValueOf((*imagex.JSRGBA)(nil)),
"TestingT": reflect.ValueOf((*imagex.TestingT)(nil)),
"Wrapped": reflect.ValueOf((*imagex.Wrapped)(nil)),
diff --git a/yaegicore/coresymbols/cogentcore_org-core-core.go b/yaegicore/coresymbols/cogentcore_org-core-core.go
index 00f3b5fafc..e390f901de 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-core.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-core.go
@@ -21,6 +21,8 @@ import (
func init() {
Symbols["cogentcore.org/core/core/core"] = map[string]reflect.Value{
// function, constant and variable definitions
+ "AddAppSettings": reflect.ValueOf(core.AddAppSettings),
+ "AddPrinterSettings": reflect.ValueOf(core.AddPrinterSettings),
"AllRenderWindows": reflect.ValueOf(&core.AllRenderWindows).Elem(),
"AllSettings": reflect.ValueOf(&core.AllSettings).Elem(),
"AppAbout": reflect.ValueOf(&core.AppAbout).Elem(),
diff --git a/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go b/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
index 7fd2b2a36f..8424337315 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-htmlcore.go
@@ -20,6 +20,7 @@ func init() {
"HasAttr": reflect.ValueOf(htmlcore.HasAttr),
"MDGetAttr": reflect.ValueOf(htmlcore.MDGetAttr),
"MDSetAttr": reflect.ValueOf(htmlcore.MDSetAttr),
+ "MDToHTML": reflect.ValueOf(htmlcore.MDToHTML),
"NewContext": reflect.ValueOf(htmlcore.NewContext),
"ReadHTML": reflect.ValueOf(htmlcore.ReadHTML),
"ReadHTMLString": reflect.ValueOf(htmlcore.ReadHTMLString),
diff --git a/yaegicore/coresymbols/cogentcore_org-core-paint.go b/yaegicore/coresymbols/cogentcore_org-core-paint.go
index 596fcd9446..67026f45c6 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-paint.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-paint.go
@@ -18,6 +18,7 @@ func init() {
"NewSVGRenderer": reflect.ValueOf(&paint.NewSVGRenderer).Elem(),
"NewSourceRenderer": reflect.ValueOf(&paint.NewSourceRenderer).Elem(),
"RenderToImage": reflect.ValueOf(paint.RenderToImage),
+ "RenderToPDF": reflect.ValueOf(paint.RenderToPDF),
"RenderToSVG": reflect.ValueOf(paint.RenderToSVG),
// type definitions
From 234f7e6266c68c55d40163a0814c65df4f0dbd6f Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Mon, 22 Dec 2025 10:58:11 +0100
Subject: [PATCH 82/99] pdf: fixes from kai review
---
content/content.go | 2 +-
content/url_js.go | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/content/content.go b/content/content.go
index 4647f7e288..44af15555d 100644
--- a/content/content.go
+++ b/content/content.go
@@ -496,7 +496,7 @@ func (ct *Content) PagePDF(path string) error {
fname := ct.currentPage.Name + ".pdf"
if path != "" {
- os.MkdirAll(path, 0777)
+ errors.Log(os.MkdirAll(path, 0777))
fname = filepath.Join(path, fname)
}
f, err := os.Create(fname)
diff --git a/content/url_js.go b/content/url_js.go
index 0b885b455a..de9a1fd844 100644
--- a/content/url_js.go
+++ b/content/url_js.go
@@ -28,7 +28,7 @@ var (
func (ct *Content) getPrintURL() string {
p := originalBase.String()
- return p[:len(p)-1]
+ return strings.TrimSuffix(p, "/")
}
// getWebURL returns the current relative web URL that should be passed to [Content.Open]
From f9fdeb94941a6401ae6b010498c7922c7536d44c Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 23 Dec 2025 06:54:27 +0100
Subject: [PATCH 83/99] pdf: key fix for xyz on web: need Size method on
renderer interface.
---
gpu/renderer.go | 3 +++
gpu/rendertexture.go | 4 ++++
gpu/surface.go | 4 ++++
xyz/render.go | 4 ++--
4 files changed, 13 insertions(+), 2 deletions(-)
diff --git a/gpu/renderer.go b/gpu/renderer.go
index bcb32fd806..ef295bff18 100644
--- a/gpu/renderer.go
+++ b/gpu/renderer.go
@@ -38,6 +38,9 @@ type Renderer interface {
// This is efficient to call if already the same size.
SetSize(size image.Point)
+ // Size returns the current render image size.
+ Size() image.Point
+
// Release frees associated GPU resources.
Release()
}
diff --git a/gpu/rendertexture.go b/gpu/rendertexture.go
index 17195d2d21..5b6ecc2cd2 100644
--- a/gpu/rendertexture.go
+++ b/gpu/rendertexture.go
@@ -117,6 +117,10 @@ func (rt *RenderTexture) SetSize(size image.Point) {
rt.ConfigFrames()
}
+func (rt *RenderTexture) Size() image.Point {
+ return rt.Format.Bounds().Size()
+}
+
func (rt *RenderTexture) ReleaseFrames() {
for _, fr := range rt.Frames {
fr.Release()
diff --git a/gpu/surface.go b/gpu/surface.go
index bfb8e3effb..7c5b469d45 100644
--- a/gpu/surface.go
+++ b/gpu/surface.go
@@ -102,6 +102,10 @@ func (sf *Surface) SetSize(sz image.Point) {
sf.Reconfig()
}
+func (sf *Surface) Size() image.Point {
+ return sf.Format.Bounds().Size()
+}
+
// GetCurrentTexture returns a TextureView that is the current
// target for rendering.
func (sf *Surface) GetCurrentTexture() (*wgpu.TextureView, error) {
diff --git a/xyz/render.go b/xyz/render.go
index 6428dbf351..b0d2ae1cd2 100644
--- a/xyz/render.go
+++ b/xyz/render.go
@@ -132,9 +132,9 @@ func (sc *Scene) RenderGrabImage() *image.NRGBA {
func (sc *Scene) render(grabImage bool) *image.NRGBA {
sc.Lock()
defer sc.Unlock()
- tex := sc.Phong.System.Renderer.(*gpu.RenderTexture)
+ tex := sc.Phong.System.Renderer
sc.Phong.System.SetClearColor(sc.Background.At(0, 0))
- sc.Camera.SetAspect(tex.Format.Bounds().Size())
+ sc.Camera.SetAspect(tex.Size())
if len(sc.SavedCams) == 0 {
sc.SaveCamera("default")
From 7abcf2501544c046df1936f841e46b025e8e1a3d Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 23 Dec 2025 21:48:58 +0100
Subject: [PATCH 84/99] pdf: how can I still be so confused about rows, columns
in matrix after all these years!
---
math32/matrix2.go | 2 +-
math32/matrix3.go | 3 +--
2 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/math32/matrix2.go b/math32/matrix2.go
index 533c495b32..c0f00a3033 100644
--- a/math32/matrix2.go
+++ b/math32/matrix2.go
@@ -39,7 +39,7 @@ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
*/
-// Matrix2 is a 2x3 matrix.
+// Matrix2 is a 3x2 matrix.
// [XX YX]
// [XY YY]
// [X0 Y0]
diff --git a/math32/matrix3.go b/math32/matrix3.go
index 94e36322c8..f2b69836e6 100644
--- a/math32/matrix3.go
+++ b/math32/matrix3.go
@@ -16,8 +16,7 @@ import "errors"
// with columns as the inner dimension.
type Matrix3 [9]float32
-// note: matrix indexes are row,column. matrix dimensions are CxR.
-// nothing like consistency...
+// note: matrix indexes and dimensions are row,column.
// Mat3 constructs a new Matrix3 from given values, in column-wise order.
func Mat3(n11, n21, n31, n12, n22, n32, n13, n23, n33 float32) Matrix3 {
From c5dbfaa3599c0235785c154badba99f56745221d Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 23 Dec 2025 22:37:33 +0100
Subject: [PATCH 85/99] pdf: remove matcolor.Scheme.Background in favor of
Surface, OnSurface
---
colors/matcolor/scheme.go | 12 ------------
core/render_notjs.go | 4 ++--
core/scene.go | 4 ++--
core/tabs.go | 2 +-
gpu/gpudraw/drawer.go | 2 +-
gpu/gsystem.go | 2 +-
styles/css.go | 2 +-
7 files changed, 8 insertions(+), 20 deletions(-)
diff --git a/colors/matcolor/scheme.go b/colors/matcolor/scheme.go
index 30f0c2905a..5b7c728ae2 100644
--- a/colors/matcolor/scheme.go
+++ b/colors/matcolor/scheme.go
@@ -81,12 +81,6 @@ type Scheme struct {
// InversePrimary is the color applied to interactive elements on top of InverseSurface
InversePrimary image.Image
- // Background is the color applied to the background of the app and other low-emphasis areas
- Background image.Image
-
- // OnBackground is the color applied to content on top of Background
- OnBackground image.Image
-
// Outline is the color applied to borders to create emphasized boundaries that need to have sufficient contrast
Outline image.Image
@@ -134,9 +128,6 @@ func NewLightScheme(p *Palette) Scheme {
InverseOnSurface: p.Neutral.AbsToneUniform(95),
InversePrimary: p.Primary.AbsToneUniform(80),
- Background: p.Neutral.AbsToneUniform(100),
- OnBackground: p.Neutral.AbsToneUniform(10),
-
Outline: p.NeutralVariant.AbsToneUniform(50),
OutlineVariant: p.NeutralVariant.AbsToneUniform(80),
@@ -182,9 +173,6 @@ func NewDarkScheme(p *Palette) Scheme {
InverseOnSurface: p.Neutral.AbsToneUniform(20),
InversePrimary: p.Primary.AbsToneUniform(40),
- Background: p.Neutral.AbsToneUniform(0),
- OnBackground: p.Neutral.AbsToneUniform(90),
-
Outline: p.NeutralVariant.AbsToneUniform(60),
OutlineVariant: p.NeutralVariant.AbsToneUniform(30),
diff --git a/core/render_notjs.go b/core/render_notjs.go
index c03fb03b41..169f61e31f 100644
--- a/core/render_notjs.go
+++ b/core/render_notjs.go
@@ -60,7 +60,7 @@ type fillInsetsSource struct {
func (ss *fillInsetsSource) Draw(c composer.Composer) {
cd := c.(*composer.ComposerDrawer)
- clr := colors.Scheme.Background
+ clr := colors.Scheme.Surface
fill := func(x0, y0, x1, y1 int) {
r := image.Rect(x0, y0, x1, y1)
@@ -77,7 +77,7 @@ func (ss *fillInsetsSource) Draw(c composer.Composer) {
fill(0, 0, rb.Min.X, wb.Max.Y) // left
}
-// fillInsets fills the window insets, if any, with [colors.Scheme.Background].
+// fillInsets fills the window insets, if any, with [colors.Scheme.Surface].
func (w *renderWindow) fillInsets(cp composer.Composer) {
// render geom and window geom
rg := w.SystemWindow.RenderGeom()
diff --git a/core/scene.go b/core/scene.go
index 40fd23d10f..a5eb283155 100644
--- a/core/scene.go
+++ b/core/scene.go
@@ -185,8 +185,8 @@ func (sc *Scene) Init() {
sc.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Clickable) // this is critical to enable click-off to turn off focus.
s.Cursor = cursors.Arrow
- s.Background = colors.Scheme.Background
- s.Color = colors.Scheme.OnBackground
+ s.Background = colors.Scheme.Surface
+ s.Color = colors.Scheme.OnSurface
// we never want borders on scenes
s.MaxBorder = styles.Border{}
s.Direction = styles.Column
diff --git a/core/tabs.go b/core/tabs.go
index 2ec2d54db2..4e5d4341e3 100644
--- a/core/tabs.go
+++ b/core/tabs.go
@@ -114,7 +114,7 @@ func (ts *Tabs) Init() {
ts.maxChars = 16
ts.CloseIcon = icons.Close
ts.Styler(func(s *styles.Style) {
- s.Color = colors.Scheme.OnBackground
+ s.Color = colors.Scheme.OnSurface
s.Grow.Set(1, 1)
if ts.Type.effective(ts).isColumn() {
s.Direction = styles.Row
diff --git a/gpu/gpudraw/drawer.go b/gpu/gpudraw/drawer.go
index 15a433aea4..3978e09f70 100644
--- a/gpu/gpudraw/drawer.go
+++ b/gpu/gpudraw/drawer.go
@@ -118,7 +118,7 @@ func (dw *Drawer) Start() {
defer dw.Unlock()
// always use the default background color for clearing in general
- dw.System.SetClearColor(colors.ToUniform(colors.Scheme.Background))
+ dw.System.SetClearColor(colors.ToUniform(colors.Scheme.Surface))
dw.opList = dw.opList[:0]
dw.images.start()
diff --git a/gpu/gsystem.go b/gpu/gsystem.go
index 98a84e88e3..e6aec7ff68 100644
--- a/gpu/gsystem.go
+++ b/gpu/gsystem.go
@@ -129,7 +129,7 @@ func (sy *GraphicsSystem) SetGraphicsDefaults() *GraphicsSystem {
for _, pl := range sy.GraphicsPipelines {
pl.SetGraphicsDefaults()
}
- sy.SetClearColor(colors.ToUniform(colors.Scheme.Background))
+ sy.SetClearColor(colors.ToUniform(colors.Scheme.Surface))
sy.SetClearDepthStencil(1, 0)
return sy
}
diff --git a/styles/css.go b/styles/css.go
index 12a7641ab1..c107b9d33d 100644
--- a/styles/css.go
+++ b/styles/css.go
@@ -103,7 +103,7 @@ func colorToCSS(c image.Image) string {
return "var(--secondary-container-color)"
case colors.Scheme.Secondary.OnContainer:
return "var(--secondary-on-container-color)"
- case colors.Scheme.Surface, colors.Scheme.OnSurface, colors.Scheme.Background, colors.Scheme.OnBackground:
+ case colors.Scheme.Surface, colors.Scheme.OnSurface:
return "" // already default
case colors.Scheme.SurfaceContainer, colors.Scheme.SurfaceContainerLowest, colors.Scheme.SurfaceContainerLow, colors.Scheme.SurfaceContainerHigh, colors.Scheme.SurfaceContainerHighest:
return "var(--surface-container-color)" // all of them are close enough for this
From 0f5920afb221d829b22adcbba974ed9f7d02efc0 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 24 Dec 2025 21:11:49 +0100
Subject: [PATCH 86/99] pdf: ZeroSpace() also nils BoxShadow and MaxBoxShadow
---
styles/style.go | 2 ++
1 file changed, 2 insertions(+)
diff --git a/styles/style.go b/styles/style.go
index e731de8358..e44a1a1489 100644
--- a/styles/style.go
+++ b/styles/style.go
@@ -255,6 +255,8 @@ func (s *Style) ZeroSpace() {
s.MaxBorder.Width.Zero()
s.Border.Width.Zero()
s.Gap.Zero()
+ s.BoxShadow = nil
+ s.MaxBoxShadow = nil
}
// VirtualKeyboards are all of the supported virtual keyboard types
From 214d0d9b58824423fa1076f0b4c7b1d38458ab76 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 24 Dec 2025 21:18:06 +0100
Subject: [PATCH 87/99] pdf: locale simplification
---
system/locale.go | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/system/locale.go b/system/locale.go
index 0ae52e8191..e94cf675ca 100644
--- a/system/locale.go
+++ b/system/locale.go
@@ -12,14 +12,8 @@ type Locale string
// Language returns the language portion of the locale tag (e.g., en, fr, ja)
func (l Locale) Language() string {
- if l == "" {
- return ""
- }
- pos := strings.LastIndex(string(l), "-")
- if pos < 0 {
- return string(l)
- }
- return string(l)[:pos]
+ lang, _, _ := strings.Cut(string(l), "-")
+ return lang
}
// Region returns the region portion of the locale tag (e.g., US, FR, JA)
From a7fc614606e1e0864a137b8ef2a161c064288558 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 24 Dec 2025 21:19:23 +0100
Subject: [PATCH 88/99] pdf: locale simplification p2
---
system/locale.go | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
diff --git a/system/locale.go b/system/locale.go
index e94cf675ca..dc63c1caf9 100644
--- a/system/locale.go
+++ b/system/locale.go
@@ -18,12 +18,6 @@ func (l Locale) Language() string {
// Region returns the region portion of the locale tag (e.g., US, FR, JA)
func (l Locale) Region() string {
- if l == "" {
- return ""
- }
- pos := strings.LastIndex(string(l), "-")
- if pos < 0 {
- return ""
- }
- return string(l)[pos+1:]
+ _, region, _ := strings.Cut(string(l), "-")
+ return region
}
From 641c747c109a78e4d3bf1fa330217ea2a746c833 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 24 Dec 2025 21:21:45 +0100
Subject: [PATCH 89/99] pdf: mdcite separator
---
text/csl/mdcite/mdcite.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/text/csl/mdcite/mdcite.go b/text/csl/mdcite/mdcite.go
index 0fed65513f..8f805edb18 100644
--- a/text/csl/mdcite/mdcite.go
+++ b/text/csl/mdcite/mdcite.go
@@ -52,7 +52,7 @@ func Generate(c *Config) error {
kl := csl.NewKeyList(refs)
if c.Dir == "" {
- c.Dir = "./"
+ c.Dir = "." + string(filepath.Separator)
}
mds := fsx.Filenames(c.Dir, ".md")
if len(mds) == 0 {
From adcbf182f8f56b60709bdb5b72ed25c1f81d0cab Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 24 Dec 2025 21:29:13 +0100
Subject: [PATCH 90/99] pdf: other review fixes
---
text/paginate/paginate_test.go | 2 --
text/paginate/runners.go | 2 +-
text/shaped/shaped_test.go | 1 +
3 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/text/paginate/paginate_test.go b/text/paginate/paginate_test.go
index 2b979df6bb..8a0feed63e 100644
--- a/text/paginate/paginate_test.go
+++ b/text/paginate/paginate_test.go
@@ -14,7 +14,6 @@ import (
"cogentcore.org/core/core"
"cogentcore.org/core/events"
"cogentcore.org/core/styles"
- "cogentcore.org/core/text/rich"
)
// RunTest runs a test for given test case.
@@ -29,7 +28,6 @@ func RunTest(t *testing.T, nm string, f func() *core.Body) {
<-showed
opts := NewOptions()
- opts.FontFamily = rich.Serif
opts.Header = NoFirst(HeaderLeftPageNumber("This is a test header"))
opts.Title = CenteredTitle("This is a Profound Statement of Something Important", "Bea A. Author", "University of Twente Department of Physiology", "March 1, 2024", `https://example.com/testing`, "The thing about this paper is that it is dealing with an issue that should be given more attention, but perhaps it really is hard to understand and that makes it difficult to get the attention it deserves. In any case, we are very proud.")
buff := bytes.Buffer{}
diff --git a/text/paginate/runners.go b/text/paginate/runners.go
index 10981d7180..124147e8bb 100644
--- a/text/paginate/runners.go
+++ b/text/paginate/runners.go
@@ -23,7 +23,7 @@ func TextStyler(s *styles.Style) {
s.Color = colors.Uniform(colors.Black)
}
-// CenteredPageNumber generates a page number cenetered in the frame
+// CenteredPageNumber generates a page number centered in the frame
// with a 1.5em space above it.
func CenteredPageNumber(frame *core.Frame, opts *Options, pageNo int) {
core.NewSpace(frame).Styler(func(s *styles.Style) { // space before
diff --git a/text/shaped/shaped_test.go b/text/shaped/shaped_test.go
index 4e735ead4b..f2b14367f4 100644
--- a/text/shaped/shaped_test.go
+++ b/text/shaped/shaped_test.go
@@ -169,6 +169,7 @@ func TestHebrew(t *testing.T) {
func TestVertical(t *testing.T) {
RunTest(t, "nihongo_ttb", 300, 300, func(pc *paint.Painter, sh Shaper, tsty *text.Style) {
+ // these are correctly inferred:
// rts.Language = "ja"
// rts.Script = language.Han
tsty.Direction = rich.TTB // rich.BTT // note: apparently BTT is actually never used
From abc39064b658833a22e36e73f34703cf99be385a Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 24 Dec 2025 22:37:09 +0100
Subject: [PATCH 91/99] pdf: always add printer settings, in core settings;
also fix issue with margins in printer settings.
---
colors/matcolor/scheme.go | 2 +-
content/settings.go | 2 --
core/settings.go | 7 +------
system/driver/base/app.go | 2 --
text/printer/settings.go | 1 +
yaegicore/coresymbols/cogentcore_org-core-core.go | 1 -
6 files changed, 3 insertions(+), 12 deletions(-)
diff --git a/colors/matcolor/scheme.go b/colors/matcolor/scheme.go
index 5b7c728ae2..0b841fff73 100644
--- a/colors/matcolor/scheme.go
+++ b/colors/matcolor/scheme.go
@@ -111,7 +111,7 @@ func NewLightScheme(p *Palette) Scheme {
Custom: map[string]Accent{},
SurfaceDim: p.Neutral.AbsToneUniform(87),
- Surface: p.Neutral.AbsToneUniform(99),
+ Surface: p.Neutral.AbsToneUniform(100),
SurfaceBright: p.Neutral.AbsToneUniform(99),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(100),
diff --git a/content/settings.go b/content/settings.go
index 9abe2bc62b..9ffe6fb3ba 100644
--- a/content/settings.go
+++ b/content/settings.go
@@ -8,12 +8,10 @@ import (
"time"
"cogentcore.org/core/content/bcontent"
- "cogentcore.org/core/core"
"cogentcore.org/core/text/paginate"
)
func init() {
- core.AddPrinterSettings()
Settings.Defaults()
}
diff --git a/core/settings.go b/core/settings.go
index 9e427a2ede..06e0601085 100644
--- a/core/settings.go
+++ b/core/settings.go
@@ -38,18 +38,13 @@ import (
// that the user will see in the settings window. It contains the base Cogent Core
// settings by default and should be modified by other apps to add their
// app settings.
-var AllSettings = []Settings{AppearanceSettings, SystemSettings, TimingSettings, DebugSettings}
+var AllSettings = []Settings{AppearanceSettings, SystemSettings, &printer.Settings, TimingSettings, DebugSettings}
// AddAppSettings inserts app-specific settings after the AppearanceSettings
func AddAppSettings(sets ...Settings) {
AllSettings = slices.Insert(AllSettings, 1, sets...)
}
-// AddPrinterSettings adds printer settings, for apps that use these.
-func AddPrinterSettings() {
- AddAppSettings(&printer.Settings)
-}
-
// Settings is the interface that describes the functionality common
// to all settings data types.
type Settings interface {
diff --git a/system/driver/base/app.go b/system/driver/base/app.go
index 10c3e34f2f..99a9599ea8 100644
--- a/system/driver/base/app.go
+++ b/system/driver/base/app.go
@@ -20,7 +20,6 @@ import (
"cogentcore.org/core/events/key"
"cogentcore.org/core/styles"
"cogentcore.org/core/system"
- "cogentcore.org/core/text/printer"
"github.com/jeandeaual/go-locale"
)
@@ -71,7 +70,6 @@ func Init(a system.App, ab *App) {
key.SystemPlatform = a.SystemPlatform().String()
// sl := a.SystemLocale()
// fmt.Println("locale:", sl, sl.Language(), sl.Region())
- printer.Settings.Defaults() // depends on system.TheApp
}
func (a *App) MainLoop() {
diff --git a/text/printer/settings.go b/text/printer/settings.go
index b1c7f598e5..f626b92656 100644
--- a/text/printer/settings.go
+++ b/text/printer/settings.go
@@ -57,6 +57,7 @@ type SettingsData struct {
func (ps *SettingsData) Defaults() {
ps.PageSize = DefaultPageSizeForRegion(system.TheApp.SystemLocale().Region())
+ ps.Update()
switch ps.Units {
case units.UnitMm:
ps.Margins.Set(25) // basically one inch
diff --git a/yaegicore/coresymbols/cogentcore_org-core-core.go b/yaegicore/coresymbols/cogentcore_org-core-core.go
index e390f901de..5bdac55c2f 100644
--- a/yaegicore/coresymbols/cogentcore_org-core-core.go
+++ b/yaegicore/coresymbols/cogentcore_org-core-core.go
@@ -22,7 +22,6 @@ func init() {
Symbols["cogentcore.org/core/core/core"] = map[string]reflect.Value{
// function, constant and variable definitions
"AddAppSettings": reflect.ValueOf(core.AddAppSettings),
- "AddPrinterSettings": reflect.ValueOf(core.AddPrinterSettings),
"AllRenderWindows": reflect.ValueOf(&core.AllRenderWindows).Elem(),
"AllSettings": reflect.ValueOf(&core.AllSettings).Elem(),
"AppAbout": reflect.ValueOf(&core.AppAbout).Elem(),
From 92082d01fd3f4f3a7222d8fd8bcd1de06485f330 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 30 Dec 2025 09:21:44 +0100
Subject: [PATCH 92/99] pdf: add NewQuatIdentity function
---
math32/quaternion.go | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/math32/quaternion.go b/math32/quaternion.go
index 97131593d9..e939b216ce 100644
--- a/math32/quaternion.go
+++ b/math32/quaternion.go
@@ -25,6 +25,11 @@ func NewQuat(x, y, z, w float32) Quat {
return Quat{X: x, Y: y, Z: z, W: w}
}
+// NewQuatIdentity returns a new quaternion with the identity rotation.
+func NewQuatIdentity() Quat {
+ return Quat{X: 0, Y: 0, Z: 0, W: 1}
+}
+
// NewQuatAxisAngle returns a new quaternion from given axis and angle rotation (radians).
func NewQuatAxisAngle(axis Vector3, angle float32) Quat {
nq := Quat{}
From 8c1c3d46495caee65e68969bfc9a5cee5c70f626 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Tue, 30 Dec 2025 09:22:20 +0100
Subject: [PATCH 93/99] pdf: yaegi update
---
yaegicore/basesymbols/cogentcore_org-core-math32.go | 1 +
1 file changed, 1 insertion(+)
diff --git a/yaegicore/basesymbols/cogentcore_org-core-math32.go b/yaegicore/basesymbols/cogentcore_org-core-math32.go
index 645aa5d4e3..e7c96a54ad 100644
--- a/yaegicore/basesymbols/cogentcore_org-core-math32.go
+++ b/yaegicore/basesymbols/cogentcore_org-core-math32.go
@@ -109,6 +109,7 @@ func init() {
"NewQuat": reflect.ValueOf(math32.NewQuat),
"NewQuatAxisAngle": reflect.ValueOf(math32.NewQuatAxisAngle),
"NewQuatEuler": reflect.ValueOf(math32.NewQuatEuler),
+ "NewQuatIdentity": reflect.ValueOf(math32.NewQuatIdentity),
"NewRay": reflect.ValueOf(math32.NewRay),
"NewSphere": reflect.ValueOf(math32.NewSphere),
"NewTriangle": reflect.ValueOf(math32.NewTriangle),
From 665c5f304b02ef5c5e901d209ca6cd082d53ae0f Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Wed, 31 Dec 2025 09:25:51 +0100
Subject: [PATCH 94/99] pdf: add WrapMax and WrapMinMax functions for efficient
wrapping (finally), including WrapPi convenience function, along with
associated tests, including for previously untested Truncate and IntMultiple
functions.
---
math32/math.go | 20 ++-
math32/math_test.go | 147 ++++++++++++++++++
.../basesymbols/cogentcore_org-core-math32.go | 3 +
3 files changed, 168 insertions(+), 2 deletions(-)
create mode 100644 math32/math_test.go
diff --git a/math32/math.go b/math32/math.go
index b67e02150e..d4f7aa97ec 100644
--- a/math32/math.go
+++ b/math32/math.go
@@ -780,8 +780,7 @@ func Yn(n int, x float32) float32 {
return math32.Yn(n, x)
}
-//////////////////////////////////////////////////////////////
-// Special additions to math. functions
+//////// Special additions to math. functions
// Clamp clamps x to the provided closed interval [a, b]
func Clamp[T cmp.Ordered](x, a, b T) T {
@@ -853,3 +852,20 @@ func Truncate64(val float64, prec int) float64 {
// pow := math.Pow(10, float64(prec))
// return math.Round(val*pow) / pow
}
+
+// WrapsMax returns x wrapped in the interval [0..max).
+func WrapMax(x, mx float32) float32 {
+ // integer math: (max + x % max) % max
+ return Mod(mx+Mod(x, mx), mx)
+}
+
+// WrapMinMax returns x wrapped in the interval [min..max).
+func WrapMinMax(x, mn, mx float32) float32 {
+ return mn + WrapMax(x-mn, mx-mn)
+}
+
+// WrapPi wraps the value into the [-Pi, Pi) interval.
+func WrapPi(x float32) float32 {
+ w := WrapMinMax(x, -Pi, Pi)
+ return w
+}
diff --git a/math32/math_test.go b/math32/math_test.go
new file mode 100644
index 0000000000..f90a24baef
--- /dev/null
+++ b/math32/math_test.go
@@ -0,0 +1,147 @@
+// Copyright (c) 2024, Cogent Core. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package math32
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+)
+
+func TestTruncate(t *testing.T) {
+ tests := []struct {
+ x float32
+ prec int
+ cor float32
+ }{
+ {x: Pi, prec: 1, cor: 3},
+ {x: Pi, prec: 2, cor: 3.1},
+ {x: Pi, prec: 3, cor: 3.14},
+ {x: Pi, prec: 4, cor: 3.142},
+ {x: Pi, prec: 5, cor: 3.1416},
+ {x: Pi, prec: 6, cor: 3.14159},
+ {x: Pi, prec: 7, cor: 3.141593},
+ }
+ for _, tt := range tests {
+ got := Truncate(tt.x, tt.prec)
+ assert.Equal(t, tt.cor, got)
+ }
+}
+
+func TestTruncate64(t *testing.T) {
+ tests := []struct {
+ x float64
+ prec int
+ cor float64
+ }{
+ {x: Pi, prec: 1, cor: 3},
+ {x: Pi, prec: 2, cor: 3.1},
+ {x: Pi, prec: 3, cor: 3.14},
+ {x: Pi, prec: 4, cor: 3.142},
+ {x: Pi, prec: 5, cor: 3.1416},
+ {x: Pi, prec: 6, cor: 3.14159},
+ {x: Pi, prec: 7, cor: 3.141593},
+ }
+ for _, tt := range tests {
+ got := Truncate64(tt.x, tt.prec)
+ assert.Equal(t, tt.cor, got)
+ }
+}
+
+func TestIntMultiple(t *testing.T) {
+ tests := []struct {
+ x, mod, cor float32
+ }{
+ {x: 2, mod: 1, cor: 2},
+ {x: 2.1, mod: 1, cor: 2},
+ {x: 2.4, mod: 1, cor: 2},
+ {x: 2.5, mod: 1, cor: 3},
+ {x: 2.9, mod: 1, cor: 3},
+ {x: 1000, mod: 50, cor: 1000},
+ {x: 1005, mod: 50, cor: 1000},
+ {x: 1020, mod: 50, cor: 1000},
+ {x: 1025, mod: 50, cor: 1050},
+ {x: 1049, mod: 50, cor: 1050},
+ }
+ for _, tt := range tests {
+ got := IntMultiple(tt.x, tt.mod)
+ assert.Equal(t, tt.cor, got)
+ }
+}
+
+func TestIntMultipleGE(t *testing.T) {
+ tests := []struct {
+ x, mod, cor float32
+ }{
+ {x: 2, mod: 1, cor: 2},
+ {x: 2.1, mod: 1, cor: 3},
+ {x: 2.4, mod: 1, cor: 3},
+ {x: 2.5, mod: 1, cor: 3},
+ {x: 2.9, mod: 1, cor: 3},
+ {x: 1000, mod: 50, cor: 1000},
+ {x: 1005, mod: 50, cor: 1050},
+ {x: 1020, mod: 50, cor: 1050},
+ {x: 1025, mod: 50, cor: 1050},
+ {x: 1049, mod: 50, cor: 1050},
+ }
+ for _, tt := range tests {
+ got := IntMultipleGE(tt.x, tt.mod)
+ assert.Equal(t, tt.cor, got)
+ }
+}
+
+func TestWrapMax(t *testing.T) {
+ tests := []struct {
+ x, mx, cor float32
+ }{
+ {x: 2, mx: 1, cor: 0},
+ {x: 2.5, mx: 2, cor: 0.5},
+ {x: 10002.5, mx: 2, cor: 0.5},
+ {x: -2.5, mx: 2, cor: 1.5},
+ {x: -200.5, mx: 2, cor: 1.5},
+ {x: 3.14, mx: 3.1, cor: 0.04},
+ }
+ for _, tt := range tests {
+ got := WrapMax(tt.x, tt.mx)
+ assert.InDelta(t, tt.cor, got, 1e-5)
+ }
+}
+
+func TestWrapMinMax(t *testing.T) {
+ tests := []struct {
+ x, mn, mx, cor float32
+ }{
+ {x: 2, mn: -1, mx: 1, cor: 0},
+ {x: 2.5, mn: -2, mx: 2, cor: -1.5},
+ {x: 10002.5, mn: -2, mx: 2, cor: -1.5},
+ {x: -2.5, mn: -2, mx: 2, cor: 1.5},
+ {x: -200.5, mn: -2, mx: 2, cor: -0.5},
+ {x: 3.14, mn: -3.1, mx: 3.1, cor: -3.06},
+ }
+ for _, tt := range tests {
+ got := WrapMinMax(tt.x, tt.mn, tt.mx)
+ assert.InDelta(t, tt.cor, got, 1e-5)
+ }
+}
+
+func TestWrapPi(t *testing.T) {
+ tests := []struct {
+ x, cor float32
+ }{
+ {x: 0, cor: 0},
+ {x: Pi, cor: -Pi},
+ {x: -Pi, cor: -Pi},
+ {x: 3 * Pi, cor: -Pi},
+ {x: -3 * Pi, cor: -Pi},
+ {x: 4 * Pi, cor: 0},
+ {x: 0.5 * Pi, cor: 0.5 * Pi},
+ {x: -0.5 * Pi, cor: -0.5 * Pi},
+ {x: 2 * Pi, cor: 0},
+ }
+ for _, tt := range tests {
+ got := WrapPi(tt.x)
+ assert.InDelta(t, tt.cor, got, 1e-5)
+ }
+}
diff --git a/yaegicore/basesymbols/cogentcore_org-core-math32.go b/yaegicore/basesymbols/cogentcore_org-core-math32.go
index e7c96a54ad..6d853916c9 100644
--- a/yaegicore/basesymbols/cogentcore_org-core-math32.go
+++ b/yaegicore/basesymbols/cogentcore_org-core-math32.go
@@ -177,6 +177,9 @@ func init() {
"Vector4FromVector3": reflect.ValueOf(math32.Vector4FromVector3),
"Vector4Scalar": reflect.ValueOf(math32.Vector4Scalar),
"W": reflect.ValueOf(math32.W),
+ "WrapMax": reflect.ValueOf(math32.WrapMax),
+ "WrapMinMax": reflect.ValueOf(math32.WrapMinMax),
+ "WrapPi": reflect.ValueOf(math32.WrapPi),
"X": reflect.ValueOf(math32.X),
"Y": reflect.ValueOf(math32.Y),
"Y0": reflect.ValueOf(math32.Y0),
From ac6d590e4e1c301bd63577910a2fd54b93cd2af3 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Thu, 1 Jan 2026 09:05:46 +0100
Subject: [PATCH 95/99] pdf: handle case that occurs in new text in canvas
where stroke = none gets set as fully transparent color, in which case text
render mode should not render stroke.
---
paint/pdf/text.go | 18 ++++++++++++++----
1 file changed, 14 insertions(+), 4 deletions(-)
diff --git a/paint/pdf/text.go b/paint/pdf/text.go
index a2fe929299..26ee83172e 100644
--- a/paint/pdf/text.go
+++ b/paint/pdf/text.go
@@ -175,10 +175,19 @@ func (r *PDF) textRun(style *styles.Paint, m math32.Matrix2, run *shapedgt.Run,
}
}
-func (r *PDF) setTextStrokeColor(clr image.Image) {
+// setTextStrokeColor sets the text stroke color. Returns false
+// if stroke color is fully transparent and thus should not be rendered.
+func (r *PDF) setTextStrokeColor(clr image.Image) bool {
sc := r.w.style().Stroke
+ if x, ok := clr.(*image.Uniform); ok {
+ c := colors.ApplyOpacity(colors.AsRGBA(x), sc.Opacity)
+ if c.A == 0 { // fully transparent
+ return false
+ }
+ }
sc.Color = clr
r.w.SetStroke(&sc)
+ return true
}
func (r *PDF) setTextFillColor(clr image.Image) {
@@ -193,16 +202,17 @@ func (r *PDF) setTextStyle(fnt *text.Font, style *styles.Paint, fill, stroke ima
sty := fnt.Style(tsty)
r.w.SetFont(sty, tsty)
mode := 0
+ hasStroke := false
if stroke != nil {
- r.setTextStrokeColor(stroke)
+ hasStroke = r.setTextStrokeColor(stroke)
}
if fill != nil {
r.setTextFillColor(fill)
- if stroke != nil {
+ if hasStroke {
mode = 2
}
} else {
- if stroke != nil {
+ if hasStroke {
mode = 1
}
}
From 166ced6437750b0174c533b8cba00c82ecb53416 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sun, 4 Jan 2026 07:24:34 -0800
Subject: [PATCH 96/99] pdf: math32 additions: IsNaN() methods for Vector, Quat
for finding bugs, and MinAngleDiff for subtracting angles.
---
math32/math.go | 13 +++++++++++++
math32/math_test.go | 16 ++++++++++++++++
math32/quaternion.go | 4 ++++
math32/vector3.go | 4 ++++
4 files changed, 37 insertions(+)
diff --git a/math32/math.go b/math32/math.go
index d4f7aa97ec..7e5fca01d4 100644
--- a/math32/math.go
+++ b/math32/math.go
@@ -869,3 +869,16 @@ func WrapPi(x float32) float32 {
w := WrapMinMax(x, -Pi, Pi)
return w
}
+
+// MinAngleDiff returns the minimum difference between two angles
+// (in radians): a-b, dealing with the wrap-around issues with angles.
+func MinAngleDiff(a, b float32) float32 {
+ d := WrapPi(a) - WrapPi(b)
+ if d > Pi {
+ d -= 2 * Pi
+ }
+ if d < -Pi {
+ d += 2 * Pi
+ }
+ return d
+}
diff --git a/math32/math_test.go b/math32/math_test.go
index f90a24baef..9ec6bebca5 100644
--- a/math32/math_test.go
+++ b/math32/math_test.go
@@ -145,3 +145,19 @@ func TestWrapPi(t *testing.T) {
assert.InDelta(t, tt.cor, got, 1e-5)
}
}
+
+func TestMinAngleDiff(t *testing.T) {
+ tests := []struct {
+ a, b, cor float32
+ }{
+ {a: 0, b: Pi, cor: Pi},
+ {a: Pi, b: -Pi - 0.5, cor: 0.5},
+ {a: -Pi, b: Pi + 0.5, cor: -0.5},
+ {a: Pi, b: -Pi, cor: 0},
+ {a: Pi, b: -Pi + 0.1, cor: -0.1},
+ }
+ for _, tt := range tests {
+ got := MinAngleDiff(tt.a, tt.b)
+ assert.InDelta(t, tt.cor, got, 1e-5)
+ }
+}
diff --git a/math32/quaternion.go b/math32/quaternion.go
index e939b216ce..e9b377a192 100644
--- a/math32/quaternion.go
+++ b/math32/quaternion.go
@@ -86,6 +86,10 @@ func (q Quat) IsNil() bool {
return q.X == 0 && q.Y == 0 && q.Z == 0 && q.W == 0
}
+func (q Quat) IsNaN() bool {
+ return IsNaN(q.X) || IsNaN(q.Y) || IsNaN(q.Z) || IsNaN(q.W)
+}
+
func (q Quat) String() string {
return fmt.Sprintf("(%v, %v, %v, %v)", q.X, q.Y, q.Z, q.W)
}
diff --git a/math32/vector3.go b/math32/vector3.go
index ca5255a2a5..0d1e34d0d2 100644
--- a/math32/vector3.go
+++ b/math32/vector3.go
@@ -120,6 +120,10 @@ func (v Vector3) ToSlice(array []float32, offset int) {
array[offset+2] = v.Z
}
+func (v Vector3) IsNaN() bool {
+ return IsNaN(v.X) || IsNaN(v.Y) || IsNaN(v.Z)
+}
+
// Basic math operations:
// Add adds the other given vector to this one and returns the result as a new vector.
From 413c6cdb5c603c64b71912534e3d83a214963a86 Mon Sep 17 00:00:00 2001
From: "Randall C. O'Reilly"
Date: Sun, 4 Jan 2026 07:26:50 -0800
Subject: [PATCH 97/99] pdf: Surface color for dark theme is 0, like the
now-removed Background value.
---
colors/matcolor/scheme.go | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/colors/matcolor/scheme.go b/colors/matcolor/scheme.go
index 0b841fff73..1976ee5094 100644
--- a/colors/matcolor/scheme.go
+++ b/colors/matcolor/scheme.go
@@ -156,7 +156,7 @@ func NewDarkScheme(p *Palette) Scheme {
Custom: map[string]Accent{},
SurfaceDim: p.Neutral.AbsToneUniform(6),
- Surface: p.Neutral.AbsToneUniform(6),
+ Surface: p.Neutral.AbsToneUniform(0),
SurfaceBright: p.Neutral.AbsToneUniform(24),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(4),
From 8ca5bf1932554f5a5868a983b8232f01642eeb52 Mon Sep 17 00:00:00 2001
From: Kai O'Reilly
Date: Mon, 5 Jan 2026 16:08:21 -0800
Subject: [PATCH 98/99] content: MakeToolbarPDF function
---
content/buttons.go | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/content/buttons.go b/content/buttons.go
index d58a32c10e..b14f42c21b 100644
--- a/content/buttons.go
+++ b/content/buttons.go
@@ -17,6 +17,8 @@ import (
"cogentcore.org/core/tree"
)
+// MakeToolbar adds the standard toolbar buttons for the content.
+// See [Content.MakeToolbarPDF] for the optional PDF button.
func (ct *Content) MakeToolbar(p *tree.Plan) {
if false && ct.SizeClass() == core.SizeCompact { // TODO: implement hamburger menu for compact
tree.Add(p, func(w *core.Button) {
@@ -74,6 +76,10 @@ func (ct *Content) MakeToolbar(p *tree.Plan) {
ct.Scene.MenuSearchDialog("Search", "Search "+core.TheApp.Name())
})
})
+}
+
+// MakeToolbarPDF adds the PDF button to the toolbar. This is optional.
+func (ct *Content) MakeToolbarPDF(p *tree.Plan) {
tree.Add(p, func(w *core.Button) {
w.SetText("PDF").SetIcon(icons.PictureAsPdf).SetTooltip("PDF generates and opens / downloads the current page as a printable PDF file. See the Settings/Printer panel (Command+,) for settings.")
w.OnClick(func(e events.Event) {
From cec4708ffef5c819266e7f93e1bee4251e272119 Mon Sep 17 00:00:00 2001
From: Kai O'Reilly
Date: Mon, 5 Jan 2026 16:17:28 -0800
Subject: [PATCH 99/99] matcolor: scheme fixes
---
colors/matcolor/scheme.go | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/colors/matcolor/scheme.go b/colors/matcolor/scheme.go
index 1976ee5094..5ff3368870 100644
--- a/colors/matcolor/scheme.go
+++ b/colors/matcolor/scheme.go
@@ -112,7 +112,7 @@ func NewLightScheme(p *Palette) Scheme {
SurfaceDim: p.Neutral.AbsToneUniform(87),
Surface: p.Neutral.AbsToneUniform(100),
- SurfaceBright: p.Neutral.AbsToneUniform(99),
+ SurfaceBright: p.Neutral.AbsToneUniform(100),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(100),
SurfaceContainerLow: p.Neutral.AbsToneUniform(98),
@@ -156,7 +156,7 @@ func NewDarkScheme(p *Palette) Scheme {
Custom: map[string]Accent{},
SurfaceDim: p.Neutral.AbsToneUniform(6),
- Surface: p.Neutral.AbsToneUniform(0),
+ Surface: p.Neutral.AbsToneUniform(6),
SurfaceBright: p.Neutral.AbsToneUniform(24),
SurfaceContainerLowest: p.Neutral.AbsToneUniform(4),