Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
75 commits
Select commit Hold shift + click to select a range
23b5e6f
pdf: basic pdf writer converted from tdewolff canvas, working for paths
rcoreilly Oct 2, 2025
5891134
pdf: pdfrender updated, layers supported
rcoreilly Oct 2, 2025
52a83e0
pdf: units converting to points properly, top-down coordinate system …
rcoreilly Oct 2, 2025
2923307
pdf: screenshot saves PDFs -- buttons in demo looks good (minus the t…
rcoreilly Oct 2, 2025
054109a
pdf: fix image rendering
rcoreilly Oct 2, 2025
1da7202
pdf: text rendering working -- tricky issues with the bottom-left coo…
rcoreilly Oct 2, 2025
dfe506d
pdf: close on text rotation but not quite.. save that for later for n…
rcoreilly Oct 3, 2025
60b2267
pdf: include transform in layers; eliminate empty push-pop sequences …
rcoreilly Oct 3, 2025
c7cb621
pdf: move pdf to top level in paint -- no need to be under ppath
rcoreilly Oct 3, 2025
9d5658d
pdf: fix import and yaegicore
rcoreilly Oct 3, 2025
284a4e1
pdf: pagination minimally working; pdf writer is not writing multiple…
rcoreilly Oct 3, 2025
ea9a396
pdf: multipage working
rcoreilly Oct 3, 2025
5e2f4fb
pdf: page layout working but doesn't look right. major update to Shap…
rcoreilly Oct 3, 2025
ac20cf1
pdf: handles font setting, updating, layout looks good.
rcoreilly Oct 3, 2025
67f7b5a
pdf: first-pass working on docs / content -- still many issues.. :)
rcoreilly Oct 3, 2025
8a83173
pdf: working solidly on docs!
rcoreilly Oct 3, 2025
09b5097
pdf: fix text rotation -- changing to top-left inevitably changes rot…
rcoreilly Oct 4, 2025
a7987eb
pdf: runners as functions, working well
rcoreilly Oct 5, 2025
5338325
pdf: context stack tracks pdf state better -- svg test not working we…
rcoreilly Oct 5, 2025
7cd80ac
pdf: new pagination framework with different stages -- much better. p…
rcoreilly Oct 5, 2025
cc25528
pdf: render.Context stores both the incremental Transform and Cumulat…
rcoreilly Oct 6, 2025
8691e11
pdf: minor svg_test fixup
rcoreilly Oct 6, 2025
c4904c4
pdf: fix shaped_test -- emoji is not working since some point!
rcoreilly Oct 6, 2025
86cdc08
pdf: paginate title example working, and content settings
rcoreilly Oct 6, 2025
a9af368
pdf: title setting url default etc
rcoreilly Oct 6, 2025
6a8d75f
pdf: title etc updated, added elements to content, changed content Au…
rcoreilly Oct 7, 2025
8001db5
pdf: start on links: got positioning right finally, but need to deal …
rcoreilly Oct 7, 2025
d92aca4
pdf: math rendering working
rcoreilly Oct 7, 2025
a8a136d
pdf: content and htmlcore element handling improved: setting tags alw…
rcoreilly Oct 7, 2025
3d3a820
pdf: move content handlers into handlers and cleanup; figure captions…
rcoreilly Oct 7, 2025
0f0b1c8
pdf: handle text improvements; block for figures to keep together -- …
rcoreilly Oct 7, 2025
a031688
pdf: move pre handler with BindTextEditor into content -- did not bel…
rcoreilly Oct 7, 2025
10c5e18
pdf: gap and left padding layout preserved in paginate
rcoreilly Oct 7, 2025
229ec7f
pdf: MouseUp always clears Active state regardless -- can be set for …
rcoreilly Oct 7, 2025
d14f362
pdf: fix the completely bizarre first-text-line-offset issue with PDF…
rcoreilly Oct 7, 2025
9a085e7
pdf: render uniform boxes, fix path cases -- just need gradient now; …
rcoreilly Oct 8, 2025
a497b88
pdf: pdf sizing issues all fixed -- need to pre-render in the same ge…
rcoreilly Oct 8, 2025
20df3c5
pdf: js/web version downloads the PDF file -- fixed crash there and h…
rcoreilly Oct 8, 2025
6441853
pdf: pdf generates references for current page; updated csl to use fs…
rcoreilly Oct 8, 2025
f8dc3f6
pdf: pdf links fully working finally. wow that was an ordeal. also: m…
rcoreilly Oct 9, 2025
9e7ab29
pdf: content set DelayedImageLoad = false regardless of current defau…
rcoreilly Oct 9, 2025
d79315c
pdf: csl generates html formatted text for references, including link…
rcoreilly Oct 9, 2025
e3fa632
pdf: support markdown in figure captions.
rcoreilly Oct 9, 2025
7749931
pdf: fixed remaining pdf incompatibility issues: now fully compliant!…
rcoreilly Oct 9, 2025
76471ba
pdf: README with validator link
rcoreilly Oct 9, 2025
c4dc38c
use ImageRenderer in pdf -- key for web
rcoreilly Oct 9, 2025
a614881
pdf: use the right base url
rcoreilly Oct 9, 2025
368bd53
pdf: fix for web-based PDF generation: images were not available b/c …
rcoreilly Oct 9, 2025
92d8414
pdf: yeah that semicolon thing is real. what wasn't real was my bad s…
rcoreilly Oct 9, 2025
17c7fa4
pdf: parse/golang: prevent recursive type loop from crashing
rcoreilly Oct 10, 2025
dea8cd1
pdf: printer.Settings handles all the standard printer settings, plug…
rcoreilly Oct 10, 2025
7c550bb
pdf: misc settings fixes -- all good
rcoreilly Oct 10, 2025
6f93ef2
pdf: more printer formatting settings and enforcement of actual font …
rcoreilly Oct 12, 2025
75097f6
pdf: even more printer formatting settings including header styling f…
rcoreilly Oct 12, 2025
84ffd34
pdf: table of contents outline generation in place
rcoreilly Oct 12, 2025
1a8978c
pdf: toc root points to start of doc
rcoreilly Oct 12, 2025
5914096
pdf: tests identify that the nomial gradient coordinates need to be u…
rcoreilly Oct 12, 2025
dcae805
pdf: gradients mostly working
rcoreilly Oct 13, 2025
4d18c93
pdf: key fixes to gradient function to handle case with non- 0,1 grad…
rcoreilly Oct 13, 2025
972fae6
pdf: rendering fixes and svg io marshal fix to not output duplicate t…
rcoreilly Oct 13, 2025
552fcb9
pdf: fix for bp_delta case: always push the transform..
rcoreilly Oct 13, 2025
454ea3a
pdf: pdf passing most svg tests -- good enough for now!
rcoreilly Oct 13, 2025
d3a3e84
pdf: svg to pdf and svg clone methods
rcoreilly Oct 14, 2025
d7a86f6
pdf: fix scene handle layout issues introduced by earlier LayoutFrame…
rcoreilly Oct 14, 2025
2744144
pdf: finally fix the svg testdata directory structure so pdf and png …
rcoreilly Oct 14, 2025
3ccc95b
pdf: convenience RenderToPDF func
rcoreilly Oct 14, 2025
a81b079
pdf: minor cleanup of addlink
rcoreilly Oct 14, 2025
bcc3cac
pdf: add OnlineURL for docs
rcoreilly Oct 15, 2025
63dcaa5
pdf: don't run TestFontEmoji
rcoreilly Oct 21, 2025
ca0781c
Merge branch 'main' into pdf
rcoreilly Oct 24, 2025
c8d48bf
pdf: paginate ensures font is black and use light mode so links are t…
rcoreilly Nov 4, 2025
80f098a
pdf: tables have less space between rows -- standard formatting for t…
rcoreilly Nov 20, 2025
59071c0
pdf: change matcolor scheme to use 100 and 0 for Background: the almo…
rcoreilly Nov 22, 2025
5b9ee06
pdf: matcolor scheme also needs to brighten the surface values a notc…
rcoreilly Nov 22, 2025
2bd13fd
pdf: minor math32 vector reordering and doc
rcoreilly Nov 30, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions base/iox/imagex/wrapjs_js.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
6 changes: 6 additions & 0 deletions base/iox/imagex/wrapjs_notjs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
11 changes: 9 additions & 2 deletions colors/gradient/linear.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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) {
Expand Down
6 changes: 6 additions & 0 deletions colors/gradient/radial.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 8 additions & 8 deletions colors/matcolor/scheme.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,14 +117,14 @@ func NewLightScheme(p *Palette) Scheme {
Custom: map[string]Accent{},

SurfaceDim: p.Neutral.AbsToneUniform(87),
Surface: p.Neutral.AbsToneUniform(98),
SurfaceBright: 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),
Expand All @@ -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),
Expand Down Expand Up @@ -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),
Expand Down
12 changes: 10 additions & 2 deletions content/bcontent/page.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,16 @@ type Page struct {
// DateString is only used for parsing the date from the TOML front matter.
DateString string `toml:"Date" json:"-"`

// Authors are the optional authors of the page.
Authors []string
// Authors are the optional author(s) of the page.
Authors string

// Affiliations are optional institutional affiliations of the authors.
// This is only used for the PDF output.
Affiliations string

// Abstract is an optional abstract.
// This is only used for the PDF output.
Abstract string

// Draft indicates that the page is a draft and should not be visible on the web.
Draft bool
Expand Down
6 changes: 6 additions & 0 deletions content/buttons.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,12 @@ func (ct *Content) MakeToolbar(p *tree.Plan) {
ct.Scene.MenuSearchDialog("Search", "Search "+core.TheApp.Name())
})
})
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) {
ct.PagePDF("pdfs")
})
})
}

