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
42 changes: 29 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,27 +30,43 @@ touch a server, and there is no server to touch.
## What is on the strip, and what is beside the page

**Open** · **Save** · **<** · **>** · **Rotate** · **Delete**, and then
one control per group of verbs: **Pages** · **Sheet** · **Marks** · **File**,
with **Fill in** at the end for a document that carries a form. The arrow keys
turn pages too.
one control per group of verbs: **Pages** · **Sheet** · **Marks** · **File** ·
**Protect** · **Read**, with **Fill in** at the end for a document that carries
a form. The arrow keys turn pages too.

A strip cannot hold the rest. The verbs the library offers mostly have to be
told something first — which pages, how many to a sheet, what to write — and
there is nowhere on a row of buttons to say it. So a group opens a panel
**beside** the page rather than instead of it:
told something first — which pages, how many to a sheet, what to write, which
password — and there is nowhere on a row of buttons to say it. So a group opens
a panel **beside** the page rather than instead of it:

- **Pages** — which pages to keep or drop, turn them a quarter, a half or
three quarters, reverse the order, move the one on the screen, crop to a
box, put a blank page in, split into files of *n* pages.
- **Sheet** — *n* pages to a sheet, fold into a booklet, add another file
after this one, lay another file over it.
- **Marks** — write a watermark across every page.
- **File** — strip what runs rather than shows.

Beside, because every one of those changes the document and the document is
drawn from what would be saved: typing a crop box and watching the page come
back cropped is the whole point of the control, and a panel over the page
would hide the one thing worth looking at.
- **Marks** — a watermark, page numbers in a shape you choose, a Bates number
padded and started where you say, and a stamp in one of nine places.
- **File** — sanitize, flatten the annotations in, drop the annotations, the
bookmarks or what the file says about itself, pack it smaller, give it a
title and an author.
- **Protect** — open a file that has a password, put a user and an owner
password on this one and say what a reader may do with it, or take the
protection off.
- **Read** — what the page says, and what it carries: every picture on it,
listed with its size and its shape, each one handed over under a name that
says what it holds.

A protected document is still drawn, because the workbench reads its own
output back with the password it was just given. It is not the same bytes
twice — encryption needs randomness, by design — but it is the same document,
protected the same way.

Beside, because nearly every one of those changes the document and the document
is drawn from what would be saved: typing a crop box and watching the page come
back cropped is the whole point of the control, and a panel over the page would
hide the one thing worth looking at. **Read** is the exception that keeps the
rule — it changes nothing, and what it reads it reads out of the document
written and read back, so what it lists is what would come out of Save.

