Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
95 changes: 86 additions & 9 deletions read.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
package main

import (
"archive/zip"
"bytes"
"errors"
"fmt"
Expand Down Expand Up @@ -40,6 +41,10 @@ func (s *state) readGroup() *column {
func() { s.read("") }), bareH)
box.add(button("Hand over this page as a PNG", toolkit.ButtonDefault,
s.pageAsPNG), bareH)
box.add(button("Hand over every page, zipped", toolkit.ButtonDefault,
s.everyPageZipped), bareH)
box.add(button("Hand over what this page says", toolkit.ButtonDefault,
s.textAsFile), bareH)
box.add(toolkit.NewLabel("None of these changes the document."), bareH)
return box
}
Expand Down Expand Up @@ -139,30 +144,102 @@ func (s *state) pageAsPNG() {
s.fail(msg)
return
}
img, err := drawPage(src, s.at, render.Options{
png, partial, err := s.drawnPNG(src, s.at)
if err != nil {
s.fail(err.Error())
return
}
s.handOver(fmt.Sprintf("page%03d.png", s.at), png)
if partial {
s.note += fmt.Sprintf("; this page was still being drawn after %s, so that is as far as it got", pageBudget)
s.refresh()
}
}

// drawnPNG draws one page and writes it, saying whether what came back is all
// of it.
func (s *state) drawnPNG(src *reader.Document, at int) (data []byte, partial bool, err error) {
img, err := drawPage(src, at, render.Options{
Scale: 2 * s.fitScale(src),
MaxDuration: pageBudget,
})
// A page that ran out of time comes back as far as it got. Handing that
// over silently would be handing over half a page as though it were the
// page, so this says which it is.
partial := errors.Is(err, render.ErrTimedOut) && img != nil
// page, so the caller is told which it is.
partial = errors.Is(err, render.ErrTimedOut) && img != nil
if err != nil && !partial {
s.fail("this page cannot be drawn: " + err.Error())
return
return nil, false, fmt.Errorf("this page cannot be drawn: %w", err)
}
var buf bytes.Buffer
if err := encodePNG(&buf, img); err != nil {
s.fail("this page cannot be written as a PNG: " + err.Error())
return nil, false, fmt.Errorf("this page cannot be written as a PNG: %w", err)
}
return buf.Bytes(), partial, nil
}

// everyPageZipped hands over the whole document as pictures, in one file.
//
// A page at a time is no use for a document of two hundred, and a browser that
// is handed two hundred downloads at once asks about each of them. A zip of
// PNGs is also what a comic reader opens under the name CBZ, which is the same
// file with another suffix.
func (s *state) everyPageZipped() {
if s.doc == nil {
s.fail("open a document first")
return
}
s.handOver(fmt.Sprintf("page%03d.png", s.at), buf.Bytes())
if partial {
s.note += fmt.Sprintf("; this page was still being drawn after %s, so that is as far as it got", pageBudget)
src, msg := s.reopen()
if msg != "" {
s.fail(msg)
return
}
var buf bytes.Buffer
zw := zip.NewWriter(&buf)
short := 0
for at := 1; at <= src.PageCount(); at++ {
png, partial, err := s.drawnPNG(src, at)
if err != nil {
s.fail(err.Error())
return
}
if partial {
short++
}
// The zip is built in memory: Create only refuses a name already
// used or a writer already closed, neither of which can happen with
// one entry per page number, and a bytes.Buffer never fails to take
// bytes. Close only flushes.
w, _ := zw.Create(fmt.Sprintf("page%03d.png", at))
w.Write(png)
}
zw.Close()
s.handOver(strings.TrimSuffix(s.name, ".pdf")+"-pages.zip", buf.Bytes())
if short > 0 {
s.note += fmt.Sprintf("; %d of them were still being drawn after %s", short, pageBudget)
s.refresh()
}
}

// textAsFile hands over what the page says, which the reading beside it shows
// on the screen and had no way of taking away.
func (s *state) textAsFile() {
if s.doc == nil {
s.fail("open a document first")
return
}
src, msg := s.reopen()
if msg != "" {
s.fail(msg)
return
}
text, err := extract.Text(src, s.at)
if err != nil {
s.fail("this page cannot be read: " + err.Error())
return
}
s.handOver(fmt.Sprintf("page%03d.txt", s.at), []byte(text))
}

// encodePNG is a variable so a test can watch what happens when writing the
// picture fails, which is the branch that decides whether a person is handed a
// truncated file or told.
Expand Down
140 changes: 140 additions & 0 deletions verbs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"testing"

"archive/zip"
"github.com/go-gfx/gfx/codec"
"github.com/go-gfx/gfx/raster"
"github.com/go-pdfkit/extract"
Expand Down Expand Up @@ -699,3 +700,142 @@ func TestAPictureThatCannotBeWrittenOut(t *testing.T) {
t.Errorf("it said %q", s.note)
}
}

func TestEveryPageZipped(t *testing.T) {
// A page at a time is no use for a document of two hundred, and a browser
// handed two hundred downloads at once asks about each of them.
s, h := opened(t, 1)
s.everyPageZipped()
if !strings.HasSuffix(h.as, "-pages.zip") {
t.Fatalf("it was handed over as %q", h.as)
}
zr, err := zip.NewReader(bytes.NewReader(h.saved), int64(len(h.saved)))
if err != nil {
t.Fatalf("what was handed over is not a zip: %v", err)
}
if len(zr.File) != s.doc.PageCount() {
t.Fatalf("%d files for %d pages", len(zr.File), s.doc.PageCount())
}
// Every one of them is a picture, not an empty entry with a name.
for _, f := range zr.File {
rc, err := f.Open()
if err != nil {
t.Fatal(err)
}
data, err := io.ReadAll(rc)
rc.Close()
if err != nil {
t.Fatal(err)
}
if codec.Sniff(data) != codec.PNG {
t.Errorf("%s is not a PNG", f.Name)
}
}
}

func TestWhatThePageSaysIsHandedOver(t *testing.T) {
// The reading beside it shows the words on the screen and had no way of
// taking them away.
h := &fakeHost{name: "words.pdf", file: wordyPDF(t)}
s := newState(surfaceW, surfaceH, h)
s.open()
if s.doc == nil {
t.Fatalf("the document did not open: %q", s.note)
}
s.textAsFile()
if h.as != "page001.txt" {
t.Fatalf("it was handed over as %q", h.as)
}
if len(h.saved) == 0 {
t.Error("a page with words on it handed over nothing")
}
}

func TestNothingOpenToHandOver(t *testing.T) {
for _, verb := range []struct {
name string
run func(*state)
}{
{"every page zipped", (*state).everyPageZipped},
{"what the page says", (*state).textAsFile},
} {
t.Run(verb.name, func(t *testing.T) {
s := newState(surfaceW, surfaceH, &fakeHost{})
verb.run(s)
if !strings.Contains(s.note, "open a document first") {
t.Errorf("it said %q", s.note)
}
})
}
}

func TestAZipOfPagesThatWillNotDraw(t *testing.T) {
s, h := opened(t, 1)
was := drawPage
t.Cleanup(func() { drawPage = was })
drawPage = func(*reader.Document, int, render.Options) (*raster.Image, error) {
return nil, errors.New("no")
}
s.everyPageZipped()
if h.as != "" {
t.Errorf("a zip was handed over anyway: %q", h.as)
}
if !strings.Contains(s.note, "cannot be drawn") {
t.Errorf("it said %q", s.note)
}
}

func TestAZipOfPagesStillBeingDrawn(t *testing.T) {
s, _ := opened(t, 1)
was := drawPage
t.Cleanup(func() { drawPage = was })
drawPage = func(d *reader.Document, i int, o render.Options) (*raster.Image, error) {
img, _ := was(d, i, o)
return img, render.ErrTimedOut
}
s.everyPageZipped()
if !strings.Contains(s.note, "still being drawn") {
t.Errorf("it said %q", s.note)
}
}

func TestADocumentThatCannotBeReopenedForPictures(t *testing.T) {
for _, verb := range []struct {
name string
run func(*state)
}{
{"page as a PNG", (*state).pageAsPNG},
{"every page zipped", (*state).everyPageZipped},
{"what the page says", (*state).textAsFile},
} {
t.Run(verb.name, func(t *testing.T) {
s, _ := opened(t, 1)
was := docBytes
t.Cleanup(func() { docBytes = was })
docBytes = func(*ops.Doc) ([]byte, error) { return nil, errors.New("the ink ran out") }
verb.run(s)
if !strings.Contains(s.note, "cannot be written") {
t.Errorf("it said %q", s.note)
}
})
}
}

func TestAPageWhoseWordsCannotBeRead(t *testing.T) {
// A page whose content stream will not decode has no words to hand over,
// and saying so beats handing over an empty file as though the page were
// blank.
s := newState(surfaceW, surfaceH, &fakeHost{name: "odd.pdf", file: unreadablePDF(t)})
s.open()
if s.doc == nil {
t.Fatal("the document did not open")
}
h, _ := s.host.(*fakeHost)
s.textAsFile()
if h.as != "" {
t.Errorf("something was handed over anyway: %q", h.as)
}
if !strings.Contains(s.note, "cannot be read") {
t.Errorf("it said %q", s.note)
}
}
Loading