func (ct *Content) MenuSearch(items *[]core.ChooserItem) {
Expand Down
164 changes: 90 additions & 74 deletions content/content.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,14 @@ import (
"bytes"
"cmp"
"fmt"
"io"
"io/fs"
"net/http"
"os"
"path/filepath"
"slices"
"strconv"
"strings"

"github.com/gomarkdown/markdown/ast"
"golang.org/x/exp/maps"

"cogentcore.org/core/base/errors"
Expand All @@ -32,10 +31,10 @@ import (
"cogentcore.org/core/htmlcore"
"cogentcore.org/core/math32"
"cogentcore.org/core/styles"
"cogentcore.org/core/styles/abilities"
"cogentcore.org/core/styles/units"
"cogentcore.org/core/system"
"cogentcore.org/core/text/csl"
"cogentcore.org/core/text/paginate"
"cogentcore.org/core/tree"
)

Expand Down Expand Up @@ -103,6 +102,10 @@ type Content struct {
// if any (in kebab-case).
currentHeading string

// inPDFRender indicates that it is rendering a PDF now, turning off
// elements that are not appropriate for that.
inPDFRender bool

// The previous and next page, if applicable. They must be stored on this struct
// to avoid stale local closure variables.
prevPage, nextPage *bcontent.Page
Expand All @@ -126,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)
}
Expand All @@ -138,73 +142,9 @@ func (ct *Content) Init() {
errors.Log(ct.embedPage(ctx))
return true
}
ct.Context.AttributeHandlers["id"] = func(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)
ch := node.GetChildren()
if len(ch) == 2 { // image or table
if entering {
sty := htmlcore.MDGetAttr(node, "style")
if sty != "" {
if img, ok := ch[1].(*ast.Image); ok {
htmlcore.MDSetAttr(img, "style", sty)
delete(node.AsContainer().Attribute.Attrs, "style")
}
}
return false
}
cp := "\n<p><b>" + lbl + ":</b>"
if img, ok := ch[1].(*ast.Image); ok {
// fmt.Printf("Image: %s\n", string(img.Destination))
// fmt.Printf("Image: %#v\n", img)
nc := len(img.Children)
if nc > 0 {
if txt, ok := img.Children[0].(*ast.Text); ok {
// fmt.Printf("text: %s\n", string(txt.Literal)) // not formatted!
cp += " " + string(txt.Literal) // todo: not formatted!
}
}
} else {
title := htmlcore.MDGetAttr(node, "title")
if title != "" {
cp += " " + title
}
}
cp += "</p>\n"
w.Write([]byte(cp))
} else if entering {
cp := "\n<span id=\"" + value + "\"><b>" + lbl + ":</b>"
title := htmlcore.MDGetAttr(node, "title")
if title != "" {
cp += " " + title
}
cp += "</span>\n"
w.Write([]byte(cp))
// fmt.Println("id:", value, lbl)
// fmt.Printf("%#v\n", node)
}
return false
}
ct.Context.AddWidgetHandler(func(w core.Widget) {
switch x := w.(type) {
case *core.Text:
x.Styler(func(s *styles.Style) {
s.Max.X.Ch(120)
})
case *core.Image:
x.Styler(func(s *styles.Style) {
s.SetAbilities(true, abilities.Clickable, abilities.DoubleClickable)
s.Overflow.Set(styles.OverflowAuto)
})
x.OnDoubleClick(func(e events.Event) {
d := core.NewBody("Image")
core.NewImage(d).SetImage(x.Image)
d.RunWindowDialog(x)
})
}
})
ct.Context.ElementHandlers["pre"] = ct.htmlPreHandler
ct.Context.AttributeHandlers["id"] = ct.htmlIDAttributeHandler
ct.Context.AddWidgetHandler(ct.widgetHandler)