Every change is applied to the document, written out, and read back before
it is drawn — so what is on the screen is what would come out of Save,
Expand Down
61 changes: 40 additions & 21 deletions browsercheck/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -149,23 +149,29 @@ func check(ctx context.Context, c *conn, page, sample, shot, dl string) error {
// driven here too, because a panel that is drawn and gets no events looks
// exactly like one that works — which is what it was until the toolkit
// underneath learned to hand a press to what is inside a scroll view.
marked, err := drivePanel(ctx, c, sid)
// What counts as a verb having run is that the file the tab hands back is
// a DIFFERENT file. Some of the panels change what is on the screen
// without changing the document at all — the Read group replaces the
// picture of the page with what the page says — and a check that stopped
// at the first press to redraw the canvas would call that a verb.
var again []byte
marked, err := drivePanel(ctx, c, sid, func() (bool, error) {
drain(c)
if err := click(ctx, c, sid, savedAt, stripY); err != nil {
return false, err
}
got, err := waitForPDF(ctx, dl, out, 6*time.Second)
if err != nil {
return false, nil // the same file back: nothing was changed
}
again = got
return true, nil
})
if err != nil {
say(c)
return err
}
fmt.Println("a verb pressed in the panel changed the page:", marked)

// And what the tab hands back now carries the mark, which is the whole
// claim: what is on the screen is what comes out of Save.
drain(c)
if err := click(ctx, c, sid, savedAt, 8+15); err != nil {
return err
}
again, err := waitForPDF(ctx, dl, out)
if err != nil {
return err
}
fmt.Println("a verb pressed in the panel changed the document:", marked)
fmt.Printf("the tab handed back a %d byte PDF after the panel was used\n", len(again))
doc, err := reader.Open(again)
if err != nil {
Expand Down Expand Up @@ -203,7 +209,7 @@ const (
//
// It works from the right hand end of the strip, so that the sweep never
// presses the controls that drop a page.
func drivePanel(ctx context.Context, c *conn, sid string) (string, error) {
func drivePanel(ctx context.Context, c *conn, sid string, confirm func() (bool, error)) (string, error) {
quiet, err := lookIn(ctx, c, sid, pageBand, 1)
if err != nil {
return "", err
Expand Down Expand Up @@ -238,7 +244,7 @@ func drivePanel(ctx context.Context, c *conn, sid string) (string, error) {
}
tried[panel.Hash] = true
fmt.Printf("a panel opened from x=%d\n", x)
hit, err := pressDownPanel(ctx, c, sid)
hit, err := pressDownPanel(ctx, c, sid, confirm)
if err != nil {
return "", err
}
Expand All @@ -257,9 +263,9 @@ func drivePanel(ctx context.Context, c *conn, sid string) (string, error) {
return "", fmt.Errorf("no group on the strip opened a panel with a verb in it")
}

// pressDownPanel presses down the open panel until the page beside it changes,
// and says where that press was.
func pressDownPanel(ctx context.Context, c *conn, sid string) (string, error) {
// pressDownPanel presses down the open panel until a press both changes the
// page beside it and changes the document, and says where that press was.
func pressDownPanel(ctx context.Context, c *conn, sid string, confirm func() (bool, error)) (string, error) {
page, err := lookIn(ctx, c, sid, 0, pageBand)
if err != nil {
return "", err
Expand All @@ -272,9 +278,22 @@ func pressDownPanel(ctx context.Context, c *conn, sid string) (string, error) {
if err != nil {
return "", err
}
if changed {
if !changed {
continue
}
did, err := confirm()
if err != nil {
return "", err
}
if did {
return fmt.Sprintf("pressed at y=%d", y), nil
}
// It redrew and changed nothing that would be saved. Take the page as
// it now stands as the new baseline and carry on down the panel.
page, err = lookIn(ctx, c, sid, 0, pageBand)
if err != nil {
return "", err
}
}
return "", nil
}
Expand Down Expand Up @@ -303,9 +322,9 @@ func changedIn(ctx context.Context, c *conn, sid string, from, to float64, was i

// waitForPDF waits for a PDF to land in the download directory that is not the
// one already seen.
func waitForPDF(ctx context.Context, dl string, notThis []byte) ([]byte, error) {
func waitForPDF(ctx context.Context, dl string, notThis []byte, patience time.Duration) ([]byte, error) {
var out []byte
err := until(ctx, 30*time.Second, func() (bool, error) {
err := until(ctx, patience, func() (bool, error) {
files, _ := os.ReadDir(dl)
for _, f := range files {
b, err := os.ReadFile(filepath.Join(dl, f.Name()))
Expand Down
227 changes: 227 additions & 0 deletions file.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
// The File and Protect groups: what happens to the file rather than to any
// page of it.
//
// Everything in File is a switch the writer is thrown before it writes, and
// none of them can be thrown back — the library takes each as a decision about
// the file that will be written, not as a setting to be toggled. So they are
// buttons that say what they did, rather than boxes to tick that would be
// stuck on once ticked.

package main

import (
"fmt"

"github.com/go-pdfkit/ops"
"github.com/go-pdfkit/reader"
"github.com/go-widgets/toolkit"
)

// fileGroup is the panel.
func (s *state) fileGroup() *column {
box := newColumn()
box.add(button("Sanitize — drop what runs", toolkit.ButtonDefault, s.sanitize), bareH)
box.add(button("Flatten the annotations in", toolkit.ButtonDefault, s.flatten), bareH)
box.add(button("Drop every annotation", toolkit.ButtonDanger, s.dropAnnots), bareH)
box.add(button("Drop the bookmarks", toolkit.ButtonDanger, s.dropOutlines), bareH)
box.add(button("Drop what it says about itself", toolkit.ButtonDanger, s.clearInfo), bareH)
box.add(button("Pack it smaller", toolkit.ButtonDefault, s.compress), bareH)

box.add(s.entryRow("Title", "what the document is called", "",
func(v string) { s.tools.title = v }), labelledH)
box.add(s.entryRow("Author", "who wrote it", "",
func(v string) { s.tools.author = v }), labelledH)
box.add(button("Say so in the file", toolkit.ButtonDefault, s.setInfo), bareH)
return box
}

// flatten draws the annotations into the pages, so that what a form says is
// part of the page rather than a layer over it.
func (s *state) flatten() {
s.changeSaying("flattened what was drawn over the pages", func(d *ops.Doc) error {
d.Flatten()
return nil
})
}

// dropAnnots removes every annotation, links included.
func (s *state) dropAnnots() {
s.changeSaying("dropped every annotation", func(d *ops.Doc) error {
d.RemoveAnnotations()
return nil
})
}

// dropOutlines removes the bookmarks.
func (s *state) dropOutlines() {
s.changeSaying("dropped the bookmarks", func(d *ops.Doc) error {
d.DropOutlines()
return nil
})
}

// clearInfo drops what the file says about itself: who made it, with what, and
// when — which is the part of a document nobody means to send.
func (s *state) clearInfo() {
s.changeSaying("dropped what it said about itself", func(d *ops.Doc) error {
d.ClearInfo()
return nil
})
}

// compress packs the objects into compressed streams, which is what every
// writer since PDF 1.5 does.
func (s *state) compress() {
before := s.written()
if !s.changeSaying("packed", func(d *ops.Doc) error {
d.Compress()
return nil
}) {
return
}
after := s.written()
if before == 0 || after == 0 {
// The redraw is about to say which of the two steps went wrong, and it
// says it better than a pair of zeroes would.
return
}
// Saying how much smaller is the only way to know it did anything, and it
// costs a write that was going to happen anyway.
s.note = fmt.Sprintf("packed: %d bytes rather than %d", after, before)
s.refresh()
}

// written is how large the file would be if it were saved now, or zero when
// there is nothing to write or it cannot be written at all — which the control
// that asked, or the redraw after it, says for itself.
func (s *state) written() int {
if s.doc == nil {
return 0
}
out, msg := s.reopenBytes()
if msg != "" {
return 0
}
return len(out)
}

// setInfo puts a title and an author into the file.
func (s *state) setInfo() {
title, author := s.tools.title, s.tools.author
s.changeSaying("said who it is by", func(d *ops.Doc) error {
d.SetInfo("Title", title)
d.SetInfo("Author", author)
return nil
})
}

// allowed is what a reader of the protected file may do.
var allowed = []struct {
name string
bit reader.Permissions
}{
{"print it", reader.PermPrint},
{"change it", reader.PermModify},
{"copy text out of it", reader.PermCopy},
{"annotate it", reader.PermAnnotate},
{"fill its forms in", reader.PermFillForms},
}

// protectGroup is the panel: a password to open a file that has one, and a
// password to put on the file that will be written.
func (s *state) protectGroup() *column {
box := newColumn()
box.add(s.secretRow("Password to open a file with", func(v string) { s.tools.openPw = v }), labelledH)
box.add(toolkit.NewLabel("Type it before pressing Open."), bareH)

box.add(s.secretRow("User password", func(v string) { s.tools.userPw = v }), labelledH)
box.add(s.secretRow("Owner password", func(v string) { s.tools.ownerPw = v }), labelledH)
for _, a := range allowed {
name := a.name
box.add(tickRow("May "+name, s.tools.allow[name],
func(on bool) { s.tools.allow[name] = on }), bareH)
}
box.add(button("Protect it", toolkit.ButtonProminent, s.encrypt), bareH)
box.add(button("Take the protection off", toolkit.ButtonDanger, s.decrypt), bareH)
box.add(button("What is it protected with?", toolkit.ButtonDefault, s.protection), bareH)
return box
}

// secretRow is a box that shows dots rather than what is typed into it.
func (s *state) secretRow(label string, to func(string)) toolkit.Widget {
e := toolkit.NewEntry("")
e.Mask = '•'
e.Text().Subscribe(to)
s.typing = append(s.typing, e)
return toolkit.NewFormField(label, e)
}

// encrypt protects the file that will be written.
//
// What is drawn afterwards is still what would be saved: the workbench writes
// the document, and reads it back with the password just given. It is not the
// same bytes twice — encryption needs randomness, so every redraw produces a
// different file — but it is the same document, protected the same way.
func (s *state) encrypt() {
if s.tools.userPw == "" && s.tools.ownerPw == "" {
s.fail("protecting a file needs a user password or an owner password")
return
}
var perms reader.Permissions
for _, a := range allowed {
if s.tools.allow[a.name] {
perms |= a.bit
}
}
how := reader.Encryption{
UserPassword: s.tools.userPw,
OwnerPassword: s.tools.ownerPw,
Permissions: perms,
}
was := s.reopenPw
s.reopenPw = s.tools.userPw
if !s.changeSaying("protected with AES-256", func(d *ops.Doc) error {
d.Encrypt(how)
return nil
}) {
s.reopenPw = was
}
}

// decrypt writes the file without protection.
func (s *state) decrypt() {
was := s.reopenPw
s.reopenPw = ""
if !s.changeSaying("the protection will not be written", func(d *ops.Doc) error {
d.Decrypt()
return nil
}) {
s.reopenPw = was
}
}

// protection says how the file this document was read from was protected. It
// says nothing about how it will be written, which is what the two buttons
// above it decide.
func (s *state) protection() {
if s.doc == nil {
s.fail("open a document first")
return
}
p, ok := s.doc.Protection()
if !ok {
s.fail("the file this came from was not protected")
return
}
s.note = fmt.Sprintf("%s, revision %d, opened as %s, allowing %s",
p.Method, p.Revision, openedAs(p.Owner), p.Permissions)
s.refresh()
}

// openedAs says which password the file was opened with.
func openedAs(owner bool) string {
if owner {
return "the owner, so the permissions do not apply"
}
return "the user"
}
Loading
Loading