diff --git a/go.mod b/go.mod index 817a54a..549cbd0 100644 --- a/go.mod +++ b/go.mod @@ -3,9 +3,11 @@ module github.com/go-pdfkit/app go 1.26.4 require ( - github.com/go-pdfkit/ops v0.5.0 - github.com/go-pdfkit/reader v0.4.1 - github.com/go-pdfkit/render v0.7.0 + github.com/go-gfx/gfx v0.10.0 + github.com/go-pdfkit/forms v0.2.1 + github.com/go-pdfkit/ops v0.7.0 + github.com/go-pdfkit/reader v0.5.0 + github.com/go-pdfkit/render v0.10.0 github.com/go-widgets/painter v0.11.0 github.com/go-widgets/toolkit v0.250.0 github.com/go-widgets/webcanvas v0.1.0 @@ -16,14 +18,12 @@ require ( github.com/coder/websocket v1.8.15 // indirect github.com/go-crdt/collab v0.25.0 // indirect github.com/go-crdt/crdt v0.31.0 // indirect - github.com/go-gfx/gfx v0.10.0 // indirect github.com/go-iconoir/iconoir v0.2.0 // indirect github.com/go-images/images v0.0.0-20260811115337-bc5d586f8e38 // indirect github.com/go-opentype/fonts v0.9.0 // indirect - github.com/go-opentype/opentype v0.9.0 // indirect + github.com/go-opentype/opentype v0.10.0 // indirect github.com/go-opentype/shape v0.5.0 // indirect - github.com/go-pdfkit/forms v0.2.1 // indirect - github.com/go-pdfkit/pdffont v0.2.0 // indirect + github.com/go-pdfkit/pdffont v0.3.0 // indirect github.com/go-richdoc/richdoc v0.2.0 // indirect github.com/go-typeset/bidi v0.3.0 // indirect github.com/go-widgets/mvvm v0.5.0 // indirect diff --git a/go.sum b/go.sum index b35a32b..7b0affe 100644 --- a/go.sum +++ b/go.sum @@ -20,24 +20,20 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-opentype/fonts v0.9.0 h1:slB6OB3riLyUPrOxqXe0s6/AzdenF1TDvCN8N87hhQk= github.com/go-opentype/fonts v0.9.0/go.mod h1:C6yQL2apHItfEZ5hztpsHF0S5mlX/hklLlq/Z5fRG/g= -github.com/go-opentype/opentype v0.9.0 h1:GFgcJ3nwTDp4NJr5O+Paw7lhZx5Jv/R+noZwvhYDlkM= -github.com/go-opentype/opentype v0.9.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU= +github.com/go-opentype/opentype v0.10.0 h1:cZVMZ3RVkcijXmxlmpqyVkjlNc33aSB2ugKVWYvoLUg= +github.com/go-opentype/opentype v0.10.0/go.mod h1:AOixevJf7XQaH7+WG+OMIOZEbYPXfMqklVk26Y6YTUU= github.com/go-opentype/shape v0.5.0 h1:jHNaOMHNBdDj5EixOevlrrsi92svMxvVMKN7GYaPfxo= github.com/go-opentype/shape v0.5.0/go.mod h1:3ImRYNIj6zpwWQ/DV3BWhgMFfmzDITH6XpZw2auQHTo= github.com/go-pdfkit/forms v0.2.1 h1:INa2GwAadxEhcXvBe7zJYgapYwaGn9WIaLxUJU/lm6A= github.com/go-pdfkit/forms v0.2.1/go.mod h1:LORxkdP4FVULFk0/PA0CdPZc1dZF9zTSaHrtfM7IYaw= -github.com/go-pdfkit/ops v0.4.0 h1:2WeYwDhN2OyoYtjYCdgm7gWGVNmiu+8OwOKEhyb3Cp0= -github.com/go-pdfkit/ops v0.4.0/go.mod h1:X89phxHICCYl6+zn3L6YXH3mRLBlsLdPYuDCryGLXzI= -github.com/go-pdfkit/ops v0.5.0 h1:b1LF7hF9QsqtF2AC0G7HjwulNCXJ0AOE7zaUSzswXrg= -github.com/go-pdfkit/ops v0.5.0/go.mod h1:Is3FBR2NcCUEzNt4eowdsVa+P0SBtGtcxK8lDlOeyrk= -github.com/go-pdfkit/pdffont v0.2.0 h1:yAp/oR5Z2kkqs4r0GWMalZMC7rc7XSCZXgwIypbpMWM= -github.com/go-pdfkit/pdffont v0.2.0/go.mod h1:y4vo5DgT95e57C3XxIWfA/xss+x6RwZwyj6KWdJc86s= -github.com/go-pdfkit/reader v0.4.0 h1:qPbNZSO+Xl+4NBvQoV1PYt7HlqHaEAQ1tUc6/IL8JAU= -github.com/go-pdfkit/reader v0.4.0/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8= -github.com/go-pdfkit/reader v0.4.1 h1:pRxFqRjsn7H/VsGfWb9nYWyFuDgTU2Pjmoq/f5mgVq4= -github.com/go-pdfkit/reader v0.4.1/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8= -github.com/go-pdfkit/render v0.7.0 h1:wTEvYxcJkYFYCXJMzrGcMUo0gVgp4A+VqAsTS/hyFq8= -github.com/go-pdfkit/render v0.7.0/go.mod h1:RCleIv5QnEDbh9kILa7J8pNUNDUyhsx5p8hv06lpcLk= +github.com/go-pdfkit/ops v0.7.0 h1:W8sK//0nfs/OACvjzvw/GK5ywtd90eDoYcdv74GxW8M= +github.com/go-pdfkit/ops v0.7.0/go.mod h1:Is3FBR2NcCUEzNt4eowdsVa+P0SBtGtcxK8lDlOeyrk= +github.com/go-pdfkit/pdffont v0.3.0 h1:G5DKcAmsZJ0e17QhSrcUaL7PKEXKjUx4P/iGMl7bAbI= +github.com/go-pdfkit/pdffont v0.3.0/go.mod h1:bfmNLna1l1CljNX/Utg55YzFylovfmI7sJnvgA3bzKI= +github.com/go-pdfkit/reader v0.5.0 h1:DaJ5C6eKXPRG9Cdu+olA/0KLmsQgyHEC1coteQQuBxU= +github.com/go-pdfkit/reader v0.5.0/go.mod h1:fQFOVfCMUui1AdvD4qhimdyvvNr9KvvJ1S7IuKZjyV8= +github.com/go-pdfkit/render v0.10.0 h1:2NXlxhb7bDWFfgWpMe1bfZXbefhEkLSeH99xWI8B1uk= +github.com/go-pdfkit/render v0.10.0/go.mod h1:ZNuGzwPk1m3kmJIg9X0bepSGrAmmCb5nTMbDEbHuFFA= github.com/go-richdoc/richdoc v0.2.0 h1:z9cLox9MoInZL6fIlweMzgDT/VqgnB2ZucSIEaRFglY= github.com/go-richdoc/richdoc v0.2.0/go.mod h1:aCX8ulqg5CoKqSWgWeEWul1Oj0d2VxQeaQNI0+jsm7c= github.com/go-typeset/bidi v0.3.0 h1:4fjGjejvjE2LzLNzY4si8PkVO321NcsKIiANhWT3jF4= diff --git a/scene.go b/scene.go index bb31c55..8f8552f 100644 --- a/scene.go +++ b/scene.go @@ -6,7 +6,9 @@ package main import ( + "errors" "fmt" + "time" "github.com/go-pdfkit/ops" "github.com/go-pdfkit/reader" @@ -346,15 +348,37 @@ func (s *state) renderPage() { if s.at < 1 { s.at = 1 } - img, err := drawPage(src, s.at, render.Options{Scale: s.fitScale(src)}) - if err != nil { + img, err := drawPage(src, s.at, render.Options{ + Scale: s.fitScale(src), + MaxDuration: pageBudget, + }) + // A page that ran out of time comes back as far as it got, which is worth + // showing: somebody scrolling would rather see most of a figure than a + // sentence saying there was one. Anything else, and there is no picture. + partial := errors.Is(err, render.ErrTimedOut) && img != nil + if err != nil && !partial { s.view = toolkit.NewFrame(toolkit.NewLabel("this page cannot be drawn: " + err.Error())) return } + if partial { + // Showing half a page without saying so would be the one thing worse + // than showing nothing, so the status line says which it is. This is + // the only place renderPage writes the note, and it overwrites + // whatever was there, because what is on the screen right now matters + // more than what happened before it. + s.note = fmt.Sprintf("this page was still being drawn after %s; this is as far as it got", pageBudget) + } s.page = toolkit.NewImageFit(img.Pix, img.W, img.H) s.view = toolkit.NewFrame(s.page) } +// pageBudget is how long one page may be drawn for before what has been drawn +// so far is shown instead. A handful of pages take minutes — of the 59 432 +// pages the renderer was measured against, 1 131 were still going after twenty +// seconds and one took two hundred and seventy-three — and a window that stops +// answering for that long reads as broken rather than as busy. +const pageBudget = 5 * time.Second + // fitScale is how much to magnify the page so that it fills the view without // spilling out of it. func (s *state) fitScale(src *reader.Document) float64 { diff --git a/scene_test.go b/scene_test.go index 5fa86d2..f6da0cc 100644 --- a/scene_test.go +++ b/scene_test.go @@ -4,9 +4,12 @@ import ( "errors" "strings" "testing" + "time" + "github.com/go-gfx/gfx/raster" "github.com/go-pdfkit/ops" "github.com/go-pdfkit/reader" + "github.com/go-pdfkit/render" "github.com/go-widgets/toolkit" ) @@ -588,3 +591,59 @@ func TestAControlIsWideEnoughForItsName(t *testing.T) { t.Errorf("the strip is %d wide and the surface is %d", total, surfaceW-2*margin) } } + +func TestAPageThatRanOutOfTimeIsShownAsFarAsItGot(t *testing.T) { + // The renderer is given a budget, and a page that overruns it comes back + // half drawn together with render.ErrTimedOut. Half a page is worth more + // to somebody scrolling than a sentence saying there was one, so the + // workbench shows it rather than the error. + s, _ := opened(t, 1) + was := drawPage + t.Cleanup(func() { drawPage = was }) + var budget time.Duration + drawPage = func(_ *reader.Document, _ int, opt render.Options) (*raster.Image, error) { + budget = opt.MaxDuration + return raster.New(4, 4), render.ErrTimedOut + } + + s.refresh() + if budget <= 0 { + t.Error("the renderer was given no budget at all") + } + if s.page == nil { + t.Fatal("the part of the page that was drawn was thrown away") + } + if !strings.Contains(s.note, "as far as it got") { + t.Errorf("half a page was shown without saying so; the status line said %q", s.note) + } + if got := s.statusLine(); !strings.Contains(strings.Join(got, "|"), "as far as it got") { + t.Errorf("the status line does not carry it: %q", got) + } + buf := buffer() + s.draw(buf) + if inked(buf, s.theme.Background) == 0 { + t.Error("nothing reached the screen") + } +} + +func TestAPageThatRanOutOfTimeWithNothingDrawnSaysSo(t *testing.T) { + // Coming back timed out and empty is not something the renderer does, + // but if it did there is no picture to show, so the view has to fall + // back on saying why rather than dereferencing nothing. + s, _ := opened(t, 1) + was := drawPage + t.Cleanup(func() { drawPage = was }) + drawPage = func(*reader.Document, int, render.Options) (*raster.Image, error) { + return nil, render.ErrTimedOut + } + + s.refresh() + if s.page != nil { + t.Error("a page was drawn from nothing") + } + buf := buffer() + s.draw(buf) + if inked(buf, s.theme.Background) == 0 { + t.Error("the reason was not drawn") + } +}