ct.Maker(func(p *tree.Plan) {
if ct.currentPage == nil {
Expand All @@ -224,19 +164,19 @@ func (ct *Content) Init() {
}
})
w.Maker(func(p *tree.Plan) {
if ct.currentPage.Title != "" {
if !ct.inPDFRender && ct.currentPage.Title != "" {
tree.Add(p, func(w *core.Text) {
w.SetType(core.TextDisplaySmall)
w.Updater(func() {
w.SetText(ct.currentPage.Title)
})
})
}
if len(ct.currentPage.Authors) > 0 {
if !ct.inPDFRender && len(ct.currentPage.Authors) > 0 {
tree.Add(p, func(w *core.Text) {
w.SetType(core.TextTitleLarge)
w.Updater(func() {
w.SetText("By " + strcase.FormatList(ct.currentPage.Authors...))
w.SetText("By " + ct.currentPage.Authors)
})
})
}
Expand All @@ -257,7 +197,9 @@ func (ct *Content) Init() {
errors.Log(ct.loadPage(w))
})
})
ct.makeBottomButtons(p)
if !ct.inPDFRender {
ct.makeBottomButtons(p)
}
})
})
})
Expand Down Expand Up @@ -372,6 +314,12 @@ func (ct *Content) addHistory(pg *bcontent.Page) {
ct.saveWebURL()
}

// reloadPage reloads the current page
func (ct *Content) reloadPage() {
ct.renderedPage = nil
ct.Update()
}

// loadPage loads the current page content into the given frame if it is not already loaded.
func (ct *Content) loadPage(w *core.Frame) error {
if ct.renderedPage == ct.currentPage {
Expand Down Expand Up @@ -529,3 +477,71 @@ func (ct *Content) setStageTitle() {
rw.SetStageTitle(name)
}
}

// PagePDF generates a PDF of the current page, to given file path
// (directory). the page name is the file name.
func (ct *Content) PagePDF(path string) error {
if ct.currentPage == nil {
return errors.Log(errors.New("Page empty"))
}
core.MessageSnackbar(ct, "Generating PDF...")

Settings.PDF.FontScale = (100.0 / core.AppearanceSettings.DocsFontSize)

ct.inPDFRender = true
ct.reloadPage()
ct.inPDFRender = false

refs := ct.PageRefs(ct.currentPage)

fname := ct.currentPage.Name + ".pdf"
if path != "" {
os.MkdirAll(path, 0777)
fname = filepath.Join(path, fname)
}
f, err := os.Create(fname)
if errors.Log(err) != nil {
return err
}
opts := Settings.PageSettings(ct, ct.currentPage)
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)
af := errors.Log1(filepath.Abs(fname))
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
}
// 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
}
3 changes: 3 additions & 0 deletions content/examples/basic/basic.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@ import (
"cogentcore.org/core/content"
"cogentcore.org/core/core"
"cogentcore.org/core/htmlcore"
_ "cogentcore.org/core/text/tex"
_ "cogentcore.org/core/yaegicore"
)

//go:embed content
var econtent embed.FS

func main() {
content.Settings.SiteTitle = "Cogent Content Example"
content.OfflineURL = "https://example.com"
b := core.NewBody("Cogent Content Example")
ct := content.NewContent(b).SetContent(econtent)
ct.Context.AddWikilinkHandler(htmlcore.GoDocWikilink("doc", "cogentcore.org/core"))
Expand Down
Loading
Loading