summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--aws.go16
-rw-r--r--cloudsettings.go13
-rw-r--r--cmd/bookpipeline/main.go35
-rw-r--r--cmd/booktopipeline/main.go19
-rw-r--r--cmd/getpipelinebook/main.go9
-rw-r--r--cmd/rescribe/TODO9
-rw-r--r--cmd/rescribe/embed_darwin.go12
-rw-r--r--cmd/rescribe/embed_darwin_amd64.go2
-rw-r--r--cmd/rescribe/embed_darwin_arm64.go2
-rw-r--r--cmd/rescribe/embed_linux.go5
-rw-r--r--cmd/rescribe/embed_other.go8
-rw-r--r--cmd/rescribe/embed_tessdata.go12
-rw-r--r--cmd/rescribe/embed_windows.go5
-rw-r--r--cmd/rescribe/gbook.go259
-rw-r--r--cmd/rescribe/gbook_test.go46
-rw-r--r--cmd/rescribe/getembeds.go77
-rw-r--r--cmd/rescribe/gui.go555
-rw-r--r--cmd/rescribe/gui_test.go77
-rw-r--r--cmd/rescribe/icon.256.pngbin0 -> 5945 bytes
-rw-r--r--cmd/rescribe/icon.pngbin0 -> 13997 bytes
-rw-r--r--cmd/rescribe/icon.svg60
-rw-r--r--cmd/rescribe/main.go343
-rw-r--r--cmd/rescribe/makefile80
-rw-r--r--cmd/rescribe/testdata/fuzz/FuzzGetBookIdFromUrl/174f82f558636f2a2
-rw-r--r--cmd/rescribe/testdata/fuzz/FuzzGetBookIdFromUrl/60892155cf2f79632
-rw-r--r--cmd/rescribe/xyz.rescribe.rescribe.appdata.xml43
-rw-r--r--cmd/rescribe/xyz.rescribe.rescribe.desktop9
-rw-r--r--cmd/rescribe/xyz.rescribe.rescribe.yml65
-rw-r--r--doc.go11
-rw-r--r--go.mod47
-rw-r--r--go.sum674
-rw-r--r--internal/pipeline/get.go34
-rw-r--r--internal/pipeline/pipeline.go251
-rw-r--r--internal/pipeline/put.go35
-rw-r--r--internal/pipeline/util.go16
-rw-r--r--internal/pipeline/util_windows.go16
-rw-r--r--local.go5
37 files changed, 2603 insertions, 251 deletions
diff --git a/aws.go b/aws.go
index 0ca657f..b954951 100644
--- a/aws.go
+++ b/aws.go
@@ -52,6 +52,7 @@ type AwsConn struct {
uploader *s3manager.Uploader
wipequrl string
prequrl string
+ prenwqurl string
ocrpgqurl string
analysequrl string
testqurl string
@@ -102,6 +103,15 @@ func (a *AwsConn) Init() error {
}
a.prequrl = *result.QueueUrl
+ a.Logger.Println("Getting preprocess no wipe queue URL")
+ result, err = a.sqssvc.GetQueueUrl(&sqs.GetQueueUrlInput{
+ QueueName: aws.String(queuePreNoWipe),
+ })
+ if err != nil {
+ return errors.New(fmt.Sprintf("Error getting preprocess no wipe queue URL: %s", err))
+ }
+ a.prenwqurl = *result.QueueUrl
+
a.Logger.Println("Getting wipeonly queue URL")
result, err = a.sqssvc.GetQueueUrl(&sqs.GetQueueUrlInput{
QueueName: aws.String(queueWipeOnly),
@@ -337,6 +347,10 @@ func (a *AwsConn) PreQueueId() string {
return a.prequrl
}
+func (a *AwsConn) PreNoWipeQueueId() string {
+ return a.prenwqurl
+}
+
func (a *AwsConn) WipeQueueId() string {
return a.wipequrl
}
@@ -616,7 +630,7 @@ func (a *AwsConn) Log(v ...interface{}) {
// TODO: also set up the necessary security group and iam stuff
func (a *AwsConn) MkPipeline() error {
buckets := []string{storageWip}
- queues := []string{queuePreProc, queueWipeOnly, queueAnalyse, queueOcrPage, queueTest}
+ queues := []string{queuePreProc, queuePreNoWipe, queueWipeOnly, queueAnalyse, queueOcrPage, queueTest}
for _, bucket := range buckets {
err := a.CreateBucket(bucket)
diff --git a/cloudsettings.go b/cloudsettings.go
index ed0b47d..5d1d41c 100644
--- a/cloudsettings.go
+++ b/cloudsettings.go
@@ -19,18 +19,19 @@ package bookpipeline
// TODO: create profile and security group with mkpipeline
const (
spotProfile = "arn:aws:iam::557852942063:instance-profile/pipeliner"
- spotImage = "ami-0ee06baa01314ca39"
+ spotImage = "ami-08b4804ab7d8616bc"
spotType = "m5.large"
spotSg = "sg-0be8a3ab89e7136b9"
)
// Queue names. Can be anything unique in SQS.
const (
- queuePreProc = "rescribepreprocess"
- queueWipeOnly = "rescribewipeonly"
- queueOcrPage = "rescribeocrpage"
- queueAnalyse = "rescribeanalyse"
- queueTest = "rescribetest1"
+ queuePreProc = "rescribepreprocess"
+ queuePreNoWipe = "rescribeprenowipe"
+ queueWipeOnly = "rescribewipeonly"
+ queueOcrPage = "rescribeocrpage"
+ queueAnalyse = "rescribeanalyse"
+ queueTest = "rescribetest1"
)
// Storage bucket names. Can be anything unique in S3.
diff --git a/cmd/bookpipeline/main.go b/cmd/bookpipeline/main.go
index 65c9b79..076df32 100644
--- a/cmd/bookpipeline/main.go
+++ b/cmd/bookpipeline/main.go
@@ -9,6 +9,7 @@ package main
import (
"bytes"
+ "context"
"flag"
"fmt"
"log"
@@ -68,6 +69,7 @@ type Clouder interface {
type Pipeliner interface {
Clouder
PreQueueId() string
+ PreNoWipeQueueId() string
WipeQueueId() string
OCRPageQueueId() string
AnalyseQueueId() string
@@ -91,7 +93,7 @@ func resetTimer(t *time.Timer, d time.Duration) {
func main() {
verbose := flag.Bool("v", false, "verbose")
- training := flag.String("t", "rescribealphav5", "default tesseract training file to use (without the .traineddata part)")
+ training := flag.String("t", "rescribev9", "default tesseract training file to use (without the .traineddata part)")
nopreproc := flag.Bool("np", false, "disable preprocessing")
nowipe := flag.Bool("nw", false, "disable wipeonly")
noocrpg := flag.Bool("nop", false, "disable ocr on individual pages")
@@ -118,6 +120,9 @@ func main() {
wipePattern := regexp.MustCompile(`[0-9]{4,6}(.bin)?.png$`)
ocredPattern := regexp.MustCompile(`.hocr$`)
+ var ctx context.Context
+ ctx = context.Background()
+
var conn Pipeliner
switch *conntype {
case "aws":
@@ -147,6 +152,7 @@ func main() {
hostname, err := os.Hostname()
var checkPreQueue <-chan time.Time
+ var checkPreNoWipeQueue <-chan time.Time
var checkWipeQueue <-chan time.Time
var checkOCRPageQueue <-chan time.Time
var checkAnalyseQueue <-chan time.Time
@@ -164,6 +170,7 @@ func main() {
if !*noanalyse {
checkAnalyseQueue = time.After(0)
}
+ checkPreNoWipeQueue = time.After(0)
var quietTime = time.Duration(*autostop) * time.Second
stopIfQuiet = time.NewTimer(quietTime)
if quietTime == 0 {
@@ -190,11 +197,29 @@ func main() {
}
conn.Log("Message received on preprocess queue, processing", msg.Body)
stopTimer(stopIfQuiet)
- err = pipeline.ProcessBook(msg, conn, pipeline.Preprocess([]float64{0.1, 0.2, 0.4, 0.5}), origPattern, conn.PreQueueId(), conn.OCRPageQueueId())
+ err = pipeline.ProcessBook(ctx, msg, conn, pipeline.Preprocess([]float64{0.1, 0.2, 0.4, 0.5}, false), origPattern, conn.PreQueueId(), conn.OCRPageQueueId())
resetTimer(stopIfQuiet, quietTime)
if err != nil {
conn.Log("Error during preprocess", err)
}
+ case <-checkPreNoWipeQueue:
+ msg, err := conn.CheckQueue(conn.PreNoWipeQueueId(), QueueTimeoutSecs)
+ checkPreNoWipeQueue = time.After(PauseBetweenChecks)
+ if err != nil {
+ conn.Log("Error checking preprocess (no wipe) queue", err)
+ continue
+ }
+ if msg.Handle == "" {
+ conn.Log("No message received on preprocess (no wipe) queue, sleeping")
+ continue
+ }
+ conn.Log("Message received on preprocess (no wipe) queue, processing", msg.Body)
+ stopTimer(stopIfQuiet)
+ err = pipeline.ProcessBook(ctx, msg, conn, pipeline.Preprocess([]float64{0.1, 0.2, 0.4, 0.5}, true), origPattern, conn.PreQueueId(), conn.OCRPageQueueId())
+ resetTimer(stopIfQuiet, quietTime)
+ if err != nil {
+ conn.Log("Error during preprocess (no wipe)", err)
+ }
case <-checkWipeQueue:
msg, err := conn.CheckQueue(conn.WipeQueueId(), QueueTimeoutSecs)
checkWipeQueue = time.After(PauseBetweenChecks)
@@ -208,7 +233,7 @@ func main() {
}
stopTimer(stopIfQuiet)
conn.Log("Message received on wipeonly queue, processing", msg.Body)
- err = pipeline.ProcessBook(msg, conn, pipeline.Wipe, wipePattern, conn.WipeQueueId(), conn.OCRPageQueueId())
+ err = pipeline.ProcessBook(ctx, msg, conn, pipeline.Wipe, wipePattern, conn.WipeQueueId(), conn.OCRPageQueueId())
resetTimer(stopIfQuiet, quietTime)
if err != nil {
conn.Log("Error during wipe", err)
@@ -228,7 +253,7 @@ func main() {
checkOCRPageQueue = time.After(0)
stopTimer(stopIfQuiet)
conn.Log("Message received on OCR Page queue, processing", msg.Body)
- err = pipeline.OcrPage(msg, conn, pipeline.Ocr(*training, ""), conn.OCRPageQueueId(), conn.AnalyseQueueId())
+ err = pipeline.OcrPage(ctx, msg, conn, pipeline.Ocr(*training, ""), conn.OCRPageQueueId(), conn.AnalyseQueueId())
resetTimer(stopIfQuiet, quietTime)
if err != nil {
conn.Log("Error during OCR Page process", err)
@@ -246,7 +271,7 @@ func main() {
}
stopTimer(stopIfQuiet)
conn.Log("Message received on analyse queue, processing", msg.Body)
- err = pipeline.ProcessBook(msg, conn, pipeline.Analyse(conn), ocredPattern, conn.AnalyseQueueId(), "")
+ err = pipeline.ProcessBook(ctx, msg, conn, pipeline.Analyse(conn, false), ocredPattern, conn.AnalyseQueueId(), "")
resetTimer(stopIfQuiet, quietTime)
if err != nil {
conn.Log("Error during analysis", err)
diff --git a/cmd/booktopipeline/main.go b/cmd/booktopipeline/main.go
index b4f4d99..ee2ef47 100644
--- a/cmd/booktopipeline/main.go
+++ b/cmd/booktopipeline/main.go
@@ -7,6 +7,7 @@
package main
import (
+ "context"
"flag"
"fmt"
"log"
@@ -18,7 +19,7 @@ import (
"rescribe.xyz/bookpipeline/internal/pipeline"
)
-const usage = `Usage: booktopipeline [-c conn] [-t training] [-prebinarised] [-notbinarised] [-v] bookdir [bookname]
+const usage = `Usage: booktopipeline [-c conn] [-t training] [-prebinarised] [-notbinarised] [-nowipe] [-v] bookdir [bookname]
Uploads the book in bookdir to the S3 'inprogress' bucket and adds it
to the 'preprocess' or 'wipeonly' SQS queue. The queue to send to is
@@ -45,6 +46,7 @@ func main() {
conntype := flag.String("c", "aws", "connection type ('aws' or 'local')")
wipeonly := flag.Bool("prebinarised", false, "Prebinarised: only preprocessing will be to wipe")
dobinarise := flag.Bool("notbinarised", false, "Not binarised: all preprocessing will be done including binarisation")
+ nowipe := flag.Bool("nowipe", false, "No wipe: Disable wiping as part of preprocessing")
training := flag.String("t", "", "Training to use (training filename without the .traineddata part)")
flag.Usage = func() {
@@ -65,6 +67,8 @@ func main() {
bookname = filepath.Base(bookdir)
}
+ var ctx context.Context
+
if *verbose {
verboselog = log.New(os.Stdout, "", log.LstdFlags)
} else {
@@ -86,7 +90,7 @@ func main() {
log.Fatalln("Failed to set up cloud connection:", err)
}
- qid := pipeline.DetectQueueType(bookdir, conn)
+ qid := pipeline.DetectQueueType(bookdir, conn, false)
// Flags set override the queue selection
if *wipeonly {
@@ -95,9 +99,12 @@ func main() {
if *dobinarise {
qid = conn.PreQueueId()
}
+ if *nowipe {
+ qid = conn.PreNoWipeQueueId()
+ }
verboselog.Println("Checking that all images are valid in", bookdir)
- err = pipeline.CheckImages(bookdir)
+ err = pipeline.CheckImages(ctx, bookdir)
if err != nil {
log.Fatalln(err)
}
@@ -112,7 +119,7 @@ func main() {
}
verboselog.Println("Uploading all images are valid in", bookdir)
- err = pipeline.UploadImages(bookdir, bookname, conn)
+ err = pipeline.UploadImages(ctx, bookdir, bookname, conn)
if err != nil {
log.Fatalln(err)
}
@@ -128,8 +135,10 @@ func main() {
var qname string
if qid == conn.PreQueueId() {
qname = "preprocess"
- } else {
+ } else if qid == conn.WipeQueueId() {
qname = "wipeonly"
+ } else {
+ qname = "nowipe"
}
fmt.Println("Uploaded book to queue", qname)
diff --git a/cmd/getpipelinebook/main.go b/cmd/getpipelinebook/main.go
index ccedd72..bc4150d 100644
--- a/cmd/getpipelinebook/main.go
+++ b/cmd/getpipelinebook/main.go
@@ -40,7 +40,7 @@ func main() {
binarisedpdf := flag.Bool("binarisedpdf", false, "Only download binarised PDF (can be used alongside -graph)")
colourpdf := flag.Bool("colourpdf", false, "Only download colour PDF (can be used alongside -graph)")
pdf := flag.Bool("pdf", false, "Only download PDFs (can be used alongside -graph)")
- png := flag.Bool("png", false, "Only download best binarised png files")
+ png := flag.Bool("png", false, "Should only download best binarised png files")
verbose := flag.Bool("v", false, "Verbose")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), usage)
@@ -125,12 +125,17 @@ func main() {
pipeline.DownloadPdfs(bookname, bookname, conn)
}
+ if *png {
+ verboselog.Println("Downloading best PNGs")
+ pipeline.DownloadBestPngs(bookname, bookname, conn)
+ }
+
if *binarisedpdf || *colourpdf || *graph || *pdf {
return
}
verboselog.Println("Downloading best pages")
- err = pipeline.DownloadBestPages(bookname, bookname, conn, *png)
+ err = pipeline.DownloadBestPages(bookname, bookname, conn)
if err != nil {
log.Fatalln(err)
}
diff --git a/cmd/rescribe/TODO b/cmd/rescribe/TODO
new file mode 100644
index 0000000..0b17c4b
--- /dev/null
+++ b/cmd/rescribe/TODO
@@ -0,0 +1,9 @@
+Add option to choose multiple languages (easy on tesseract level, slightly tricky on GUI level)
+
+Write to PDF as we go along, so memory requirements are reduced. Would require further modifying fpdf: https://github.com/jung-kurt/gofpdf/issues/110
+
+We had reported (with video) a bug with permissions requests repeatedly being displayed on a new Mac, may be related to this: https://github.com/fyne-io/fyne/issues/2799
+
+Maybe pay apple $100 for a certificate to avoid their security warnings (can be easily included in 'fyne package')
+
+Add more tests.
diff --git a/cmd/rescribe/embed_darwin.go b/cmd/rescribe/embed_darwin.go
new file mode 100644
index 0000000..4f43b87
--- /dev/null
+++ b/cmd/rescribe/embed_darwin.go
@@ -0,0 +1,12 @@
+// Copyright 2021 Nick White.
+// Use of this source code is governed by the GPLv3
+// license that can be found in the LICENSE file.
+
+//go:build embed
+
+package main
+
+import _ "embed"
+
+//go:embed getgbook-darwin-b14f62f.zip
+var gbookzip []byte
diff --git a/cmd/rescribe/embed_darwin_amd64.go b/cmd/rescribe/embed_darwin_amd64.go
index 719c9cc..1f7f8c2 100644
--- a/cmd/rescribe/embed_darwin_amd64.go
+++ b/cmd/rescribe/embed_darwin_amd64.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.
+//go:build embed
+
package main
import _ "embed"
diff --git a/cmd/rescribe/embed_darwin_arm64.go b/cmd/rescribe/embed_darwin_arm64.go
index a1ca9b8..4c154be 100644
--- a/cmd/rescribe/embed_darwin_arm64.go
+++ b/cmd/rescribe/embed_darwin_arm64.go
@@ -2,6 +2,8 @@
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.
+//go:build embed
+
package main
import _ "embed"
diff --git a/cmd/rescribe/embed_linux.go b/cmd/rescribe/embed_linux.go
index c720b6e..3cfd18b 100644
--- a/cmd/rescribe/embed_linux.go
+++ b/cmd/rescribe/embed_linux.go
@@ -2,9 +2,14 @@
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.
+//go:build embed
+
package main
import _ "embed"
//go:embed tesseract-linux-v5.0.0-alpha.20210510.zip
var tesszip []byte
+
+//go:embed getgbook-linux-cac42fb.zip
+var gbookzip []byte
diff --git a/cmd/rescribe/embed_other.go b/cmd/rescribe/embed_other.go
index fe51fd0..ac9ce3a 100644
--- a/cmd/rescribe/embed_other.go
+++ b/cmd/rescribe/embed_other.go
@@ -2,12 +2,12 @@
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.
-// +build !darwin
-// +build !linux
-// +build !windows
+//go:build (!darwin && !linux && !windows) || !embed
package main
// if not one of the above platforms, we won't embed anything, so
-// just create an empty byte slice
+// just create empty byte slices
var tesszip []byte
+var gbookzip []byte
+var tessdatazip []byte
diff --git a/cmd/rescribe/embed_tessdata.go b/cmd/rescribe/embed_tessdata.go
new file mode 100644
index 0000000..ea9ce8f
--- /dev/null
+++ b/cmd/rescribe/embed_tessdata.go
@@ -0,0 +1,12 @@
+// Copyright 2022 Nick White.
+// Use of this source code is governed by the GPLv3
+// license that can be found in the LICENSE file.
+
+//go:build embed
+
+package main
+
+import _ "embed"
+
+//go:embed tessdata.20220322.zip
+var tessdatazip []byte
diff --git a/cmd/rescribe/embed_windows.go b/cmd/rescribe/embed_windows.go
index c447624..f3fe193 100644
--- a/cmd/rescribe/embed_windows.go
+++ b/cmd/rescribe/embed_windows.go
@@ -2,9 +2,14 @@
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.
+//go:build embed
+
package main
import _ "embed"
//go:embed tesseract-w32-v5.0.0-alpha.20210506.zip
var tesszip []byte
+
+//go:embed getgbook-w32-c2824685.zip
+var gbookzip []byte
diff --git a/cmd/rescribe/gbook.go b/cmd/rescribe/gbook.go
new file mode 100644
index 0000000..a011181
--- /dev/null
+++ b/cmd/rescribe/gbook.go
@@ -0,0 +1,259 @@
+// Copyright 2022 Nick White.
+// Use of this source code is governed by the GPLv3
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "os"
+ "os/exec"
+ "path"
+ "regexp"
+ "strings"
+ "unicode"
+
+ "rescribe.xyz/bookpipeline/internal/pipeline"
+)
+
+const maxPartLength = 48
+
+// formatAuthors formats a list of authors by just selecting
+// the first one listed, and returning the uppercased final
+// name.
+func formatAuthors(authors []string) string {
+ if len(authors) == 0 {
+ return ""
+ }
+
+ s := authors[0]
+
+ parts := strings.Fields(s)
+ if len(parts) > 1 {
+ s = parts[len(parts)-1]
+ }
+
+ s = strings.ToUpper(s)
+
+ if len(s) > maxPartLength {
+ // truncate to maxPartLength
+ m := fmt.Sprintf("%%.%ds", maxPartLength)
+ s = fmt.Sprintf(m, s)
+ }
+
+ s = strings.Map(stripNonLetters, s)
+
+ return s
+}
+
+// mapTitle is a function for strings.Map to strip out
+// unwanted characters from the title.
+func stripNonLetters(r rune) rune {
+ if !unicode.IsLetter(r) {
+ return -1
+ }
+ return r
+}
+
+// formatTitle formats a title to our preferences, notably
+// by stripping spaces and punctuation characters.
+func formatTitle(title string) string {
+ s := strings.Map(stripNonLetters, title)
+ if len(s) > maxPartLength {
+ // truncate to maxPartLength
+ m := fmt.Sprintf("%%.%ds", maxPartLength)
+ s = fmt.Sprintf(m, s)
+ }
+ return s
+}
+
+// getMetadata queries Google Books for metadata we care about
+// and returns it formatted as we need it.
+func getMetadata(id string) (string, string, string, error) {
+ var author, title, year string
+ url := fmt.Sprintf("https://www.googleapis.com/books/v1/volumes/%s", id)
+
+ // designed to be unmarshalled by encoding/json's Unmarshal()
+ type bookInfo struct {
+ VolumeInfo struct {
+ Title string
+ Authors []string
+ PublishedDate string
+ }
+ }
+
+ resp, err := http.Get(url)
+ if err != nil {
+ return author, title, year, fmt.Errorf("Error downloading metadata %s: %v", url, err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode != http.StatusOK {
+ return author, title, year, fmt.Errorf("Error downloading metadata %s: %v", url, err)
+ }
+
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return author, title, year, fmt.Errorf("Error reading metadata %s: %v", url, err)
+ }
+
+ v := bookInfo{}
+ err = json.Unmarshal(b, &v)
+ if err != nil {
+ return author, title, year, fmt.Errorf("Error parsing metadata %s: %v", url, err)
+ }
+
+ author = formatAuthors(v.VolumeInfo.Authors)
+ title = formatTitle(v.VolumeInfo.Title)
+ year = v.VolumeInfo.PublishedDate
+
+ return author, title, year, nil
+}
+
+// moveFile just copies a file to the destination without
+// using os.Rename, as that can fail if crossing filesystem
+// boundaries
+func moveFile(from string, to string) error {
+ ffrom, err := os.Open(from)
+ if err != nil {
+ return err
+ }
+ defer ffrom.Close()
+
+ fto, err := os.Create(to)
+ if err != nil {
+ return err
+ }
+ defer fto.Close()
+
+ _, err = io.Copy(fto, ffrom)
+ if err != nil {
+ return err
+ }
+
+ ffrom.Close()
+ err = os.Remove(from)
+ if err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// getGoogleBook downloads all images of a book to a directory
+// named YEAR_AUTHORSURNAME_Title_bookid inside basedir, returning
+// the directory path
+func getGoogleBook(ctx context.Context, gbookcmd string, id string, basedir string) (string, error) {
+ author, title, year, err := getMetadata(id)
+ if err != nil {
+ return "", err
+ }
+
+ tmpdir, err := ioutil.TempDir("", "bookpipeline")
+ if err != nil {
+ return "", fmt.Errorf("Error setting up temporary directory: %v", err)
+ }
+
+ cmd := exec.CommandContext(ctx, gbookcmd, id)
+ pipeline.HideCmd(cmd)
+ cmd.Dir = tmpdir
+ cmd.Stdout = os.Stdout
+ cmd.Stderr = os.Stderr
+ err = cmd.Run()
+ if err != nil {
+ return "", fmt.Errorf("Error running getgbook %s: %v", id, err)
+ }
+
+ select {
+ case <-ctx.Done():
+ _ = os.Remove(tmpdir)
+ return "", ctx.Err()
+ default:
+ }
+
+ // getgbook downloads into id directory, so move files out of
+ // there directly into dir
+ tmpdir = path.Join(tmpdir, id)
+ f, err := os.Open(tmpdir)
+ if err != nil {
+ return "", fmt.Errorf("Failed to open %s to move files: %v", tmpdir, err)
+ }
+ files, err := f.Readdir(0)
+ if err != nil {
+ return "", fmt.Errorf("Failed to readdir %s to move files: %v", tmpdir, err)
+ }
+
+ d := fmt.Sprintf("%s_%s_%s_%s", year, author, title, id)
+ dir := path.Join(basedir, d)
+ err = os.MkdirAll(dir, 0755)
+ if err != nil {
+ return "", fmt.Errorf("Couldn't create directory %s: %v", dir, err)
+ }
+
+ for _, v := range files {
+ orig := path.Join(tmpdir, v.Name())
+ new := path.Join(dir, v.Name())
+ err = moveFile(orig, new)
+ if err != nil {
+ return dir, fmt.Errorf("Failed to move %s to %s: %v", orig, new, err)
+ }
+ }
+
+ err = os.Remove(tmpdir)
+ if err != nil {
+ return dir, fmt.Errorf("Failed to remove temporary directory %s: %v", tmpdir, err)
+ }
+
+ return dir, nil
+}
+
+// getBookIdFromUrl returns a 12 character Google Book ID from
+// a Google URL, or an error if one can't be found.
+// Example URLs:
+// https://books.google.it/books?id=QjQepCuN8JYC
+// https://www.google.it/books/edition/_/VJbr-Oe2au0C
+func getBookIdFromUrl(url string) (string, error) {
+ lurl := strings.ToLower(url)
+ if len(url) == 12 && !strings.ContainsAny(url, "?/:") {
+ return url, nil
+ }
+
+ matchUrl, err := regexp.MatchString("https://www.google.[^\\/]*/books/", url)
+ if err != nil {
+ return "", err
+ }
+
+ if strings.HasPrefix(lurl, "https://books.google") {
+ start := strings.Index(lurl, "?id=")
+ if start == -1 {
+ start = strings.Index(lurl, "&id=")
+ }
+
+ if start >= 0 {
+ start += 4
+ if len(url) - start < 12 {
+ return "", fmt.Errorf("Could not find book ID in URL")
+ }
+ return url[start : start+12], nil
+ }
+
+ return "", fmt.Errorf("Could not find book ID in URL")
+ }
+ if matchUrl == true {
+ start := strings.Index(lurl, "edition/_/")
+
+ if start >= 0 {
+ start += 10
+ if len(url) - start < 12 {
+ return "", fmt.Errorf("Could not find book ID in URL")
+ }
+ return url[start : start+12], nil
+ }
+ }
+ return "", fmt.Errorf("Could not find book ID in URL")
+}
diff --git a/cmd/rescribe/gbook_test.go b/cmd/rescribe/gbook_test.go
new file mode 100644
index 0000000..f7df595
--- /dev/null
+++ b/cmd/rescribe/gbook_test.go
@@ -0,0 +1,46 @@
+// Copyright 2022 Nick White.
+// Use of this source code is governed by the GPLv3
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "testing"
+)
+
+func TestGetBookIdFromUrl(t *testing.T) {
+ cases := []struct {
+ url string
+ id string
+ }{
+ {"https://books.google.it/books?id=QjQepCuN8JYC", "QjQepCuN8JYC"},
+ {"https://www.google.it/books/edition/_/VJbr-Oe2au0C", "VJbr-Oe2au0C"},
+ }
+
+ for _, c := range cases {
+ t.Run(c.url, func(t *testing.T) {
+ id, err := getBookIdFromUrl(c.url)
+ if err != nil {
+ t.Fatalf("Error running test: %v", err)
+ }
+ if id != c.id {
+ t.Fatalf("Expected %s, got %s", c.id, id)
+ }
+ })
+ }
+}
+
+func FuzzGetBookIdFromUrl(f *testing.F) {
+ cases := []string {
+ "https://books.google.it/books?id=QjQepCuN8JYC",
+ "https://www.google.it/books/edition/_/VJbr-Oe2au0C",
+ }
+
+ for _, c := range cases {
+ f.Add(c)
+ }
+
+ f.Fuzz(func(t *testing.T, url string) {
+ getBookIdFromUrl(url)
+ })
+}
diff --git a/cmd/rescribe/getembeds.go b/cmd/rescribe/getembeds.go
index 91cd480..57c7ce0 100644
--- a/cmd/rescribe/getembeds.go
+++ b/cmd/rescribe/getembeds.go
@@ -2,15 +2,19 @@
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.
-// +build ignore
+//go:build ignore
// this downloads the needed files to embed into the binary,
// and is run by `go generate`
package main
import (
+ "bytes"
+ "crypto/sha256"
+ "encoding/hex"
"fmt"
"io"
+ "io/ioutil"
"net/http"
"os"
"path"
@@ -30,6 +34,9 @@ func dl(url string) error {
return fmt.Errorf("Error getting url %s: %v", url, err)
}
defer r.Body.Close()
+ if r.StatusCode != 200 {
+ return fmt.Errorf("Error getting url %s: got code %v", url, r.StatusCode)
+ }
_, err = io.Copy(f, r.Body)
if err != nil {
@@ -39,19 +46,69 @@ func dl(url string) error {
return nil
}
+// present returns true if the file is present and matches the
+// checksum, false otherwise
+func present(url string, sum string) bool {
+ fn := path.Base(url)
+ _, err := os.Stat(fn)
+ if err != nil && !os.IsExist(err) {
+ return false
+ }
+
+ b, err := ioutil.ReadFile(fn)
+ if err != nil {
+ return false
+ }
+
+ expected, err := hex.DecodeString(sum)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error decoding checksum for %s: %v\n", url, err)
+ os.Exit(1)
+ }
+
+ actual := sha256.Sum256(b)
+
+ var a []byte
+ for _, v := range actual {
+ a = append(a, v)
+ }
+
+ if !bytes.Equal(a, expected) {
+ return false
+ }
+
+ return true
+}
+
func main() {
- urls := []string{
- "https://rescribe.xyz/rescribe/embeds/tessdata.20211001.zip",
- "https://rescribe.xyz/rescribe/embeds/tesseract-linux-v5.0.0-alpha.20210510.zip",
- "https://rescribe.xyz/rescribe/embeds/tesseract-osx-v4.1.1.20191227.zip",
- "https://rescribe.xyz/rescribe/embeds/tesseract-osx-m1-v4.1.1.20210802.zip",
- "https://rescribe.xyz/rescribe/embeds/tesseract-w32-v5.0.0-alpha.20210506.zip",
+ urls := []struct {
+ url string
+ sum string
+ }{
+ {"https://rescribe.xyz/rescribe/embeds/tessdata.20220322.zip", "725fd570a3c3dc0eba9463248ce47a8646db8bafb198d428d6bb8f0be18540ee"},
+ {"https://rescribe.xyz/rescribe/embeds/tesseract-linux-v5.0.0-alpha.20210510.zip", "81cfba632b8aaf0a00180b1aa62d357d50f343b0e9bd51b941ee14c289ccd889"},
+ {"https://rescribe.xyz/rescribe/embeds/tesseract-osx-v4.1.1.20191227.zip", "5f567b95f1dea9d0581ad42ada4d1f1160a38ea22ae338f9efe190015265636b"},
+ {"https://rescribe.xyz/rescribe/embeds/tesseract-osx-m1-v4.1.1.20210802.zip", "c9a454633f7e5175e2d50dd939d30a6e5bdfb3b8c78590a08b5aa21edbf32ca4"},
+ {"https://rescribe.xyz/rescribe/embeds/tesseract-w32-v5.0.0-alpha.20210506.zip", "96734f3db4bb7c3b9a241ab6d89ab3e8436cea43b1cbbcfb13999497982f63e3"},
+ {"https://rescribe.xyz/rescribe/embeds/getgbook-darwin-b14f62f.zip", "d21bc4d51c5f43af68d77ef257061a0635cce0610b769d23a340b3be528a92d8"},
+ {"https://rescribe.xyz/rescribe/embeds/getgbook-linux-cac42fb.zip", "c3b40a1c13da613d383f990bda5dd72425a7f26b89102d272a3388eb3d05ddb6"},
+ {"https://rescribe.xyz/rescribe/embeds/getgbook-w32-c2824685.zip", "1c258a77a47d6515718fbbd7e54d5c2b516291682a878d122add55901c9f2914"},
}
for _, v := range urls {
- fmt.Printf("Downloading %s\n", v)
- err := dl(v)
+ if present(v.url, v.sum) {
+ fmt.Printf("Skipping downloading of already present %s\n", path.Base(v.url))
+ continue
+ }
+
+ fmt.Printf("Downloading %s\n", v.url)
+ err := dl(v.url)
if err != nil {
- fmt.Printf("Error downloading %s: %v\n", v, err)
+ fmt.Fprintf(os.Stderr, "Error downloading %s: %v\n", v.url, err)
+ os.Exit(1)
+ }
+
+ if !present(v.url, v.sum) {
+ fmt.Fprintf(os.Stderr, "Error: downloaded %s does not match expected checksum\n", v.url)
os.Exit(1)
}
}
diff --git a/cmd/rescribe/gui.go b/cmd/rescribe/gui.go
index 654d875..73f1db2 100644
--- a/cmd/rescribe/gui.go
+++ b/cmd/rescribe/gui.go
@@ -1,4 +1,4 @@
-// Copyright 2021 Nick White.
+// Copyright 2021-2022 Nick White.
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.
@@ -6,6 +6,8 @@ package main
import (
"bufio"
+ "context"
+ "errors"
"fmt"
"io"
"log"
@@ -18,19 +20,35 @@ import (
"fyne.io/fyne/v2/container"
"fyne.io/fyne/v2/dialog"
"fyne.io/fyne/v2/layout"
+ "fyne.io/fyne/v2/storage"
"fyne.io/fyne/v2/theme"
"fyne.io/fyne/v2/widget"
)
+var progressPoints = map[float64]string{
+ 0.11: "Downloading",
+ 0.12: "Processing PDF",
+ 0.2: "Preprocessing",
+ 0.5: "OCRing",
+ 0.9: "Analysing",
+ 1.0: "Done",
+}
+
+var trainingNames = map[string]string{
+ "eng": "English (modern print)",
+ "lat": "Latin (modern print)",
+ "rescribev9_fast": "Latin/English/French (printed ca 1500-1800)",
+}
+
// copyStdoutToChan creates a pipe to copy anything written
-// to stdout instead to a rune channel
+// to the file also to a rune channel.
func copyStdoutToChan() (chan rune, error) {
c := make(chan rune)
- origStdout := os.Stdout
+ origFile := os.Stdout
r, w, err := os.Pipe()
if err != nil {
- return c, fmt.Errorf("Error creating pipe for stdout redirection: %v", err)
+ return c, fmt.Errorf("Error creating pipe for file redirection: %v", err)
}
os.Stdout = w
@@ -40,7 +58,7 @@ func copyStdoutToChan() (chan rune, error) {
defer func() {
close(c)
w.Close()
- os.Stdout = origStdout
+ os.Stdout = origFile
}()
for {
r, _, err := bufReader.ReadRune()
@@ -57,81 +75,512 @@ func copyStdoutToChan() (chan rune, error) {
return c, nil
}
-// startGui starts the gui process
-func startGui(log log.Logger, cmd string, training string, systess bool, tessdir string) error {
- myApp := app.New()
- myWindow := myApp.NewWindow("Rescribe OCR")
+// copyStderrToChan creates a pipe to copy anything written
+// to the file also to a rune channel.
+// TODO: would be nice to merge this with copyStdoutToChan,
+// but a naive version using *os.File didn't work.
+func copyStderrToChan() (chan rune, error) {
+ c := make(chan rune)
- var gobtn *widget.Button
+ origFile := os.Stderr
+ r, w, err := os.Pipe()
+ if err != nil {
+ return c, fmt.Errorf("Error creating pipe for file redirection: %v", err)
+ }
+ os.Stderr = w
- dir := widget.NewEntry()
- dir.SetPlaceHolder("Folder to process")
- dir.OnChanged = func(s string) {
- // TODO: also check if string is a directory, and only enable if so
- if dir.Text != "" {
- gobtn.Enable()
- } else {
- gobtn.Disable()
+ bufReader := bufio.NewReader(r)
+
+ go func() {
+ defer func() {
+ close(c)
+ w.Close()
+ os.Stderr = origFile
+ }()
+ for {
+ r, _, err := bufReader.ReadRune()
+ if err != nil && err != io.EOF {
+ return
+ }
+ c <- r
+ if err == io.EOF {
+ return
+ }
+ }
+ }()
+
+ return c, nil
+}
+
+// trainingSelectOnChange is a closure to handle change of the training
+// select box. It does nothing in most cases, but if "Other..." has been
+// selected, then it pops up a file chooser and adds the result to the
+// list, also copying the file to the TESSDATA_PREFIX, and selects it.
+func trainingSelectOnChange(sel *widget.Select, parent fyne.Window) func(string) {
+ return func(str string) {
+ if sel == nil {
+ return
+ }
+ if str != "Other..." {
+ return
}
+ d := dialog.NewFileOpen(func(uri fyne.URIReadCloser, err error) {
+ if err != nil || uri == nil {
+ sel.SetSelectedIndex(0)
+ return
+ }
+ defer uri.Close()
+ name := uri.URI().Name()
+ newpath := filepath.Join(os.Getenv("TESSDATA_PREFIX"), name)
+ f, err := os.Create(newpath)
+ if err != nil {
+ msg := fmt.Sprintf("Error creating temporary file to store custom training: %v\n", err)
+ dialog.ShowError(errors.New(msg), parent)
+ fmt.Fprintf(os.Stderr, msg)
+ sel.SetSelectedIndex(0)
+ return
+ }
+ defer f.Close()
+ _, err = io.Copy(f, uri)
+ if err != nil {
+ msg := fmt.Sprintf("Error copying custom training to temporary file: %v\n", err)
+ dialog.ShowError(errors.New(msg), parent)
+ fmt.Fprintf(os.Stderr, msg)
+ sel.SetSelectedIndex(0)
+ return
+ }
+
+ basicname := strings.TrimSuffix(name, ".traineddata")
+ opts := append([]string{basicname}, sel.Options...)
+ sel.Options = opts
+ sel.SetSelectedIndex(0)
+ }, parent)
+ d.SetFilter(storage.NewExtensionFileFilter([]string{".traineddata"}))
+ d.Resize(fyne.NewSize(740, 600))
+ d.Show()
}
+}
- openbtn := widget.NewButtonWithIcon("Choose folder", theme.FolderOpenIcon(), func() {
- dialog.ShowFolderOpen(func(uri fyne.ListableURI, err error) {
- if err == nil && uri != nil {
- dir.SetText(uri.Path())
+// mkTrainingSelect returns a select widget with all training
+// files in TESSDATA_PREFIX/training, any other trainings listed
+// in the extras slice, selecting the first entry.
+func mkTrainingSelect(extras []string, parent fyne.Window) *widget.Select {
+ prefix := os.Getenv("TESSDATA_PREFIX")
+ fn, err := filepath.Glob(prefix + "/*.traineddata")
+ if err != nil {
+ fn = []string{}
+ }
+ var opts []string
+ for _, v := range append(extras, fn...) {
+ t := strings.TrimSuffix(strings.TrimPrefix(v, prefix), ".traineddata")
+ if t == "osd" {
+ continue
+ }
+ for code, name := range trainingNames {
+ if t == code {
+ t = fmt.Sprintf("%s [%s]", name, code)
+ break
}
- }, myWindow)
- })
+ }
+ alreadythere := 0
+ for _, opt := range opts {
+ if t == opt {
+ alreadythere = 1
+ break
+ }
+ }
+ if alreadythere == 0 {
+ opts = append(opts, t)
+ }
+ }
- progressBar := widget.NewProgressBar()
+ opts = append(opts, "Other...")
+ s := widget.NewSelect(opts, func(string) {})
+ // OnChanged is set outside of NewSelect so the reference to s isn't nil
+ s.OnChanged = trainingSelectOnChange(s, parent)
+ s.SetSelectedIndex(0)
+ return s
+}
- logarea := widget.NewMultiLineEntry()
- logarea.Disable()
+// formatProgressBar uses the progressPoints map to set the text for the progress bar
+// appropriately
+func formatProgressBar(bar *widget.ProgressBar) func() string {
+ return func() string {
+ for i, v := range progressPoints {
+ if bar.Value == i {
+ return v
+ }
+ }
+ // OCRing gets special treatment as the bar can be updated within the range
+ if bar.Value >= 0.5 && bar.Value < 0.9 {
+ return progressPoints[0.5]
+ }
+ if bar.Value == 0 {
+ return ""
+ }
+ return "Processing"
+ }
+}
- // TODO: have the button be pressed if enter is pressed
- gobtn = widget.NewButtonWithIcon("Process OCR", theme.UploadIcon(), func() {
- if dir.Text == "" {
- return
+// updateProgress parses the last line of a log and updates a progress
+// bar appropriately.
+func updateProgress(log string, progressBar *widget.ProgressBar) {
+ lines := strings.Split(log, "\n")
+ lastline := lines[len(lines)-1]
+ for i, v := range progressPoints {
+ if strings.HasPrefix(lastline, " "+v) {
+ // OCRing has a number of dots after it showing how many pages have been processed,
+ // which we can use to update progress bar more often
+ // TODO: calculate number of pages we expect, so this can be set accurately
+ if v == "OCRing" {
+ if progressBar.Value < 0.5 {
+ progressBar.SetValue(0.5)
+ }
+ numdots := strings.Count(lastline, ".")
+ newval := float64(0.5) + (float64(numdots) * float64(0.01))
+ if newval >= 0.9 {
+ newval = 0.89
+ }
+ progressBar.SetValue(newval)
+ break
+ }
+ progressBar.SetValue(i)
+ }
+ }
+}
+
+// start sets up the gui to start the core process, and if all is well
+// it starts it
+func start(ctx context.Context, log *log.Logger, cmd string, tessdir string, gbookcmd string, dir string, training string, win fyne.Window, logarea *widget.Entry, progressBar *widget.ProgressBar, abortbtn *widget.Button, wipe bool, bigpdf bool, disableWidgets []fyne.Disableable) {
+ if dir == "" {
+ return
+ }
+
+ stdout, err := copyStdoutToChan()
+ if err != nil {
+ msg := fmt.Sprintf("Internal error\n\nError copying stdout to chan: %v\n", err)
+ dialog.ShowError(errors.New(msg), win)
+ fmt.Fprintf(os.Stderr, msg)
+ return
+ }
+ go func() {
+ for r := range stdout {
+ logarea.SetText(logarea.Text + string(r))
+ logarea.CursorRow = strings.Count(logarea.Text, "\n")
+ updateProgress(logarea.Text, progressBar)
+ }
+ }()
+
+ stderr, err := copyStderrToChan()
+ if err != nil {
+ msg := fmt.Sprintf("Internal error\n\nError copying stdout to chan: %v\n", err)
+ dialog.ShowError(errors.New(msg), win)
+ fmt.Fprintf(os.Stderr, msg)
+ return
+ }
+ go func() {
+ for r := range stderr {
+ logarea.SetText(logarea.Text + string(r))
+ logarea.CursorRow = strings.Count(logarea.Text, "\n")
}
+ }()
- gobtn.Disable()
- gobtn.SetText("Processing...")
+ // Do this in a goroutine so the GUI remains responsive
+ go func() {
+ letsGo(ctx, log, cmd, tessdir, gbookcmd, dir, training, win, logarea, progressBar, abortbtn, wipe, bigpdf, disableWidgets)
+ }()
+}
- progressBar.SetValue(0.5)
+// letsGo starts the core process
+func letsGo(ctx context.Context, log *log.Logger, cmd string, tessdir string, gbookcmd string, dir string, training string, win fyne.Window, logarea *widget.Entry, progressBar *widget.ProgressBar, abortbtn *widget.Button, wipe bool, bigpdf bool, disableWidgets []fyne.Disableable) {
+ bookdir := dir
+ savedir := dir
+ bookname := strings.ReplaceAll(filepath.Base(dir), " ", "_")
- stdout, err := copyStdoutToChan()
- if err != nil {
- fmt.Fprintf(os.Stderr, "Error copying stdout to chan: %v\n", err)
+ f, err := os.Stat(bookdir)
+ if err != nil && !strings.HasPrefix(bookdir, "Google Book: ") {
+ msg := fmt.Sprintf("Error opening %s: %v", bookdir, err)
+ dialog.ShowError(errors.New(msg), win)
+ fmt.Fprintf(os.Stderr, msg)
+
+ progressBar.SetValue(0.0)
+ for _, v := range disableWidgets {
+ v.Enable()
+ }
+ abortbtn.Disable()
+ return
+ }
+
+ for _, v := range disableWidgets {
+ v.Disable()
+ }
+
+ abortbtn.Enable()
+
+ progressBar.SetValue(0.1)
+
+ if strings.HasPrefix(dir, "Google Book: ") {
+ if gbookcmd == "" {
+ msg := fmt.Sprintf("No getgbook found, can't download Google Book. Either set -gbookcmd on the command line, or use the official build which includes an embedded copy of getgbook.\n")
+ dialog.ShowError(errors.New(msg), win)
+ fmt.Fprintf(os.Stderr, msg)
+ progressBar.SetValue(0.0)
+ for _, v := range disableWidgets {
+ v.Enable()
+ }
+ abortbtn.Disable()
return
}
+ progressBar.SetValue(0.11)
+ start := len("Google Book: ")
+ bookname = dir[start : start+12]
- // update log area with output from outC in a concurrent goroutine
- go func() {
- for r := range stdout {
- logarea.SetText(logarea.Text + string(r))
- logarea.CursorRow = strings.Count(logarea.Text, "\n")
- // TODO: set text on progress bar, or a label below it, to latest line printed, rather than just using a whole multiline entry like this
- // TODO: parse the stdout and set progressBar based on that
+ start = start + 12 + len(" Save to: ")
+ bookdir = dir[start:]
+ savedir = bookdir
+
+ fmt.Printf("Downloading Google Book\n")
+ d, err := getGoogleBook(ctx, gbookcmd, bookname, bookdir)
+ if err != nil {
+ if !strings.HasSuffix(err.Error(), "signal: killed") {
+ msg := fmt.Sprintf("Error downloading Google Book %s\n", bookname)
+ dialog.ShowError(errors.New(msg), win)
+ fmt.Fprintf(os.Stderr, msg)
}
- }()
+ progressBar.SetValue(0.0)
+ for _, v := range disableWidgets {
+ v.Enable()
+ }
+ abortbtn.Disable()
+ return
+ }
+ bookdir = d
+ savedir = d
+ bookname = filepath.Base(d)
+ }
- err = startProcess(log, cmd, dir.Text, filepath.Base(dir.Text), training, systess, dir.Text, tessdir)
+ if strings.HasSuffix(dir, ".pdf") && !f.IsDir() {
+ progressBar.SetValue(0.12)
+ bookdir, err = extractPdfImgs(ctx, bookdir)
if err != nil {
- fmt.Fprintf(os.Stderr, "Error executing process: %v\n", err)
+ if !strings.HasSuffix(err.Error(), "context canceled") {
+ msg := fmt.Sprintf("Error opening PDF %s: %v\n", bookdir, err)
+ dialog.ShowError(errors.New(msg), win)
+ fmt.Fprintf(os.Stderr, msg)
+ }
+
+ progressBar.SetValue(0.0)
+ for _, v := range disableWidgets {
+ v.Enable()
+ }
+ abortbtn.Disable()
return
}
- progressBar.SetValue(1.0)
- gobtn.SetText("Process OCR")
- gobtn.Enable()
+ // happens if extractPdfImgs recovers from a PDF panic,
+ // which will occur if we encounter an image we can't decode
+ if bookdir == "" {
+ msg := fmt.Sprintf("Error opening PDF\nThe format of this PDF is not supported, extract the images to .jpg manually into a\nfolder first, using a tool like the PDF image extractor at https://pdfcandy.com/extract-images.html.\n")
+ dialog.ShowError(errors.New(msg), win)
+ fmt.Fprintf(os.Stderr, msg)
+
+ progressBar.SetValue(0.0)
+ for _, v := range disableWidgets {
+ v.Enable()
+ }
+ abortbtn.Disable()
+ return
+ }
+
+ savedir = strings.TrimSuffix(savedir, ".pdf")
+ bookname = strings.TrimSuffix(bookname, ".pdf")
+ }
+
+ if strings.Contains(training, "[") {
+ start := strings.Index(training, "[") + 1
+ end := strings.Index(training, "]")
+ training = training[start:end]
+ }
+
+ err = startProcess(ctx, log, cmd, bookdir, bookname, training, savedir, tessdir, wipe, bigpdf)
+ if err != nil && strings.HasSuffix(err.Error(), "context canceled") {
+ progressBar.SetValue(0.0)
+ return
+ }
+ if err != nil {
+ msg := fmt.Sprintf("Error during processing: %v\n", err)
+ if strings.HasSuffix(err.Error(), "No images found") && strings.HasSuffix(dir, ".pdf") && !f.IsDir() {
+ msg = fmt.Sprintf("Error opening PDF\nNo images found in the PDF. Most likely the format of this PDF is not supported,\nextract the images to .jpg manually into a folder first, using a tool like\nthe PDF image extractor at https://pdfcandy.com/extract-images.html.\n")
+ }
+ dialog.ShowError(errors.New(msg), win)
+ fmt.Fprintf(os.Stderr, msg)
+
+ progressBar.SetValue(0.0)
+ for _, v := range disableWidgets {
+ v.Enable()
+ }
+ abortbtn.Disable()
+ return
+ }
+
+ progressBar.SetValue(1.0)
+
+ for _, v := range disableWidgets {
+ v.Enable()
+ }
+ abortbtn.Disable()
+
+ msg := fmt.Sprintf("OCR process finished successfully.\n\nYour completed files have been saved in:\n%s", savedir)
+ dialog.ShowInformation("OCR Complete", msg, win)
+}
+
+// startGui starts the gui process
+func startGui(log *log.Logger, cmd string, gbookcmd string, training string, tessdir string) error {
+ myApp := app.New()
+ myWindow := myApp.NewWindow("Rescribe OCR")
+
+ myWindow.Resize(fyne.NewSize(800, 400))
+
+ var abortbtn, gobtn *widget.Button
+ var chosen *fyne.Container
+
+ dir := widget.NewLabel("")
+
+ dirIcon := widget.NewIcon(theme.FolderIcon())
+
+ folderBtn := widget.NewButtonWithIcon("Choose folder", theme.FolderOpenIcon(), func() {
+ d := dialog.NewFolderOpen(func(uri fyne.ListableURI, err error) {
+ if err != nil || uri == nil {
+ return
+ }
+ dir.SetText(uri.Path())
+ dirIcon.SetResource(theme.FolderIcon())
+ chosen.Show()
+ gobtn.Enable()
+ }, myWindow)
+ d.Resize(fyne.NewSize(740, 600))
+ d.Show()
+ })
+
+ pdfBtn := widget.NewButtonWithIcon("Choose PDF", theme.DocumentIcon(), func() {
+ d := dialog.NewFileOpen(func(uri fyne.URIReadCloser, err error) {
+ if err != nil || uri == nil {
+ return
+ }
+ uri.Close()
+ dir.SetText(uri.URI().Path())
+ dirIcon.SetResource(theme.DocumentIcon())
+ chosen.Show()
+ gobtn.Enable()
+ }, myWindow)
+ d.SetFilter(storage.NewExtensionFileFilter([]string{".pdf"}))
+ d.Resize(fyne.NewSize(740, 600))
+ d.Show()
+ })
+
+ gbookBtn := widget.NewButtonWithIcon("Get Google Book", theme.SearchIcon(), func() {
+ dirEntry := widget.NewEntry()
+ bookId := widget.NewEntry()
+ homeDir, err := os.UserHomeDir()
+ if err == nil {
+ dirEntry.SetText(homeDir)
+ }
+ dirEntry.Validator = func(s string) error {
+ if s == "" {
+ return fmt.Errorf("No save folder set")
+ }
+ return nil
+ }
+ dirBtn := widget.NewButtonWithIcon("Browse", theme.FolderIcon(), func() {
+ d := dialog.NewFolderOpen(func(uri fyne.ListableURI, err error) {
+ if err != nil || uri == nil {
+ return
+ }
+ dirEntry.SetText(uri.Path())
+ }, myWindow)
+ d.Resize(fyne.NewSize(740, 600))
+ d.Show()
+ })
+ bookId.Validator = func(s string) error {
+ _, err := getBookIdFromUrl(s)
+ return err
+ }
+ f1 := widget.NewFormItem("Book ID / URL", bookId)
+ saveDir := container.New(layout.NewBorderLayout(nil, nil, nil, dirBtn), dirEntry, dirBtn)
+ f2 := widget.NewFormItem("Save in folder", saveDir)
+ d := dialog.NewForm("Enter Google Book ID or URL", "OK", "Cancel", []*widget.FormItem{f1, f2}, func(b bool) {
+ if b != true {
+ return
+ }
+ id, err := getBookIdFromUrl(bookId.Text)
+ if err != nil {
+ return
+ }
+ if dirEntry.Text == "" {
+ dirEntry.SetText(homeDir)
+ }
+ dir.SetText(fmt.Sprintf("Google Book: %s Save to: %s", id, dirEntry.Text))
+ dirIcon.SetResource(theme.SearchIcon())
+ chosen.Show()
+ gobtn.Enable()
+ }, myWindow)
+ d.Resize(fyne.NewSize(600, 200))
+ d.Show()
+ })
+
+ wipe := widget.NewCheck("Automatically clean image sides", func(bool) {})
+
+ bigpdf := widget.NewCheck("Use highest image quality for searchable PDF (requires lots of RAM)", func(bool) {})
+ bigpdf.Checked = false
+
+ trainingLabel := widget.NewLabel("Language / Script")
+
+ trainingOpts := mkTrainingSelect([]string{training}, myWindow)
+
+ progressBar := widget.NewProgressBar()
+ progressBar.TextFormatter = formatProgressBar(progressBar)
+
+ logarea := widget.NewMultiLineEntry()
+
+ detail := widget.NewAccordion(widget.NewAccordionItem("Log", logarea))
+
+ var ctx context.Context
+ var cancel context.CancelFunc
+ ctx, cancel = context.WithCancel(context.Background())
+
+ gobtn = widget.NewButtonWithIcon("Start OCR", theme.UploadIcon(), func() {})
+
+ disableWidgets := []fyne.Disableable{folderBtn, pdfBtn, gbookBtn, wipe, bigpdf, trainingOpts, gobtn}
+
+ abortbtn = widget.NewButtonWithIcon("Abort", theme.CancelIcon(), func() {
+ fmt.Printf("\nAbort\n")
+ cancel()
+ progressBar.SetValue(0.0)
+ for _, v := range disableWidgets {
+ v.Enable()
+ }
+ abortbtn.Disable()
+ ctx, cancel = context.WithCancel(context.Background())
})
+ abortbtn.Disable()
+
+ gobtn.OnTapped = func() {
+ start(ctx, log, cmd, tessdir, gbookcmd, dir.Text, trainingOpts.Selected, myWindow, logarea, progressBar, abortbtn, !wipe.Checked, bigpdf.Checked, disableWidgets)
+ }
+
gobtn.Disable()
- diropener := container.New(layout.NewGridLayout(2), dir, openbtn)
+ choices := container.New(layout.NewGridLayout(3), folderBtn, pdfBtn, gbookBtn)
+
+ chosen = container.New(layout.NewBorderLayout(nil, nil, dirIcon, nil), dirIcon, dir)
+ chosen.Hide()
+
+ trainingBits := container.New(layout.NewBorderLayout(nil, nil, trainingLabel, nil), trainingLabel, trainingOpts)
- content := container.NewVBox(diropener, gobtn, progressBar, logarea)
+ startBox := container.NewVBox(choices, chosen, trainingBits, wipe, bigpdf, gobtn, abortbtn, progressBar)
+ startContent := container.NewBorder(startBox, nil, nil, nil, detail)
- myWindow.SetContent(content)
+ myWindow.SetContent(startContent)
myWindow.Show()
myApp.Run()
diff --git a/cmd/rescribe/gui_test.go b/cmd/rescribe/gui_test.go
new file mode 100644
index 0000000..99a924f
--- /dev/null
+++ b/cmd/rescribe/gui_test.go
@@ -0,0 +1,77 @@
+// Copyright 2022 Nick White.
+// Use of this source code is governed by the GPLv3
+// license that can be found in the LICENSE file.
+
+package main
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+
+ "fyne.io/fyne/v2/app"
+ "fyne.io/fyne/v2/widget"
+)
+
+func TestFormatProgressBar(t *testing.T) {
+ cases := []struct {
+ val float64
+ str string
+ }{
+ {0.0, ""},
+ {0.01, "Processing"},
+ {0.11, "Downloading"},
+ {0.12, "Processing PDF"},
+ {0.2, "Preprocessing"},
+ {0.5, "OCRing"},
+ {0.55, "OCRing"},
+ {0.89, "OCRing"},
+ {0.9, "Analysing"},
+ {1.0, "Done"},
+ {1.1, "Processing"},
+ }
+
+ _ = app.New() // shouldn't be needed for test but we get a panic without it
+ bar := widget.NewProgressBar()
+
+ for _, c := range cases {
+ t.Run(fmt.Sprintf("%s_%.1f", c.str, c.val), func(t *testing.T) {
+ bar.Value = c.val
+ got := formatProgressBar(bar)()
+ if got != c.str {
+ t.Fatalf("Expected %s, got %s", c.str, got)
+ }
+ })
+ }
+}
+
+func TestUpdateProgress(t *testing.T) {
+ cases := []struct {
+ log string
+ val float64
+ }{
+ {"Downloading", 0.11},
+ {"Preprocessing", 0.2},
+ {"Preprocessing\nOCRing", 0.5},
+ {"Preprocessing\nOCRing...", 0.53},
+ {"OCRing........................................", 0.89},
+ {"OCRing..\nAnalysing", 0.9},
+ {"Done", 1.0},
+ {"Weirdness", 0.0},
+ }
+
+ _ = app.New() // shouldn't be needed for test but we get a panic without it
+ bar := widget.NewProgressBar()
+
+ for _, c := range cases {
+ t.Run(c.log, func(t *testing.T) {
+ l := strings.ReplaceAll(" "+c.log, "\n", "\n ")
+ bar.Value = 0.0
+ updateProgress(l, bar)
+ got := bar.Value
+ if got != c.val {
+ t.Fatalf("Expected %f, got %f", c.val, got)
+ }
+ })
+ }
+}
diff --git a/cmd/rescribe/icon.256.png b/cmd/rescribe/icon.256.png
new file mode 100644
index 0000000..79e922e
--- /dev/null
+++ b/cmd/rescribe/icon.256.png
Binary files differ
diff --git a/cmd/rescribe/icon.png b/cmd/rescribe/icon.png
new file mode 100644
index 0000000..dcfb0f5
--- /dev/null
+++ b/cmd/rescribe/icon.png
Binary files differ
diff --git a/cmd/rescribe/icon.svg b/cmd/rescribe/icon.svg
new file mode 100644
index 0000000..d60a36c
--- /dev/null
+++ b/cmd/rescribe/icon.svg
@@ -0,0 +1,60 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+ xmlns:dc="http://purl.org/dc/elements/1.1/"
+ xmlns:cc="http://creativecommons.org/ns#"
+ xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:svg="http://www.w3.org/2000/svg"
+ xmlns="http://www.w3.org/2000/svg"
+ xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+ xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+ version="1.1"
+ id="svg2"
+ width="204.8"
+ height="204.8"
+ viewBox="0 0 204.8 204.8"
+ sodipodi:docname="icon-trans.svg"
+ inkscape:version="1.0.2 (e86c870879, 2021-01-15)">
+ <metadata
+ id="metadata8">
+ <rdf:RDF>
+ <cc:Work
+ rdf:about="">
+ <dc:format>image/svg+xml</dc:format>
+ <dc:type
+ rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+ <dc:title></dc:title>
+ </cc:Work>
+ </rdf:RDF>
+ </metadata>
+ <defs
+ id="defs6" />
+ <sodipodi:namedview
+ pagecolor="#ffffff"
+ bordercolor="#666666"
+ borderopacity="1"
+ objecttolerance="10"
+ gridtolerance="10"
+ guidetolerance="10"
+ inkscape:pageopacity="0"
+ inkscape:pageshadow="2"
+ inkscape:window-width="1051"
+ inkscape:window-height="1058"
+ id="namedview4"
+ showgrid="false"
+ inkscape:zoom="1.52608"
+ inkscape:cx="103.24875"
+ inkscape:cy="65.817935"
+ inkscape:window-x="0"
+ inkscape:window-y="17"
+ inkscape:window-maximized="0"
+ inkscape:current-layer="g10" />
+ <g
+ inkscape:groupmode="layer"
+ inkscape:label="Image"
+ id="g10">
+ <path
+ style="fill:#000000;stroke-width:0.16"
+ d="m 165.46012,194.36098 c 0.506,-0.0317 1.334,-0.0317 1.84,0 0.506,0.0318 0.092,0.0577 -0.92,0.0577 -1.012,0 -1.426,-0.026 -0.92,-0.0577 z m 11.92286,0.002 c 0.28759,-0.0356 0.71959,-0.0349 0.96,0.002 0.24044,0.0365 0.005,0.0656 -0.52286,0.0647 -0.528,-0.002 -0.7247,-0.0308 -0.43714,-0.0664 z m -13.08286,-0.76181 c -0.132,-0.0342 -0.66331,-0.10904 -1.1807,-0.16621 -0.71693,-0.0792 -1.02946,-0.19971 -1.3138,-0.50651 -0.24372,-0.26297 -0.76884,-0.50646 -1.51445,-0.70221 -0.62775,-0.16481 -1.37025,-0.44112 -1.65001,-0.61401 -0.79923,-0.49395 -2.30109,-0.65095 -6.18104,-0.6461 -3.91542,0.005 -6.65774,0.18683 -6.93072,0.45981 -0.21841,0.21842 -1.89226,0.22682 -2.38175,0.012 -0.20214,-0.0887 -1.19507,-0.30016 -2.2065,-0.46986 -1.53368,-0.25731 -1.89227,-0.37251 -2.16,-0.69388 -0.17657,-0.21196 -0.4528,-0.43248 -0.61386,-0.49005 -0.16106,-0.0576 -0.39937,-0.37589 -0.52957,-0.70735 -0.13428,-0.34184 -0.44096,-0.71987 -0.70861,-0.87348 -0.25954,-0.14896 -0.54282,-0.45741 -0.62952,-0.68544 -0.0883,-0.23229 -0.35766,-0.48434 -0.6125,-0.57319 -0.27445,-0.0957 -0.54453,-0.36221 -0.68092,-0.672 -0.12433,-0.28238 -0.41896,-0.60491 -0.65473,-0.71673 -0.26127,-0.12391 -0.50117,-0.42293 -0.61428,-0.76567 -0.10208,-0.30929 -0.35917,-0.67609 -0.57132,-0.81509 -0.21215,-0.139 -0.38572,-0.34692 -0.38572,-0.46206 0,-0.11512 -0.18,-0.42323 -0.4,-0.68469 -0.22,-0.26145 -0.4,-0.55499 -0.4,-0.65229 0,-0.0973 -0.11737,-0.17689 -0.26082,-0.17689 -0.2926,0 -1.75977,-1.42295 -2.22713,-2.16 -0.1674,-0.264 -0.40148,-0.54059 -0.52017,-0.61464 -0.1187,-0.0741 -0.3533,-0.45541 -0.52135,-0.84746 -0.22145,-0.51661 -0.43836,-0.7591 -0.78804,-0.88101 -0.26537,-0.0925 -0.48249,-0.2738 -0.48249,-0.40288 0,-0.57 -0.70555,-1.83969 -1.15,-2.06953 -0.25513,-0.13192 -0.53745,-0.40136 -0.62738,-0.59874 -0.0899,-0.19737 -0.31543,-0.4279 -0.5011,-0.5123 -0.18568,-0.0844 -0.47244,-0.4165 -0.63725,-0.73802 -0.16691,-0.32565 -0.45911,-0.62461 -0.65965,-0.67494 -0.19889,-0.0499 -0.52144,-0.3776 -0.72073,-0.73218 -0.1984,-0.35301 -0.47056,-0.70329 -0.60481,-0.77843 -0.13426,-0.0751 -0.39037,-0.70658 -0.56916,-1.40323 -0.17878,-0.69666 -0.415,-1.37464 -0.52494,-1.50664 -0.26978,-0.32394 -0.50169,-1.36013 -0.73408,-3.28 -0.19763,-1.63275 -0.57683,-2.68362 -1.03131,-2.85802 -0.13972,-0.0536 -0.36701,-0.36785 -0.50508,-0.69832 -0.13808,-0.33045 -0.42283,-0.76003 -0.63278,-0.95461 -0.20995,-0.19457 -0.38428,-0.51921 -0.3874,-0.72141 -0.003,-0.20225 -0.10857,-0.54769 -0.23433,-0.76769 -0.12576,-0.22 -0.23121,-0.55947 -0.23433,-0.75437 -0.003,-0.19491 -0.14967,-0.43144 -0.32567,-0.52563 -0.176,-0.0942 -0.32244,-0.29472 -0.32543,-0.44563 -0.003,-0.1509 -0.14454,-0.56237 -0.31457,-0.91437 -0.17003,-0.352 -0.31159,-0.72428 -0.31457,-0.82729 -0.003,-0.103 -0.21646,-0.23363 -0.47437,-0.29028 -0.52059,-0.11434 -0.74819,-0.32983 -0.83384,-0.78948 -0.0315,-0.16888 -0.26684,-0.4232 -0.52304,-0.56516 -0.46283,-0.25646 -0.63103,-0.46923 -0.79854,-1.01019 -0.0481,-0.15532 -0.26495,-0.34989 -0.48192,-0.43238 -0.2626,-0.0998 -0.44829,-0.34491 -0.55547,-0.73306 -0.0885,-0.32069 -0.41116,-0.82603 -0.71692,-1.12298 -0.36358,-0.3531 -0.64409,-0.84318 -0.81073,-1.4164 -0.14014,-0.48207 -0.32162,-0.91779 -0.40329,-0.96826 -0.19777,-0.12223 -0.76117,-1.33123 -0.88084,-1.89018 -0.0586,-0.27356 -0.24668,-0.50317 -0.48713,-0.59459 -0.21545,-0.0819 -0.51958,-0.42047 -0.67585,-0.75234 -0.15627,-0.33187 -0.47465,-0.69472 -0.70752,-0.80633 -0.24035,-0.1152 -0.61102,-0.56175 -0.85738,-1.03289 -0.23868,-0.45648 -0.60333,-0.96319 -0.81033,-1.12601 -0.207,-0.16283 -0.42412,-0.55061 -0.48249,-0.86174 -0.0719,-0.38349 -0.23169,-0.6229 -0.496,-0.74332 -0.21695,-0.0988 -0.42633,-0.35994 -0.47208,-0.58865 -0.0452,-0.22605 -0.44066,-0.80643 -0.87879,-1.28974 -0.43813,-0.4833 -0.83797,-1.07849 -0.88853,-1.32263 -0.0506,-0.24415 -0.29307,-0.58713 -0.53892,-0.76219 -0.24584,-0.17505 -0.49402,-0.53242 -0.5515,-0.79415 -0.0575,-0.26172 -0.26346,-0.60274 -0.45772,-0.75781 -0.19426,-0.15507 -0.59686,-0.65995 -0.89466,-1.12195 -0.29781,-0.462 -0.596191,-0.84 -0.663091,-0.84 -0.26013,0 -0.6238,-0.52327 -0.788332,-1.13431 -0.111492,-0.41405 -0.305253,-0.70049 -0.54896,-0.81153 -0.26961,-0.12284 -0.4771,-0.47322 -0.730103,-1.23289 -0.194398,-0.5837 -0.478317,-1.20527 -0.630929,-1.38127 -0.152612,-0.176 -0.44348,-0.84829 -0.646375,-1.49398 -0.218997,-0.69694 -0.494358,-1.24957 -0.677645,-1.36 -0.330003,-0.19883 -0.815212,-1.04979 -0.972753,-1.70602 -0.05282,-0.22 -0.261032,-0.472 -0.462704,-0.56 -0.223637,-0.0976 -0.446171,-0.40966 -0.570459,-0.8 -0.112079,-0.352 -0.328079,-0.748 -0.48,-0.88 -0.15192,-0.132 -0.519188,-0.72245 -0.816149,-1.31211 -0.296963,-0.58965 -0.644237,-1.09365 -0.77172,-1.12 -0.231253,-0.0478 -0.833223,-1.54669 -0.843125,-2.0994 -0.0085,-0.47195 -0.794578,-1.6798 -1.340358,-2.0594 -0.277676,-0.19313 -0.744477,-0.62519 -1.037333,-0.96012 -0.292858,-0.33493 -0.621387,-0.60897 -0.730066,-0.60897 -0.108678,0 -0.400987,-0.21229 -0.649573,-0.47176 -0.248585,-0.25947 -0.604056,-0.49347 -0.789934,-0.52 -0.220666,-0.0315 -0.397059,-0.23108 -0.508245,-0.57506 -0.09366,-0.28976 -0.309656,-0.6837 -0.48,-0.87544 -0.170342,-0.19173 -0.381715,-0.47144 -0.469715,-0.62157 -0.088,-0.15013 -0.484,-0.64146 -0.88,-1.09184 -0.396,-0.45037 -0.796198,-1.0856 -0.889328,-1.4116 -0.120624,-0.42224 -0.31471,-0.66176 -0.674765,-0.83273 -0.277989,-0.132 -0.574342,-0.43311 -0.65856,-0.66914 -0.108502,-0.30407 -0.324571,-0.47647 -0.741438,-0.59157 -0.323571,-0.0893 -0.6766,-0.28318 -0.784507,-0.43076 -0.186928,-0.25564 -1.122752,-0.49761 -3.371402,-0.87174 -1.623947,-0.27019 -1.642797,-0.27926 -1.843251,-0.88664 -0.112922,-0.34216 -0.336261,-0.62632 -0.558741,-0.71091 -0.203845,-0.0775 -0.415181,-0.28129 -0.469637,-0.45287 -0.05446,-0.17157 -0.294617,-0.38165 -0.533691,-0.46684 -0.916472,-0.32656 -1.58611,-0.51957 -1.80604,-0.52054 -0.125752,-4.8e-4 -0.34691,-0.20892 -0.491462,-0.46304 -0.144552,-0.25411 -0.467672,-0.5602 -0.718044,-0.6802 -0.250371,-0.119999 -0.481849,-0.361639 -0.514395,-0.536959 -0.07988,-0.43034 -0.336853,-0.72371 -0.782264,-0.89305 -0.203872,-0.0775 -0.399901,-0.23201 -0.435621,-0.34333 -0.03572,-0.11133 -0.178537,-0.61176 -0.317371,-1.11209 -0.175254,-0.63158 -0.349691,-0.94056 -0.570582,-1.01067 -0.442239,-0.14036 -0.573287,-0.79176 -0.256539,-1.27518 0.1417,-0.21626 0.257638,-0.50374 0.257638,-0.63884 0,-0.13509 0.06558,-0.24563 0.145726,-0.24563 0.08015,0 0.302573,-0.324 0.494274,-0.72 0.232416,-0.4801 0.450296,-0.72 0.653933,-0.72 0.167961,0 0.788699,-0.252 1.379414,-0.56 0.590717,-0.308001 1.188471,-0.560001 1.328341,-0.560001 0.139872,0 0.338722,-0.0844 0.44189,-0.18757 0.1276,-0.1276 1.608691,-0.22996 4.632,-0.32011 6.455761,-0.19251 8.595198,-0.33908 8.923228,-0.61133 0.205885,-0.17087 1.017452,-0.25563 3.187796,-0.33294 1.662859,-0.0592 3.057577,-0.18184 3.251976,-0.28588 0.187212,-0.10019 0.491547,-0.18217 0.676299,-0.18217 0.213065,0 0.454461,-0.20092 0.660059,-0.54938 0.209195,-0.35456 0.490286,-0.58587 0.792654,-0.65228 0.25768,-0.0566 0.618082,-0.29305 0.80089,-0.52546 0.182808,-0.2324 0.535539,-0.50797 0.783845,-0.61238 0.248305,-0.1044 0.5734,-0.39624 0.722432,-0.64853 0.239653,-0.40571 0.755515,-0.69784 1.899278,-1.07559 0.16172,-0.0534 0.374835,-0.31117 0.473593,-0.5728 0.10965,-0.29051 0.35547,-0.53702 0.63145,-0.63322 0.2905,-0.10127 0.536001,-0.35882 0.687381,-0.72114 0.15234,-0.3646 0.37688,-0.5991 0.63591,-0.66411 0.26026,-0.0653 0.43451,-0.24831 0.49781,-0.5228 0.28205,-1.223 0.59068,-1.91545 0.93612,-2.10033 0.22303,-0.11936 0.40954,-0.40124 0.46373,-0.70082 0.0499,-0.27564 0.1374,-0.77792 0.19456,-1.11619 0.0809,-0.47895 0.19972,-0.64843 0.53682,-0.76595 0.37969,-0.13236 0.46776,-0.30255 0.71669,-1.38496 0.15609,-0.67873 0.4659,-1.50332 0.68847,-1.83242 0.39281,-0.58081 0.83586,-2.061488 0.84093,-2.810415 10e-4,-0.202828 0.1468,-0.685528 0.32316,-1.072665 0.46654,-1.024115 0.77318,-2.665963 0.96165,-5.148922 0.18858,-2.484496 0.24991,-2.821045 0.63593,-3.489635 0.15243,-0.264 0.36507,-0.948 0.47253,-1.52 0.38313,-2.03931 0.55564,-2.643752 0.83318,-2.919368 0.15472,-0.153653 0.319,-0.513653 0.36506,-0.8 0.26373,-1.639546 0.43072,-4.370296 0.33514,-5.480363 -0.0936,-1.087058 -0.19276,-1.405051 -0.65702,-2.107112 -0.30072,-0.454763 -0.63685,-1.097456 -0.74695,-1.428205 -0.12611,-0.378854 -0.37615,-0.698438 -0.67575,-0.863723 -0.39396,-0.217335 -0.52378,-0.449922 -0.7564,-1.355219 -0.15446,-0.601072 -0.38158,-1.208317 -0.50471,-1.349434 -0.26351,-0.301987 -0.90332,-1.729424 -0.90332,-2.015338 0,-0.108945 -0.13482,-0.369793 -0.29959,-0.57966 -0.16478,-0.209868 -0.42793,-0.813578 -0.58479,-1.341578 -0.15685,-0.528 -0.37327,-1.248 -0.48092,-1.6 -0.12587,-0.41156 -0.3439,-0.713754 -0.61084,-0.84663 -0.27195,-0.135373 -0.47191,-0.417623 -0.57985,-0.818461 -0.0906,-0.336507 -0.37869,-0.881045 -0.64018,-1.210083 -0.26148,-0.32904 -0.59802,-0.876117 -0.74786,-1.215728 -0.16014,-0.36297 -0.44214,-0.684943 -0.6842,-0.781165 -0.43059,-0.171173 -1.135431,-0.563855 -1.835991,-1.022879 -0.227149,-0.148836 -1.187628,-0.388396 -2.134391,-0.532355 -1.059745,-0.161137 -2.090377,-0.430049 -2.681387,-0.699627 -0.947051,-0.431978 -1.407837,-0.571677 -2.452046,-0.743398 -0.355965,-0.05854 -0.590092,-0.214888 -0.707431,-0.472421 -0.09646,-0.211709 -0.36104,-0.426034 -0.587953,-0.476279 -1.080445,-0.239236 -1.800564,-0.611 -2.025282,-1.045556 -0.129216,-0.249876 -0.470499,-0.552744 -0.758408,-0.67304 -0.287909,-0.120296 -0.641312,-0.398568 -0.785339,-0.618381 -0.144027,-0.219815 -0.472408,-0.44177 -0.729735,-0.493235 -0.319774,-0.06396 -0.529201,-0.241655 -0.661649,-0.561413 -0.106583,-0.257311 -0.363674,-0.545245 -0.571317,-0.639853 -0.207642,-0.09461 -0.498979,-0.427949 -0.647416,-0.740755 -0.155971,-0.328685 -0.38679,-0.568741 -0.546856,-0.568741 -0.362566,0 -0.806568,-0.499728 -0.806568,-0.9078 0,-0.364202 -0.392246,-0.763336 -0.902725,-0.918574 -0.188499,-0.05732 -0.375219,-0.228485 -0.414936,-0.38036 -0.07688,-0.294007 -1.038656,-0.55521 -3.025982,-0.82181 -0.585003,-0.07848 -1.210271,-0.221162 -1.389482,-0.317072 -0.179211,-0.09591 -0.924571,-0.174786 -1.656357,-0.175277 -0.731785,-4.8e-4 -1.60827,-0.07801 -1.947745,-0.172277 -0.715248,-0.198601 -2.93101,-0.0076 -5.862773,0.50537 -2.699179,0.47228 -3.00511,0.585914 -3.310595,1.229675 -0.143043,0.30144 -0.416776,0.619471 -0.608298,0.706733 -0.191521,0.08726 -0.473161,0.370168 -0.625867,0.628678 -0.152706,0.258511 -0.455418,0.586498 -0.672693,0.728863 -0.385229,0.252411 -0.884523,1.169486 -1.074648,1.973851 -0.052,0.22 -0.25471,0.472 -0.450467,0.56 -0.858486,0.385923 -1.153374,2.905253 -0.709261,6.059466 0.482659,3.427966 0.603747,6.134964 0.351611,7.860534 -0.271,1.85468 -0.773497,4.283797 -0.910201,4.4 -0.248987,0.211646 -0.653117,4.35011 -0.794581,8.136843 -0.15948,4.269018 -0.351574,6.188768 -0.689211,6.887823 -0.275059,0.569494 -0.246151,5.8951 0.03472,6.396627 0.170552,0.304539 0.223048,2.478576 0.260781,10.800004 0.03898,8.59637 0.09122,10.67057 0.298669,11.8587 0.138284,0.792 0.361278,2.232 0.49554,3.200001 0.134263,0.968 0.285978,1.904 0.337146,2.08 0.05117,0.176 0.121594,0.824 0.156502,1.44 0.03491,0.616 0.145855,1.8271 0.246546,2.69132 0.165278,1.418559 0.109611,3.115959 -0.121981,3.719479 -0.04837,0.12606 -0.186877,0.2292 -0.307785,0.2292 -0.120908,0 -0.264426,0.198 -0.318927,0.44 -0.0545,0.242 -0.157294,0.692 -0.22843,1 -0.07113,0.308 -0.174304,0.92 -0.229263,1.36 -0.124932,1.0002 -0.442713,2.16 -0.591832,2.16 -0.199993,0 -0.434979,0.99029 -0.777235,3.27545 -0.194677,1.29981 -0.432213,2.34308 -0.567526,2.4926 -0.365883,0.40429 -0.313159,3.22572 0.08728,4.67076 0.176,0.63512 0.32,1.4614 0.32,1.83619 0,0.37479 0.113642,1.70824 0.252538,2.96322 0.182758,1.65128 0.334057,2.39692 0.547556,2.69847 0.466901,0.65946 0.602631,1.45779 0.798196,4.69482 0.209596,3.46928 0.341625,4.34739 0.694172,4.61689 0.44898,0.34322 0.766951,3.34972 0.883272,8.3516 0.128058,5.50655 0.126464,5.92129 -0.04461,11.6 -0.142469,4.72915 0.01435,7.17776 0.493624,7.70734 0.137221,0.15164 0.371987,0.83751 0.521701,1.52418 0.227876,1.04515 0.338984,1.28086 0.682244,1.44733 0.291381,0.14129 0.440704,0.3728 0.516013,0.8 0.16748,0.95005 0.60905,1.71433 1.024576,1.77337 0.233408,0.0332 0.462898,0.25216 0.628773,0.6 0.143666,0.30128 0.353072,0.54778 0.465346,0.54778 0.297598,0 0.856598,0.65149 0.856598,0.99834 0,0.18916 0.203941,0.3832 0.558485,0.53134 0.373872,0.15621 0.633841,0.4137 0.786445,0.77893 0.151769,0.36323 0.352808,0.56326 0.601513,0.59848 0.256274,0.0363 0.425088,0.21233 0.537687,0.56067 0.192532,0.59565 0.674905,1.17224 0.980668,1.17224 0.11836,0 0.215202,0.0968 0.215202,0.2152 0,0.11837 0.126,0.32808 0.28,0.46603 0.226944,0.20331 0.68929,0.24803 2.44,0.236 1.188,-0.008 2.540133,0.0412 3.004741,0.10963 0.784417,0.11559 0.86879,0.17191 1.181533,0.7888 0.185235,0.3654 0.540011,0.89733 0.78839,1.18208 0.248381,0.28477 0.555413,0.86077 0.682296,1.28 0.126883,0.41925 0.388062,1.12226 0.580397,1.56226 0.381443,0.87262 0.358291,1.94536 -0.06215,2.88 -0.265611,0.59045 -0.551843,1.40848 -0.768037,2.19499 -0.09491,0.3453 -0.290049,0.63514 -0.464985,0.69066 -0.165791,0.0526 -0.347275,0.28888 -0.403299,0.52502 -0.141876,0.59799 -0.521444,0.98933 -0.959572,0.98933 -0.250099,0 -0.451632,0.15574 -0.618112,0.47768 -0.13586,0.26272 -0.426817,0.52282 -0.646571,0.57797 -0.219752,0.0551 -0.560443,0.30406 -0.757089,0.55312 -0.314512,0.39835 -0.540453,0.48136 -1.877538,0.68976 -2.691163,0.41945 -7.520941,0.62888 -10.174398,0.44117 -1.31208,-0.0928 -3.213602,-0.21245 -4.225602,-0.26584 -2.640154,-0.13932 -3.51511,-0.30495 -4.415603,-0.83589 -0.438056,-0.25829 -1.056515,-0.5184 -1.374354,-0.57802 -0.317838,-0.0596 -0.665939,-0.21451 -0.773557,-0.34419 -0.107614,-0.12966 -0.297861,-0.23576 -0.422765,-0.23576 -0.124904,0 -0.367323,-0.13064 -0.538708,-0.2903 -0.213912,-0.1993 -0.87649,-0.37368 -2.113309,-0.5562 -2.937026,-0.43342 -3.919114,-0.47032 -5.641701,-0.21193 -0.88,0.13198 -3.076,0.32365 -4.88,0.42589 -2.836818,0.1608 -3.32463,0.22689 -3.610312,0.48923 -0.181672,0.16681 -0.422563,0.30331 -0.535314,0.30331 -0.11275,0 -0.39611,0.0999 -0.629688,0.22197 -0.233577,0.12208 -0.624132,0.3012 -0.867899,0.39803 -0.243766,0.0968 -0.505313,0.33894 -0.581216,0.53803 -0.116958,0.30677 -0.395352,0.41765 -1.825558,0.72711 -1.458335,0.31553 -1.792311,0.33956 -2.458784,0.17689 -0.920535,-0.22465 -1.171229,-0.39787 -1.171229,-0.80926 0,-0.16943 -0.193232,-0.48714 -0.429403,-0.706 -0.550967,-0.51063 -0.70364,-1.56125 -0.509591,-3.50677 0.112396,-1.12686 0.211263,-1.49219 0.454647,-1.68 0.171059,-0.132 0.404017,-0.528 0.517683,-0.88 0.12344,-0.38227 0.351637,-0.7036 0.566664,-0.79794 0.198,-0.0869 0.36,-0.21905 0.36,-0.29374 0,-0.29661 1.198315,-1.82742 1.61991,-2.06938 0.246732,-0.14161 0.490644,-0.42496 0.542024,-0.62969 0.05138,-0.20472 0.306618,-0.49458 0.567191,-0.64411 0.260573,-0.14956 0.543334,-0.4549 0.628361,-0.67853 0.08503,-0.22363 0.252447,-0.40661 0.372047,-0.40661 0.119598,0 0.544233,-0.34514 0.94363,-0.76698 0.399399,-0.42184 1.059327,-0.94372 1.466507,-1.15972 0.407181,-0.21602 0.74033,-0.47647 0.74033,-0.5788 0,-0.10232 0.231173,-0.38058 0.513718,-0.61832 0.502255,-0.42261 1.086282,-1.47213 1.086282,-1.95207 0,-0.13253 0.238528,-0.46667 0.530061,-0.74253 0.525169,-0.49696 0.714664,-0.95915 1.0912,-2.66158 0.107049,-0.484 0.32704,-1.10608 0.488865,-1.38242 0.419732,-0.7167 0.599647,-2.43051 0.60233,-5.73758 0.0029,-3.58661 -0.205211,-6.19294 -0.568931,-7.12481 -0.172877,-0.44292 -0.303091,-1.31052 -0.342043,-2.27898 -0.05378,-1.33715 -0.01778,-1.6386 0.248424,-2.08 0.292256,-0.4846 0.448665,-1.4733 0.916942,-5.79621 0.0572,-0.528 0.159405,-1.032 0.227131,-1.12 0.265983,-0.3456 0.644075,-3.73301 0.901867,-8.08 0.222023,-3.74385 0.249861,-5.87729 0.155536,-11.92 -0.203592,-13.04284 -0.40428,-17.63254 -0.797558,-18.24 -0.135587,-0.20943 -0.271862,-1.50863 -0.394392,-3.76 -0.20999,-3.858349 -0.06674,-6.178229 0.449186,-7.274379 0.184046,-0.39104 0.435003,-1.655081 0.636425,-3.205621 0.290786,-2.23845 0.312047,-2.8313 0.169259,-4.72 -0.08981,-1.188 -0.230908,-3.40231 -0.313545,-4.9207 -0.08264,-1.51838 -0.224613,-2.89965 -0.315503,-3.06948 -0.09089,-0.16983 -0.165254,-0.45151 -0.165254,-0.62597 0,-0.17445 -0.07358,-0.34171 -0.163504,-0.37168 -0.350392,-0.1168 -0.491906,-2.04264 -0.574048,-7.812167 -0.05964,-4.189254 -0.01933,-7.03816 0.133574,-9.44 0.323008,-5.073798 -0.08867,-11.863782 -0.92644,-15.28 -0.343,-1.398675 -0.412467,-2.193906 -0.552089,-6.32 -0.137283,-4.056954 -0.123547,-5.000952 0.09778,-6.72 0.512008,-3.976706 0.442685,-6.059803 -0.233256,-7.009074 -0.318431,-0.447203 -1.759841,-1.310926 -2.18771,-1.310926 -0.118889,0 -0.310245,-0.129768 -0.425235,-0.288373 -0.182963,-0.25236 -0.879309,-0.4656 -2.529072,-0.774469 -1.062438,-0.198908 -1.6,-0.407611 -1.6,-0.621177 0,-0.127427 -0.234,-0.339307 -0.52,-0.470845 -0.956173,-0.439765 -1.561227,-0.843541 -1.88961,-1.261011 -0.179092,-0.22768 -0.510494,-0.506251 -0.736446,-0.619045 -0.478019,-0.238624 -0.768453,-0.980603 -0.907034,-2.317221 -0.141195,-1.361835 0.343932,-2.920038 0.91268,-2.931492 0.09923,-0.002 0.294247,-0.235999 0.433381,-0.52 0.158304,-0.32313 0.440269,-0.578477 0.753466,-0.682337 0.275272,-0.09128 0.665433,-0.319883 0.867029,-0.507998 0.285508,-0.26642 0.709988,-0.380049 1.920228,-0.51403 2.054674,-0.227463 8.044392,-0.219867 11.246306,0.01426 2.060832,0.150692 3.293154,0.143007 6.32,-0.03941 2.501347,-0.150749 6.143408,-0.212951 10.88,-0.185818 7.314414,0.0419 10.413752,-0.109958 10.889712,-0.533565 0.250366,-0.222828 1.150192,-0.337126 5.830288,-0.7405764 2.971763,-0.256183 10.731882,-0.234943 14.764398,0.04041 2.701808,0.184489 3.008298,0.235474 3.306708,0.5500784 0.309217,0.325998 1.153246,0.617664 2.248894,0.777134 0.264,0.03843 0.876,0.186904 1.36,0.329952 1.879128,0.555385 5.576279,0.928045 7.769529,0.78314 1.10414,-0.07295 1.38502,-0.03649 1.95363,0.253597 0.36525,0.186337 0.77243,0.338794 0.90484,0.338794 0.13241,0 0.32085,0.14969 0.41877,0.332645 0.0979,0.182954 0.37672,0.401911 0.61958,0.48657 0.25072,0.0874 0.53924,0.367387 0.6676,0.647838 0.21264,0.464607 0.61264,0.693978 1.82605,1.047106 0.176,0.05122 0.54571,0.334487 0.82158,0.629484 0.27587,0.294997 0.64897,0.536357 0.8291,0.536357 0.1813,0 0.48013,0.214302 0.66932,0.48 0.18798,0.264 0.42689,0.480658 0.53089,0.481462 0.10401,8e-4 0.52898,0.166079 0.94438,0.367274 0.58542,0.283546 0.84936,0.544075 1.17369,1.158536 0.23014,0.436 0.58504,0.936728 0.78868,1.112728 0.20364,0.176 0.49402,0.518414 0.64531,0.76092 0.26778,0.429254 1.45559,1.078059 1.97739,1.080091 0.14281,4.8e-4 0.37807,0.20917 0.5228,0.463584 0.16832,0.295898 0.44824,0.503227 0.77686,0.575405 0.31817,0.06988 0.60936,0.279464 0.76495,0.550571 0.13818,0.240755 0.39521,0.503368 0.57118,0.583584 0.17597,0.08021 0.45839,0.415845 0.6276,0.745845 0.1692,0.33 0.38919,0.6 0.48886,0.6 0.0997,0 0.55399,0.154501 1.00958,0.343334 0.63923,0.264944 0.84698,0.438469 0.90993,0.76 0.22062,1.127008 0.43652,1.68904 0.79694,2.074528 0.26514,0.283591 0.51723,0.913581 0.76808,1.919504 0.30472,1.221941 0.43967,1.522564 0.73368,1.634343 0.21136,0.08036 0.39844,0.321163 0.45264,0.582632 0.23084,1.113643 0.50341,1.755806 0.90226,2.125659 0.23724,0.22 0.48052,0.598798 0.54062,0.841773 0.0601,0.242974 0.30919,0.584137 0.55356,0.758139 0.35896,0.255603 0.47657,0.508787 0.61236,1.318227 0.0924,0.551024 0.21287,1.217861 0.26762,1.481861 0.0548,0.264 0.1313,0.636965 0.17009,0.828811 0.0388,0.191847 0.16042,0.445165 0.27029,0.562931 0.39387,0.422178 0.71627,1.185031 0.91996,2.176772 0.11393,0.554683 0.35849,1.199889 0.54347,1.433793 0.18911,0.239125 0.41538,0.883229 0.51692,1.471487 0.41492,2.403811 0.57999,2.996137 1.00503,3.606206 0.24523,0.352 0.49339,0.885987 0.55146,1.186638 0.0581,0.300652 0.28703,0.78454 0.50881,1.075309 0.65607,0.860149 0.79216,1.987418 0.7893,6.538053 -0.003,4.99048 -0.21416,6.982606 -0.80193,7.570379 -0.2163,0.216291 -0.39326,0.462359 -0.39326,0.546815 0,0.08446 -0.11433,0.546824 -0.25406,1.027486 -0.18641,0.641238 -0.35453,0.912128 -0.63139,1.017392 -0.25883,0.09841 -0.41001,0.317611 -0.48137,0.69799 -0.0572,0.304989 -0.29509,0.73844 -0.5286,0.963224 -0.23352,0.224784 -0.60307,0.590501 -0.82122,0.812706 -0.21814,0.222205 -0.52311,0.404008 -0.67769,0.404008 -0.18253,0 -0.44839,0.331478 -0.75838,0.945552 -0.26252,0.520053 -0.68605,1.094187 -0.94117,1.275847 -0.25512,0.18167 -0.55126,0.54131 -0.65809,0.79921 -0.10683,0.25791 -0.3233,0.50267 -0.48105,0.54393 -0.16565,0.0433 -0.38447,0.36792 -0.51794,0.76833 -0.1544,0.46321 -0.38675,0.77956 -0.70007,0.95317 -0.25793,0.14292 -0.4944,0.39803 -0.52549,0.56691 -0.0709,0.38524 -0.48905,0.86705 -0.75247,0.86705 -0.10967,0 -0.34874,0.25311 -0.53126,0.56246 -0.18252,0.30936 -0.50779,0.62935 -0.72281,0.7111 -0.21502,0.0818 -0.48679,0.35064 -0.60394,0.59754 -0.11715,0.2469 -0.43777,0.5569 -0.71248,0.6889 -0.27472,0.132 -0.61522,0.44122 -0.75667,0.68716 -0.14145,0.24594 -0.45669,0.51704 -0.70052,0.60245 -0.24383,0.0854 -0.56598,0.37018 -0.71589,0.63284 -0.15419,0.27016 -0.40625,0.47755 -0.58041,0.47755 -0.31999,0 -0.61639,0.27834 -0.90078,0.84591 -0.0914,0.18248 -0.36532,0.38486 -0.60862,0.44973 -0.46338,0.12356 -3.63763,3.16112 -3.63763,3.48099 0,0.10085 -0.13145,0.18337 -0.29211,0.18337 -0.16066,0 -0.46121,0.18 -0.66789,0.4 -0.20668,0.22 -0.51067,0.4 -0.67553,0.4 -0.16486,0 -0.45831,0.16976 -0.65211,0.37724 -0.42544,0.45547 -1.1108,0.57414 -4.23351,0.73305 -2.02918,0.10327 -2.32192,0.15439 -2.65031,0.4629 -0.20304,0.19075 -0.47612,0.34681 -0.60685,0.34681 -0.13073,0 -0.30609,0.0664 -0.38969,0.14748 -0.0836,0.0811 -0.3991,0.21042 -0.70112,0.28735 -0.40908,0.1042 -0.58797,0.26041 -0.70148,0.61252 -0.20374,0.632001 -0.41108,0.872651 -0.75187,0.872651 -0.43035,0 -0.49951,0.33632 -0.67455,3.28 -0.12766,2.147 -0.11552,3.03938 0.0576,4.23609 0.19551,1.351149 0.2629,1.537629 0.61938,1.714129 0.25344,0.12549 0.44078,0.39072 0.51131,0.72392 0.35673,1.68523 0.45789,1.88586 0.95087,1.88586 0.18757,0 0.40269,0.20905 0.56468,0.54875 0.15473,0.32447 0.41869,0.58815 0.64585,0.64516 0.2113,0.053 0.53149,0.3461 0.71154,0.65126 0.22321,0.37831 0.44485,0.55483 0.69668,0.55483 0.53839,0 2.66603,2.17619 3.20532,3.27846 0.3386,0.69206 0.51121,0.88154 0.80308,0.88154 0.40791,0 0.55008,0.15824 0.79067,0.88 0.1068,0.32039 0.28421,0.49759 0.53356,0.5329 0.24895,0.0353 0.4498,0.2354 0.60214,0.6 0.12573,0.30091 0.34081,0.5471 0.47796,0.5471 0.38659,0 1.70258,0.82311 2.13639,1.33624 0.21453,0.25376 0.6643,0.75548 0.99949,1.11495 0.3352,0.35946 0.65568,0.801 0.71219,0.98119 0.0565,0.18019 0.27274,0.48618 0.48051,0.67998 0.20777,0.1938 0.37776,0.527 0.37776,0.74046 0,0.6075 0.42553,1.47468 0.86489,1.76257 0.23756,0.15565 0.44067,0.47816 0.49987,0.79371 0.0548,0.29234 0.34119,0.83667 0.63632,1.20963 0.29514,0.37296 0.69151,1.16032 0.88083,1.74969 0.18931,0.58937 0.48533,1.22341 0.65781,1.40898 0.17247,0.18557 0.39008,0.61757 0.48357,0.96 0.20873,0.76455 0.40779,1.0226 0.78881,1.0226 0.19358,0 0.40159,0.24566 0.61333,0.72432 0.17622,0.39838 0.88983,1.31892 1.5858,2.04563 0.69596,0.72672 1.316,1.48078 1.37786,1.67568 0.0619,0.1949 0.22947,0.35437 0.37247,0.35437 0.32903,0 1.93896,1.67694 2.19537,2.28674 0.10608,0.2523 0.36165,0.543 0.56792,0.64599 0.22977,0.11473 0.42579,0.40419 0.50605,0.74727 0.31144,1.33139 0.47478,1.76 0.67072,1.76 0.30322,0 0.73838,0.53329 0.73838,0.90488 0,0.17583 0.324,0.67259 0.72,1.10392 0.396,0.43132 0.72,0.83275 0.72,0.89206 0,0.0593 0.20605,0.17967 0.45789,0.26746 0.39261,0.13686 0.49902,0.31155 0.74632,1.22515 0.22845,0.84394 0.3783,1.11201 0.72056,1.289 0.35865,0.18546 0.45074,0.3675 0.54153,1.0705 0.16133,1.24911 0.61092,2.51415 0.93013,2.61714 0.15697,0.0506 0.35386,0.37888 0.45561,0.75954 0.0986,0.36869 0.43365,1.02574 0.74468,1.46011 0.31103,0.43437 0.62991,1.023 0.70862,1.30808 0.0957,0.3465 0.28016,0.564 0.55656,0.65613 0.33406,0.11135 0.48244,0.35173 0.77278,1.25192 0.23078,0.71553 0.47594,1.17359 0.68526,1.28036 0.564,0.28768 0.84006,0.58255 0.84006,0.8973 0,0.17826 0.16743,0.36803 0.4041,0.45801 0.22226,0.0845 0.44478,0.28072 0.49451,0.43604 0.21988,0.68683 0.45762,0.99767 0.87407,1.14284 0.24574,0.0857 0.55722,0.36931 0.6922,0.63031 0.13672,0.2644 0.46052,0.54955 0.73118,0.6439 0.2735,0.0953 0.55122,0.34149 0.63555,0.56329 0.11249,0.29586 0.36736,0.45719 1.02371,0.648 0.48066,0.13973 0.98788,0.25406 1.12716,0.25406 0.13927,0 0.35845,0.25184 0.48706,0.55965 0.12861,0.30779 0.41399,0.64173 0.63418,0.74205 0.2202,0.10032 0.47103,0.38515 0.55741,0.63296 0.1094,0.3138 0.31232,0.49076 0.66864,0.5831 0.3929,0.10181 0.55317,0.26128 0.6909,0.68741 0.12674,0.39211 0.28871,0.57032 0.55229,0.60766 0.22262,0.0315 0.50452,0.27579 0.6993,0.60591 0.17948,0.3042 0.55902,0.65032 0.84343,0.76915 0.38294,0.16 0.54364,0.34875 0.61939,0.72749 0.0735,0.36764 0.23044,0.55979 0.55813,0.68348 0.25478,0.0962 0.52743,0.3617 0.61813,0.60197 0.11568,0.30648 0.31518,0.46048 0.69496,0.53643 0.59263,0.11853 1.1137,0.51668 1.1137,0.85096 0,0.12026 0.17653,0.29908 0.39229,0.39738 0.21576,0.0983 0.50714,0.42077 0.64751,0.71658 0.1418,0.29881 0.37631,0.53828 0.52771,0.53888 0.14987,0 0.48849,0.14409 0.75249,0.31894 0.264,0.17485 0.59772,0.31837 0.7416,0.31894 0.14388,0 0.37954,0.18106 0.52369,0.40106 0.16728,0.2553 0.41117,0.4 0.6742,0.4 0.24669,0 0.58294,0.18286 0.83763,0.4555 0.42947,0.45976 1.86006,1.3045 2.2092,1.3045 0.10387,0 0.27323,0.25562 0.37634,0.56805 0.10312,0.31243 0.3247,0.65376 0.49241,0.75849 0.16771,0.10474 0.30493,0.26765 0.30493,0.36202 0,0.0944 0.216,0.37851 0.48,0.63144 0.264,0.25293 0.48,0.5904 0.48,0.74994 0,0.15953 0.0955,0.29006 0.21211,0.29006 0.11666,0 0.38296,0.18186 0.59178,0.40413 0.89246,0.95 2.34227,1.32553 5.65206,1.46405 1.94954,0.0816 2.42333,0.14878 2.64,0.37438 0.38042,0.39613 1.16914,0.60496 2.95687,0.78294 0.86505,0.0861 1.66318,0.23159 1.77363,0.32325 0.23795,0.19749 0.48189,0.98327 0.74854,2.41125 0.10946,0.58611 0.32314,1.16491 0.48962,1.32624 0.16656,0.16139 0.29529,0.51029 0.29518,0.8 -1.1e-4,0.28256 0.0827,1.10674 0.18394,1.83149 0.13849,0.99117 0.1388,1.76437 0.002,3.12 -0.16573,1.63315 -0.22053,1.82979 -0.58392,2.09561 -0.22056,0.16135 -0.40103,0.3925 -0.40103,0.51367 0,0.12117 -0.12129,0.61424 -0.26955,1.09571 l -0.26955,0.87538 -0.80674,0.11096 c -0.44369,0.061 -2.7164,0.0916 -5.05044,0.0679 -2.33404,-0.0236 -5.46772,0 -6.96372,0.0521 -1.496,0.0523 -2.828,0.0671 -2.96,0.0328 z"
+ id="path32" />
+ </g>
+</svg>
diff --git a/cmd/rescribe/main.go b/cmd/rescribe/main.go
index 59d8166..16c284c 100644
--- a/cmd/rescribe/main.go
+++ b/cmd/rescribe/main.go
@@ -1,4 +1,4 @@
-// Copyright 2021 Nick White.
+// Copyright 2021-2022 Nick White.
// Use of this source code is governed by the GPLv3
// license that can be found in the LICENSE file.
@@ -12,7 +12,7 @@ package main
import (
"archive/zip"
"bytes"
- _ "embed"
+ "context"
"flag"
"fmt"
"image/jpeg"
@@ -28,13 +28,14 @@ import (
"strings"
"time"
+ "golang.org/x/image/tiff"
"rescribe.xyz/bookpipeline"
"rescribe.xyz/bookpipeline/internal/pipeline"
"rescribe.xyz/pdf"
"rescribe.xyz/utils/pkg/hocr"
)
-const usage = `Usage: rescribe [-v] [-gui] [-systess] [-tesscmd] [-t training] bookdir/book.pdf [savedir]
+const usage = `Usage: rescribe [-v] [-gui] [-systess] [-tesscmd cmd] [-gbookcmd cmd] [-t training] bookdir/book.pdf [savedir]
Process and OCR a book using the Rescribe pipeline on a local machine.
@@ -42,9 +43,6 @@ OCR results are saved into the bookdir directory unless savedir is
specified.
`
-//go:embed tessdata.20211001.zip
-var tessdatazip []byte
-
const QueueTimeoutSecs = 2 * 60
const PauseBetweenChecks = 1 * time.Second
const LogSaveTime = 1 * time.Minute
@@ -73,6 +71,7 @@ type Clouder interface {
type Pipeliner interface {
Clouder
PreQueueId() string
+ PreNoWipeQueueId() string
WipeQueueId() string
OCRPageQueueId() string
AnalyseQueueId() string
@@ -93,7 +92,7 @@ func resetTimer(t *time.Timer, d time.Duration) {
}
}
-// unpackTessZip unpacks a byte array of a zip file into a directory
+// unpackZip unpacks a byte array of a zip file into a directory
func unpackZip(b []byte, dir string) error {
br := bytes.NewReader(b)
zr, err := zip.NewReader(br, br.Size())
@@ -138,22 +137,25 @@ func unpackZip(b []byte, dir string) error {
func main() {
deftesscmd := "tesseract"
+ defgbookcmd := "getgbook"
if runtime.GOOS == "windows" {
deftesscmd = "C:\\Program Files\\Tesseract-OCR\\tesseract.exe"
+ defgbookcmd = "getgbook.exe"
}
verbose := flag.Bool("v", false, "verbose")
usegui := flag.Bool("gui", false, "Use graphical user interface")
systess := flag.Bool("systess", false, "Use the system installed Tesseract, rather than the copy embedded in rescribe.")
- training := flag.String("t", "rescribev8_fast.traineddata", `Path to the tesseract training file to use.
+ training := flag.String("t", "rescribev9_fast.traineddata", `Path to the tesseract training file to use.
These training files are included in rescribe, and are always available:
-- carolinemsv1_fast.traineddata (Caroline Miniscule)
-- eng.traineddata (Modern English)
-- lat.traineddata (Latin modern printing)
-- rescribefrav2_fast.traineddata (French historic printing)
-- rescribev8_fast.traineddata (Latin historic printing)
+- eng.traineddata (English, modern print)
+- lat.traineddata (Latin, modern print)
+- rescribev9_fast.traineddata (Latin/English/French, printed ca 1500-1800)
`)
+ gbookcmd := flag.String("gbookcmd", defgbookcmd, "The getgbook executable to run. You may need to set this to the full path of getgbook.exe if you're on Windows.")
tesscmd := flag.String("tesscmd", deftesscmd, "The Tesseract executable to run. You may need to set this to the full path of Tesseract.exe if you're on Windows.")
+ wipe := flag.Bool("wipe", false, "Use wiper tool to remove noise like gutters from page before processing.")
+ fullpdf := flag.Bool("fullpdf", false, "Use highest image quality for searchable PDF (requires lots of RAM).")
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), usage)
@@ -185,7 +187,7 @@ These training files are included in rescribe, and are always available:
log.Fatalln("Error setting up tesseract directory:", err)
}
- if !*systess {
+ if !*systess && len(tesszip) > 0 {
err = unpackZip(tesszip, tessdir)
if err != nil {
log.Fatalln("Error unpacking embedded Tesseract zip:", err)
@@ -200,30 +202,73 @@ These training files are included in rescribe, and are always available:
}
}
+ _, err = exec.LookPath(tessCommand)
+ if err != nil {
+ log.Fatalf("No tesseract executable found [tried %s], either set -tesscmd and -systess on the command line or use the official build which includes an embedded copy of Tesseract.", tessCommand)
+ }
+
+ gbookCommand := *gbookcmd
+ if len(gbookzip) > 0 {
+ err = unpackZip(gbookzip, tessdir)
+ if err != nil {
+ log.Fatalln("Error unpacking embedded getgbook zip:", err)
+ }
+ switch runtime.GOOS {
+ case "darwin":
+ gbookCommand = filepath.Join(tessdir, "getgbook")
+ case "linux":
+ gbookCommand = filepath.Join(tessdir, "getgbook")
+ case "windows":
+ gbookCommand = filepath.Join(tessdir, "getgbook.exe")
+ }
+ }
+
+ _, err = exec.LookPath(gbookCommand)
+ if err != nil {
+ log.Printf("No getgbook found [tried %s], google book downloading will be disabled, either set -gbookcmd on the command line or use the official build which includes an embedded getgbook.", gbookCommand)
+ gbookCommand = ""
+ }
+
tessdatadir := filepath.Join(tessdir, "tessdata")
err = os.MkdirAll(tessdatadir, 0755)
if err != nil {
log.Fatalln("Error setting up tessdata directory:", err)
}
- err = unpackZip(tessdatazip, tessdatadir)
- if err != nil {
- log.Fatalln("Error unpacking embedded tessdata zip:", err)
+ if len(tessdatazip) > 0 {
+ err = unpackZip(tessdatazip, tessdatadir)
+ if err != nil {
+ log.Fatalln("Error unpacking embedded tessdata zip:", err)
+ }
}
- // if trainingPath doesn't exist, set it to the embedded training instead
- _, err = os.Stat(trainingPath)
- if err != nil && !os.IsExist(err) {
- trainingPath = filepath.Base(trainingPath)
- trainingPath = filepath.Join(tessdatadir, trainingPath)
+ // copy training path to the tessdir directory, so that we can keep that a
+ // writeable space, which is needed opening other trainings in sandboxes
+ // like flatpak
+ in, err := os.Open(trainingPath)
+ trainingPath = filepath.Join(tessdatadir, filepath.Base(trainingPath))
+ if err != nil {
+ in, err = os.Open(trainingPath)
+ if err != nil {
+ log.Fatalf("Error opening training file %s: %v", trainingPath, err)
+ }
}
-
- f, err := os.Open(trainingPath)
+ defer in.Close()
+ newPath := trainingPath + ".new"
+ out, err := os.Create(newPath)
if err != nil {
- fmt.Fprintf(os.Stderr, "Error: Training files %s or %s could not be opened.\n", *training, trainingPath)
- fmt.Fprintf(os.Stderr, "Set the `-t` flag with path to a tesseract .traineddata file.\n")
- os.Exit(1)
+ log.Fatalf("Error creating training file %s: %v", newPath, err)
+ }
+ defer out.Close()
+ _, err = io.Copy(out, in)
+ if err != nil {
+ log.Fatalf("Error copying training file to %s: %v", newPath, err)
+ }
+ in.Close()
+ out.Close()
+ err = os.Rename(newPath, trainingPath)
+ if err != nil {
+ log.Fatalf("Error moving new training file to %s: %v", trainingPath, err)
}
- f.Close()
abstraining, err := filepath.Abs(trainingPath)
if err != nil {
@@ -237,13 +282,26 @@ These training files are included in rescribe, and are always available:
}
if flag.NArg() < 1 || *usegui {
- err := startGui(*verboselog, tessCommand, trainingName, *systess, tessdir)
+ err := startGui(verboselog, tessCommand, gbookCommand, trainingName, tessdir)
+ err = os.RemoveAll(tessdir)
+ if err != nil {
+ log.Printf("Error removing tesseract directory %s: %v", tessdir, err)
+ }
+
if err != nil {
log.Fatalln("Error in gui:", err)
}
return
}
+ f, err := os.Open(trainingPath)
+ if err != nil {
+ fmt.Fprintf(os.Stderr, "Error: Training files %s or %s could not be opened.\n", *training, trainingPath)
+ fmt.Fprintf(os.Stderr, "Set the `-t` flag with path to a tesseract .traineddata file.\n")
+ os.Exit(1)
+ }
+ f.Close()
+
bookdir := flag.Arg(0)
bookname := strings.ReplaceAll(filepath.Base(bookdir), " ", "_")
savedir := bookdir
@@ -258,27 +316,44 @@ These training files are included in rescribe, and are always available:
log.Fatalln("Error opening book file/dir:", err)
}
+ var ctx context.Context
+ ctx = context.Background()
+
+ // TODO: support google book downloading, as done with the GUI
+
// try opening as a PDF, and extracting
if !fi.IsDir() {
if flag.NArg() < 2 {
savedir = strings.TrimSuffix(bookdir, ".pdf")
}
- bookdir, err = extractPdfImgs(bookdir)
+ bookdir, err = extractPdfImgs(ctx, bookdir)
if err != nil {
log.Fatalln("Error opening file as PDF:", err)
}
+ // if this occurs then extractPdfImgs() will have recovered from
+ // a panic in the pdf package
+ if bookdir == "" {
+ log.Fatalln("Error opening file as PDF: image type not supported, you will need to extract images manually.")
+ }
bookname = strings.TrimSuffix(bookname, ".pdf")
ispdf = true
}
- err = startProcess(*verboselog, tessCommand, bookdir, bookname, trainingName, *systess, savedir, tessdir)
+ err = startProcess(ctx, verboselog, tessCommand, bookdir, bookname, trainingName, savedir, tessdir, !*wipe, *fullpdf)
if err != nil {
log.Fatalln(err)
}
+ if !*systess {
+ err = os.RemoveAll(tessdir)
+ if err != nil {
+ log.Printf("Error removing tesseract directory %s: %v", tessdir, err)
+ }
+ }
+
if ispdf {
os.RemoveAll(filepath.Clean(filepath.Join(bookdir, "..")))
}
@@ -286,7 +361,16 @@ These training files are included in rescribe, and are always available:
// extractPdfImgs extracts all images embedded in a PDF to a
// temporary directory, which is returned on success.
-func extractPdfImgs(path string) (string, error) {
+func extractPdfImgs(ctx context.Context, path string) (string, error) {
+ defer func() {
+ // unfortunately the pdf library will panic if it sees an encoding
+ // it can't decode, so recover from that and give a warning
+ r := recover()
+ if r != nil {
+ fmt.Fprintf(os.Stderr, "Warning: Error extracting from PDF: %v\n", r)
+ }
+ }()
+
p, err := pdf.Open(path)
if err != nil {
return "", err
@@ -305,6 +389,11 @@ func extractPdfImgs(path string) (string, error) {
}
for pgnum := 1; pgnum <= p.NumPage(); pgnum++ {
+ select {
+ case <-ctx.Done():
+ return "", ctx.Err()
+ default:
+ }
if p.Page(pgnum).V.IsNull() {
continue
}
@@ -323,7 +412,7 @@ func extractPdfImgs(path string) (string, error) {
continue
}
- fn := fmt.Sprintf("%s-%04d.jpg", k, pgnum)
+ fn := fmt.Sprintf("%04d-%s.jpg", pgnum, k)
path := filepath.Join(tempdir, fn)
w, err := os.Create(path)
defer w.Close()
@@ -347,12 +436,20 @@ func extractPdfImgs(path string) (string, error) {
}
// TODO: check for places where there are multiple images per page, and only keep largest ones where that's the case
+ select {
+ case <-ctx.Done():
+ return "", ctx.Err()
+ default:
+ }
+
return tempdir, nil
}
// rmIfNotImage attempts to decode a given file as an image. If it is
// decode-able as PNG, then rename file extension from .jpg to .png,
-// if it fails to be read as PNG or JPEG it will be deleted.
+// if it is decode-able as TIFF then convert to PNG and rename file
+// extension appropriately, if it fails to be read as PNG, TIFF or
+// JPEG it will just be deleted.
func rmIfNotImage(f string) error {
r, err := os.Open(f)
defer r.Close()
@@ -363,9 +460,9 @@ func rmIfNotImage(f string) error {
r.Close()
if err == nil {
b := strings.TrimSuffix(f, ".jpg")
- err = os.Rename(f, b + ".png")
+ err = os.Rename(f, b+".png")
if err != nil {
- return fmt.Errorf("Error renaming %s to %s: %v", f, b + ".png", err)
+ return fmt.Errorf("Error renaming %s to %s: %v", f, b+".png", err)
}
return nil
}
@@ -376,19 +473,49 @@ func rmIfNotImage(f string) error {
return fmt.Errorf("Failed to open image %s: %v\n", f, err)
}
_, err = jpeg.Decode(r)
+ r.Close()
+ if err == nil {
+ return nil
+ }
+
+ r, err = os.Open(f)
+ defer r.Close()
if err != nil {
+ return fmt.Errorf("Failed to open image %s: %v\n", f, err)
+ }
+ t, err := tiff.Decode(r)
+ if err == nil {
+ b := strings.TrimSuffix(f, ".jpg")
+ n, err := os.Create(b + ".png")
+ defer n.Close()
+ if err != nil {
+ return fmt.Errorf("Failed to create file to store new png %s from tiff %s: %v\n", b+".png", f, err)
+ }
+ err = png.Encode(n, t)
+ if err != nil {
+ return fmt.Errorf("Failed to encode tiff as png for %s: %v\n", f, err)
+ }
r.Close()
err = os.Remove(f)
if err != nil {
- return fmt.Errorf("Failed to remove invalid image %s: %v", f, err)
+ return fmt.Errorf("Failed to remove original tiff %s: %v\n", f, err)
}
+ return nil
+ }
+
+ r.Close()
+ err = os.Remove(f)
+ if err != nil {
+ return fmt.Errorf("Failed to remove invalid image %s: %v", f, err)
}
return nil
}
-func startProcess(logger log.Logger, tessCommand string, bookdir string, bookname string, trainingName string, systess bool, savedir string, tessdir string) error {
- _, err := exec.Command(tessCommand, "--help").Output()
+func startProcess(ctx context.Context, logger *log.Logger, tessCommand string, bookdir string, bookname string, trainingName string, savedir string, tessdir string, nowipe bool, fullpdf bool) error {
+ cmd := exec.Command(tessCommand, "--help")
+ pipeline.HideCmd(cmd)
+ _, err := cmd.Output()
if err != nil {
errmsg := "Error, Can't run Tesseract\n"
errmsg += "Ensure that Tesseract is installed and available, or don't use the -systess flag.\n"
@@ -404,7 +531,7 @@ func startProcess(logger log.Logger, tessCommand string, bookdir string, booknam
}
var conn Pipeliner
- conn = &bookpipeline.LocalConn{Logger: &logger, TempDir: tempdir}
+ conn = &bookpipeline.LocalConn{Logger: logger, TempDir: tempdir}
conn.Log("Setting up session")
err = conn.Init()
@@ -415,14 +542,14 @@ func startProcess(logger log.Logger, tessCommand string, bookdir string, booknam
fmt.Printf("Copying book to pipeline\n")
- err = uploadbook(bookdir, bookname, conn)
+ err = uploadbook(ctx, bookdir, bookname, conn, nowipe)
if err != nil {
_ = os.RemoveAll(tempdir)
return fmt.Errorf("Error uploading book: %v", err)
}
fmt.Printf("Processing book\n")
- err = processbook(trainingName, tessCommand, conn)
+ err = processbook(ctx, trainingName, tessCommand, conn, fullpdf)
if err != nil {
_ = os.RemoveAll(tempdir)
return fmt.Errorf("Error processing book: %v", err)
@@ -444,18 +571,16 @@ func startProcess(logger log.Logger, tessCommand string, bookdir string, booknam
return fmt.Errorf("Error removing temporary directory %s: %v", tempdir, err)
}
- if !systess {
- err = os.RemoveAll(tessdir)
- if err != nil {
- return fmt.Errorf("Error removing tesseract directory %s: %v", tessdir, err)
- }
- }
-
hocrs, err := filepath.Glob(fmt.Sprintf("%s%s*.hocr", savedir, string(filepath.Separator)))
if err != nil {
return fmt.Errorf("Error looking for .hocr files: %v", err)
}
+ err = addFullTxt(hocrs, bookname)
+ if err != nil {
+ log.Fatalf("Error creating full txt version: %v", err)
+ }
+
for _, v := range hocrs {
err = addTxtVersion(v)
if err != nil {
@@ -471,11 +596,46 @@ func startProcess(logger log.Logger, tessCommand string, bookdir string, booknam
if err != nil {
log.Fatalf("Error moving hocr %s to hocr directory: %v", v, err)
}
+
+ pngname := strings.Replace(v, ".hocr", ".png", 1)
+ err = os.MkdirAll(filepath.Join(savedir, "png"), 0755)
+ if err != nil {
+ log.Fatalf("Error creating png directory: %v", err)
+ }
+
+ err = os.Rename(pngname, filepath.Join(savedir, "png", filepath.Base(pngname)))
+ if err != nil {
+ log.Fatalf("Error moving png %s to png directory: %v", pngname, err)
+ }
+
}
// For simplicity, remove .binarised.pdf and rename .colour.pdf to .pdf
- _ = os.Remove(filepath.Join(savedir, bookname+".binarised.pdf"))
- _ = os.Rename(filepath.Join(savedir, bookname+".colour.pdf"), filepath.Join(savedir, bookname+".pdf"))
+ // providing they both exist, otherwise just rename whichever exists
+ // to .pdf.
+ binpath := filepath.Join(savedir, bookname+".binarised.pdf")
+ colourpath := filepath.Join(savedir, bookname+".colour.pdf")
+ fullsizepath := filepath.Join(savedir, bookname+".original.pdf")
+ pdfpath := filepath.Join(savedir, bookname+" searchable.pdf")
+
+ // If full size pdf is requested, replace colour.pdf with it
+ if fullpdf {
+ _ = os.Rename(fullsizepath, colourpath)
+ }
+
+ _, err = os.Stat(binpath)
+ binexists := err == nil || os.IsExist(err)
+ _, err = os.Stat(colourpath)
+ colourexists := err == nil || os.IsExist(err)
+
+ if binexists && colourexists {
+ _ = os.Remove(binpath)
+ _ = os.Rename(colourpath, pdfpath)
+ } else if binexists {
+ _ = os.Rename(binpath, pdfpath)
+ } else if colourexists {
+ _ = os.Rename(colourpath, pdfpath)
+ }
return nil
}
@@ -506,21 +666,48 @@ func addTxtVersion(hocrfn string) error {
return nil
}
-func uploadbook(dir string, name string, conn Pipeliner) error {
+func addFullTxt(hocrs []string, bookname string) error {
+ if len(hocrs) == 0 {
+ return nil
+ }
+ var full string
+ for i, v := range hocrs {
+ t, err := hocr.GetText(v)
+ if err != nil {
+ return fmt.Errorf("Error getting text from hocr file %s: %v", v, err)
+ }
+ if i > 0 {
+ full += "\n"
+ }
+ full += t
+ }
+
+ dir := filepath.Dir(hocrs[0])
+ fn := filepath.Join(dir, bookname+".txt")
+ err := ioutil.WriteFile(fn, []byte(full), 0644)
+ if err != nil {
+ return fmt.Errorf("Error creating text file %s: %v", fn, err)
+ }
+
+ return nil
+}
+
+func uploadbook(ctx context.Context, dir string, name string, conn Pipeliner, nowipe bool) error {
_, err := os.Stat(dir)
if err != nil && !os.IsExist(err) {
return fmt.Errorf("Error: directory %s not found", dir)
}
- err = pipeline.CheckImages(dir)
+ err = pipeline.CheckImages(ctx, dir)
if err != nil {
return fmt.Errorf("Error with images in %s: %v", dir, err)
}
- err = pipeline.UploadImages(dir, name, conn)
+ err = pipeline.UploadImages(ctx, dir, name, conn)
if err != nil {
return fmt.Errorf("Error saving images to process from %s: %v", dir, err)
}
- qid := pipeline.DetectQueueType(dir, conn)
+ qid := pipeline.DetectQueueType(dir, conn, nowipe)
+ fmt.Printf("Uploading to queue %s\n", qid)
err = conn.AddToQueue(qid, name)
if err != nil {
@@ -531,9 +718,14 @@ func uploadbook(dir string, name string, conn Pipeliner) error {
}
func downloadbook(dir string, name string, conn Pipeliner) error {
- err := pipeline.DownloadBestPages(dir, name, conn, false)
+ err := pipeline.DownloadBestPages(dir, name, conn)
if err != nil {
- return fmt.Errorf("Error downloading best pages: %v", err)
+ return fmt.Errorf("No images found")
+ }
+
+ err = pipeline.DownloadBestPngs(dir, name, conn)
+ if err != nil {
+ return fmt.Errorf("No images found")
}
err = pipeline.DownloadPdfs(dir, name, conn)
@@ -549,17 +741,19 @@ func downloadbook(dir string, name string, conn Pipeliner) error {
return nil
}
-func processbook(training string, tesscmd string, conn Pipeliner) error {
+func processbook(ctx context.Context, training string, tesscmd string, conn Pipeliner, fullpdf bool) error {
origPattern := regexp.MustCompile(`[0-9]{4}.(jpg|png)$`)
wipePattern := regexp.MustCompile(`[0-9]{4,6}(.bin)?.(jpg|png)$`)
ocredPattern := regexp.MustCompile(`.hocr$`)
var checkPreQueue <-chan time.Time
+ var checkPreNoWipeQueue <-chan time.Time
var checkWipeQueue <-chan time.Time
var checkOCRPageQueue <-chan time.Time
var checkAnalyseQueue <-chan time.Time
var stopIfQuiet *time.Timer
checkPreQueue = time.After(0)
+ checkPreNoWipeQueue = time.After(0)
checkWipeQueue = time.After(0)
checkOCRPageQueue = time.After(0)
checkAnalyseQueue = time.After(0)
@@ -571,6 +765,27 @@ func processbook(training string, tesscmd string, conn Pipeliner) error {
for {
select {
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-checkPreNoWipeQueue:
+ msg, err := conn.CheckQueue(conn.PreNoWipeQueueId(), QueueTimeoutSecs)
+ checkPreNoWipeQueue = time.After(PauseBetweenChecks)
+ if err != nil {
+ return fmt.Errorf("Error checking preprocess no wipe queue: %v", err)
+ }
+ if msg.Handle == "" {
+ conn.Log("No message received on preprocess no wipe queue, sleeping")
+ continue
+ }
+ stopTimer(stopIfQuiet)
+ conn.Log("Message received on preprocess no wipe queue, processing", msg.Body)
+ fmt.Printf(" Preprocessing book (binarising only, no wiping)\n")
+ err = pipeline.ProcessBook(ctx, msg, conn, pipeline.Preprocess(thresholds, true), origPattern, conn.PreNoWipeQueueId(), conn.OCRPageQueueId())
+ resetTimer(stopIfQuiet, quietTime)
+ if err != nil {
+ return fmt.Errorf("Error during preprocess (no wipe): %v", err)
+ }
+ fmt.Printf(" OCRing pages ") // this is expected to be added to with dots by OCRPage output
case <-checkPreQueue:
msg, err := conn.CheckQueue(conn.PreQueueId(), QueueTimeoutSecs)
checkPreQueue = time.After(PauseBetweenChecks)
@@ -584,12 +799,12 @@ func processbook(training string, tesscmd string, conn Pipeliner) error {
stopTimer(stopIfQuiet)
conn.Log("Message received on preprocess queue, processing", msg.Body)
fmt.Printf(" Preprocessing book (binarising and wiping)\n")
- err = pipeline.ProcessBook(msg, conn, pipeline.Preprocess(thresholds), origPattern, conn.PreQueueId(), conn.OCRPageQueueId())
- fmt.Printf(" OCRing pages ") // this is expected to be added to with dots by OCRPage output
+ err = pipeline.ProcessBook(ctx, msg, conn, pipeline.Preprocess(thresholds, false), origPattern, conn.PreQueueId(), conn.OCRPageQueueId())
resetTimer(stopIfQuiet, quietTime)
if err != nil {
return fmt.Errorf("Error during preprocess: %v", err)
}
+ fmt.Printf(" OCRing pages ") // this is expected to be added to with dots by OCRPage output
case <-checkWipeQueue:
msg, err := conn.CheckQueue(conn.WipeQueueId(), QueueTimeoutSecs)
checkWipeQueue = time.After(PauseBetweenChecks)
@@ -603,12 +818,12 @@ func processbook(training string, tesscmd string, conn Pipeliner) error {
stopTimer(stopIfQuiet)
conn.Log("Message received on wipeonly queue, processing", msg.Body)
fmt.Printf(" Preprocessing book (wiping only)\n")
- err = pipeline.ProcessBook(msg, conn, pipeline.Wipe, wipePattern, conn.WipeQueueId(), conn.OCRPageQueueId())
- fmt.Printf(" OCRing pages ") // this is expected to be added to with dots by OCRPage output
+ err = pipeline.ProcessBook(ctx, msg, conn, pipeline.Wipe, wipePattern, conn.WipeQueueId(), conn.OCRPageQueueId())
resetTimer(stopIfQuiet, quietTime)
if err != nil {
return fmt.Errorf("Error during wipe: %v", err)
}
+ fmt.Printf(" OCRing pages ") // this is expected to be added to with dots by OCRPage output
case <-checkOCRPageQueue:
msg, err := conn.CheckQueue(conn.OCRPageQueueId(), QueueTimeoutSecs)
checkOCRPageQueue = time.After(PauseBetweenChecks)
@@ -624,7 +839,7 @@ func processbook(training string, tesscmd string, conn Pipeliner) error {
stopTimer(stopIfQuiet)
conn.Log("Message received on OCR Page queue, processing", msg.Body)
fmt.Printf(".")
- err = pipeline.OcrPage(msg, conn, pipeline.Ocr(training, tesscmd), conn.OCRPageQueueId(), conn.AnalyseQueueId())
+ err = pipeline.OcrPage(ctx, msg, conn, pipeline.Ocr(training, tesscmd), conn.OCRPageQueueId(), conn.AnalyseQueueId())
resetTimer(stopIfQuiet, quietTime)
if err != nil {
return fmt.Errorf("\nError during OCR Page process: %v", err)
@@ -642,7 +857,7 @@ func processbook(training string, tesscmd string, conn Pipeliner) error {
stopTimer(stopIfQuiet)
conn.Log("Message received on analyse queue, processing", msg.Body)
fmt.Printf("\n Analysing OCR and compiling PDFs\n")
- err = pipeline.ProcessBook(msg, conn, pipeline.Analyse(conn), ocredPattern, conn.AnalyseQueueId(), "")
+ err = pipeline.ProcessBook(ctx, msg, conn, pipeline.Analyse(conn, fullpdf), ocredPattern, conn.AnalyseQueueId(), "")
resetTimer(stopIfQuiet, quietTime)
if err != nil {
return fmt.Errorf("Error during analysis: %v", err)
diff --git a/cmd/rescribe/makefile b/cmd/rescribe/makefile
index aee2114..0cd7d7b 100644
--- a/cmd/rescribe/makefile
+++ b/cmd/rescribe/makefile
@@ -1,19 +1,77 @@
# See LICENSE file for copyright and license details.
+#
+# This is a set of make(1) rules to cross compile rescribe
+# from Linux to other architectures - as we use Fyne, CGO
+# is required, so we have to do more to cross compile than
+# just rely on the standard go tools. It relies on osxcross
+# being set up for the Mac builds, and mingw-w64 being
+# installed for the Windows build.
+#
+# The standard go tools work perfectly for native builds on
+# all architectures - note that "go generate" needs to be
+# run before building to download the dependencies which are
+# embedded.
-all: dist/linux/rescribe dist/darwin_amd64/rescribe dist/darwin_arm64/rescribe dist/windows/rescribe.exe
+# For osxcross, there are many versions of the MacOS SDK
+# that are too old or too new to build Rescribe correctly.
+# SDK 11.3, as extracted from XCode 12.5.1, seems to work
+# perfectly for us.
+OSXCROSSBIN=$(HOME)/src/osxcross/target/bin
-dist/linux/rescribe:
+EMBEDS=embed_darwin.go embed_darwin_amd64.go embed_darwin_arm64.go embed_linux.go embed_windows.go embed_other.go
+GODEPS=gui.go main.go $(EMBEDS)
+
+all: dist/linux/rescribe dist/linux/wayland/rescribe dist/darwin/rescribe.zip dist/windows/rescribe.exe
+
+dist/linux/rescribe: $(GODEPS)
+ go generate
mkdir -p dist/linux
- GOOS=linux GOARCH=amd64 go build -o $@ .
+ GOOS=linux GOARCH=amd64 go build -tags embed -o $@ .
+
+dist/linux/wayland/rescribe: $(GODEPS)
+ go generate
+ mkdir -p dist/linux
+ GOOS=linux GOARCH=amd64 go build -tags embed,wayland -o $@ .
+
+build/darwin_amd64/rescribe: $(GODEPS)
+ go generate
+ mkdir -p build/darwin_amd64
+ PATH="$(PATH):$(OSXCROSSBIN)" CC="o64-clang" CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -tags embed -o $@ .
-dist/darwin_amd64/rescribe:
- mkdir -p dist/darwin_amd64
- GOOS=darwin GOARCH=amd64 go build -o $@ .
+build/darwin_arm64/rescribe: $(GODEPS)
+ go generate
+ mkdir -p build/darwin_arm64
+ PATH="$(PATH):$(OSXCROSSBIN)" CC="oa64-clang" CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -tags embed -o $@ .
-dist/darwin_arm64/rescribe:
- mkdir -p dist/darwin_arm64
- GOOS=darwin GOARCH=arm64 go build -o $@ .
+build/darwin/rescribe: build/darwin_amd64/rescribe build/darwin_arm64/rescribe
+ mkdir -p build/darwin
+ PATH="$(PATH):$(OSXCROSSBIN)" lipo -create build/darwin_amd64/rescribe build/darwin_arm64/rescribe -output $@
-dist/windows/rescribe.exe:
+build/darwin/Rescribe.app: build/darwin/rescribe
+ go install fyne.io/fyne/v2/cmd/fyne@v2.1.2
+ fyne package --release --certificate Rescribe --id xyz.rescribe.rescribe --tags embed --name Rescribe --exe build/darwin/rescribe --os darwin --icon icon.png
+ codesign -s Rescribe Rescribe.app
+ mv Rescribe.app $@
+
+dist/darwin/rescribe.zip: build/darwin/Rescribe.app
+ mkdir -p dist/darwin
+ cd build/darwin; zip -r ../../dist/darwin/rescribe.zip Rescribe.app
+
+build/windows/rescribe-bin.exe: $(GODEPS)
+ go generate
+ mkdir -p build/windows
+ CC="x86_64-w64-mingw32-gcc" CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -tags embed -o $@ .
+
+dist/windows/rescribe.exe: build/windows/rescribe-bin.exe
mkdir -p dist/windows
- GOOS=windows GOARCH=386 go build -o $@ .
+ CC="x86_64-w64-mingw32-gcc" fyne package --tags embed --name Rescribe --exe build/windows/rescribe-bin.exe --os windows --icon icon.png
+ mv rescribe.exe $@
+
+# used for flatpak building
+modules.tar.xz: ../../go.mod
+ go mod vendor
+ cd ../.. && tar c vendor | xz > cmd/rescribe/$@
+
+clean:
+ rm -rf dist build
+ rm -rf ../../vendor
diff --git a/cmd/rescribe/testdata/fuzz/FuzzGetBookIdFromUrl/174f82f558636f2a b/cmd/rescribe/testdata/fuzz/FuzzGetBookIdFromUrl/174f82f558636f2a
new file mode 100644
index 0000000..1a7ed9c
--- /dev/null
+++ b/cmd/rescribe/testdata/fuzz/FuzzGetBookIdFromUrl/174f82f558636f2a
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("https://www0google\xf7/books/edition/_/")
diff --git a/cmd/rescribe/testdata/fuzz/FuzzGetBookIdFromUrl/60892155cf2f7963 b/cmd/rescribe/testdata/fuzz/FuzzGetBookIdFromUrl/60892155cf2f7963
new file mode 100644
index 0000000..b637539
--- /dev/null
+++ b/cmd/rescribe/testdata/fuzz/FuzzGetBookIdFromUrl/60892155cf2f7963
@@ -0,0 +1,2 @@
+go test fuzz v1
+string("https://Books.google\xc1&id=")
diff --git a/cmd/rescribe/xyz.rescribe.rescribe.appdata.xml b/cmd/rescribe/xyz.rescribe.rescribe.appdata.xml
new file mode 100644
index 0000000..98916b5
--- /dev/null
+++ b/cmd/rescribe/xyz.rescribe.rescribe.appdata.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="desktop-application">
+ <id>xyz.rescribe.rescribe</id>
+
+ <name>Rescribe</name>
+ <developer_name>Rescribe Ltd</developer_name>
+ <summary>High quality OCR for images, PDFs and Google Books.</summary>
+
+ <description><p>An easy-to-use desktop tool for OCR of images, PDFs and Google Books. It uses the Tesseract OCR engine, combined with modern and efficient preprocessing and analysis pipelines, to produce high quality output in plain text, hOCR and searchable PDF format. The tool has been built with a focus on OCR of historical printed works, but it includes modern language options and also works well on modern printed works.</p></description>
+
+ <screenshots>
+ <screenshot type="default"><image>https://rescribe.xyz/rescribe/screenshot-03.png</image></screenshot>
+ <screenshot><image>https://rescribe.xyz/rescribe/screenshot-04.png</image></screenshot>
+ </screenshots>
+
+ <url type="homepage">https://rescribe.xyz/rescribe</url>
+
+ <metadata_license>MIT</metadata_license>
+ <project_license>GPL-3.0</project_license>
+
+ <launchable type="desktop-id">xyz.rescribe.rescribe.desktop</launchable>
+
+ <content_rating type="oars-1.1" />
+
+ <releases>
+ <release version="1.2.0" date="2024-02-16" type="stable">
+ <description>
+ <p>Fixed bug with directories containing files with spaces causing the process to fail, added concatenated text output named bookname.txt, fixed selecting a custom training in flatpak build, fixed getgbook on arm64 MacOS, improved layout of log area to fill all available space in the window, improved readability of log area text.</p>
+ </description>
+ </release>
+ <release version="1.1.0" date="2023-02-13" type="stable">
+ <description>
+ <p>Improved PDF reading by adding support for embedded CCITT images. Improved PDF parsing to prevent a possible crash with bad PDF files. Improved error messages for unreadable PDFs. Improved GUI theme thanks to an update to Fyne.</p>
+ </description>
+ </release>
+ <release version="1.0.0" date="2022-03-22" type="stable">
+ <description>
+ <p>Thanks to our fabulous Kickstarter backers, lots of improvements! Added GUI, added PDF extractor, added Google Book downloader, created a single binary for OSX for M1 and amd64, added file renamer so page files no longer need a particular naming format, added option to disable page wiping, added option to create full size PDF.</p>
+ </description>
+ </release>
+ </releases>
+
+</component>
diff --git a/cmd/rescribe/xyz.rescribe.rescribe.desktop b/cmd/rescribe/xyz.rescribe.rescribe.desktop
new file mode 100644
index 0000000..331079f
--- /dev/null
+++ b/cmd/rescribe/xyz.rescribe.rescribe.desktop
@@ -0,0 +1,9 @@
+[Desktop Entry]
+Version=1.0
+Type=Application
+Name=Rescribe
+Comment=An easy-to-use desktop tool for performing OCR on image files, PDFs and Google Books.
+Categories=Office;Literature;OCR;Scanning;TextTools
+Icon=xyz.rescribe.rescribe
+Exec=rescribe
+Terminal=false
diff --git a/cmd/rescribe/xyz.rescribe.rescribe.yml b/cmd/rescribe/xyz.rescribe.rescribe.yml
new file mode 100644
index 0000000..bd8faf4
--- /dev/null
+++ b/cmd/rescribe/xyz.rescribe.rescribe.yml
@@ -0,0 +1,65 @@
+app-id: xyz.rescribe.rescribe
+runtime: org.freedesktop.Platform
+runtime-version: '23.08'
+sdk: org.freedesktop.Sdk
+sdk-extensions: org.freedesktop.Sdk.Extension.golang
+build-options:
+ append-path: /usr/lib/sdk/golang/bin
+finish-args:
+ - --socket=fallback-x11
+ - --share=ipc # needed for X11
+ - --socket=wayland
+ - --device=dri # OpenGL
+ - --share=network # Used for google book downloading
+ - --filesystem=home
+command: rescribe
+modules:
+ - name: rescribe
+ buildsystem: simple
+ build-commands:
+ - cd cmd/rescribe && go build .
+ - cd cmd/rescribe && go build -tags wayland -o rescribe-wayland .
+ - install -Dm00755 cmd/rescribe/rescribe $FLATPAK_DEST/bin/rescribe-bin
+ - install -Dm00755 cmd/rescribe/rescribe-wayland $FLATPAK_DEST/bin/rescribe-bin-wayland
+ - install -Dm00644 cmd/rescribe/icon.256.png $FLATPAK_DEST/share/icons/hicolor/256x256/apps/xyz.rescribe.rescribe.png
+ - install -Dm00644 cmd/rescribe/xyz.rescribe.rescribe.desktop $FLATPAK_DEST/share/applications/xyz.rescribe.rescribe.desktop
+ - install -Dm00644 cmd/rescribe/xyz.rescribe.rescribe.appdata.xml $FLATPAK_DEST/share/appdata/xyz.rescribe.rescribe.appdata.xml
+ - printf '#!/bin/sh\nexport TMPDIR=$XDG_RUNTIME_DIR\nbin=rescribe-bin\ntest -n "$WAYLAND_DISPLAY" && bin=rescribe-bin-wayland\n"$bin" -gbookcmd "/app/bin/getgbook" -tesscmd "/app/bin/tesseract" -t "/app/share/tessdata/rescribev9_fast.traineddata"\n' > $FLATPAK_DEST/bin/rescribe
+ - chmod 755 $FLATPAK_DEST/bin/rescribe
+ - mkdir -p $FLATPAK_DEST/share/tessdata
+ - cp -r tessdata/* $FLATPAK_DEST/share/tessdata/
+ sources:
+ - type: git
+ url: https://github.com/rescribe/bookpipeline
+ tag: v1.2.0
+ commit: bf6e4762191aee0c27242f1d9cbbc2b8972c12f9
+ - type: archive
+ url: https://rescribe.xyz/rescribe/modules-20240206-d2399a.tar.xz
+ sha256: 682820d4cb6129c564cf8df494dc12d35ab059ed99dba34c0b3d6260f7fc30fb
+ strip-components: 0
+ - type: archive
+ url: https://rescribe.xyz/rescribe/embeds/tessdata.20220322.zip
+ sha256: 725fd570a3c3dc0eba9463248ce47a8646db8bafb198d428d6bb8f0be18540ee
+ strip-components: 0
+ dest: tessdata
+ - name: leptonica
+ sources:
+ - type: git
+ url: https://github.com/DanBloomberg/leptonica
+ tag: 1.82.0
+ commit: f4138265b390f1921b9891d6669674d3157887d8
+ - name: tesseract-ocr
+ sources:
+ - type: git
+ url: https://github.com/tesseract-ocr/tesseract
+ tag: 5.2.0
+ commit: 5ad5325a0aa8effc47ca033625b6a51682f82767
+ - name: getxbook
+ buildsystem: simple
+ build-commands:
+ - make PREFIX=$FLATPAK_DEST install
+ sources:
+ - type: git
+ url: https://git.njw.name/getxbook.git
+ commit: c770a86cca74f3b6235000c77c2ab74487e2ac2a
+ disable-shallow-clone: true
diff --git a/doc.go b/doc.go
index bd4da11..9624177 100644
--- a/doc.go
+++ b/doc.go
@@ -131,6 +131,17 @@ which have been prebinarised.
example message: APolishGentleman_MemoirByAdamKruczkiewicz
example message: APolishGentleman_MemoirByAdamKruczkiewicz rescribefrav2
+queuePreNoWipe
+
+This queue works the same as queuePreProc, except that it doesn'T
+wipe the pages, only runs the binarisation. It is designed for books
+which don't have tricky gutters or similar noise around the edges, but
+do have marginal content which might be inadventently removed by the
+wiper.
+
+ example message: APolishGentleman_MemoirByAdamKruczkiewicz
+ example message: APolishGentleman_MemoirByAdamKruczkiewicz rescribefrav2
+
queueOcrPage
This queue contains the path of individual pages, optionally followed by
diff --git a/go.mod b/go.mod
index f12564b..c17f4c7 100644
--- a/go.mod
+++ b/go.mod
@@ -1,15 +1,48 @@
module rescribe.xyz/bookpipeline
-go 1.16
+go 1.21
require (
- fyne.io/fyne/v2 v2.1.1
+ fyne.io/fyne/v2 v2.4.3
github.com/aws/aws-sdk-go v1.40.6
- github.com/nickjwhite/gofpdf v1.12.7-0.20210817123627-3cbaeb9797ef
+ github.com/nickjwhite/gofpdf v1.12.7-0.20240307131705-b017c7c7e41b
github.com/wcharczuk/go-chart/v2 v2.1.0
- golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d
- golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70 // indirect
- rescribe.xyz/pdf v0.1.3
- rescribe.xyz/preproc v0.4.2
+ golang.org/x/image v0.11.0
+ rescribe.xyz/pdf v0.1.6
+ rescribe.xyz/preproc v0.4.3
rescribe.xyz/utils v0.1.3
)
+
+require (
+ fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e // indirect
+ github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/fredbi/uri v1.0.0 // indirect
+ github.com/fsnotify/fsnotify v1.6.0 // indirect
+ github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe // indirect
+ github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 // indirect
+ github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 // indirect
+ github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 // indirect
+ github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b // indirect
+ github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 // indirect
+ github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a // indirect
+ github.com/godbus/dbus/v5 v5.1.0 // indirect
+ github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
+ github.com/gopherjs/gopherjs v1.17.2 // indirect
+ github.com/jmespath/go-jmespath v0.4.0 // indirect
+ github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e // indirect
+ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
+ github.com/pmezard/go-difflib v1.0.0 // indirect
+ github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
+ github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
+ github.com/stretchr/testify v1.8.4 // indirect
+ github.com/tevino/abool v1.2.0 // indirect
+ github.com/yuin/goldmark v1.5.5 // indirect
+ golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda // indirect
+ golang.org/x/net v0.17.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
+ golang.org/x/text v0.13.0 // indirect
+ gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 // indirect
+ rescribe.xyz/integral v0.6.1 // indirect
+)
diff --git a/go.sum b/go.sum
index eb92ea0..de18345 100644
--- a/go.sum
+++ b/go.sum
@@ -1,121 +1,697 @@
-fyne.io/fyne/v2 v2.1.1 h1:3p39SwQ/rBiYODVYI4ggTuwMufWYmqaRMJvXTFg7jSw=
-fyne.io/fyne/v2 v2.1.1/go.mod h1:c1vwI38Ebd0dAdxVa6H1Pj6/+cK1xtDy61+I31g+s14=
+cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
+cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU=
+cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU=
+cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY=
+cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc=
+cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0=
+cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To=
+cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4=
+cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M=
+cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc=
+cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk=
+cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs=
+cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc=
+cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY=
+cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI=
+cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk=
+cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg=
+cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8=
+cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0=
+cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=
+cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE=
+cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc=
+cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg=
+cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc=
+cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ=
+cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE=
+cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk=
+cloud.google.com/go/firestore v1.1.0/go.mod h1:ulACoGHTpvq5r8rxGJ4ddJZBZqakUQqClKRT5SZwBmk=
+cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I=
+cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw=
+cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA=
+cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU=
+cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw=
+cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos=
+cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk=
+cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
+cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
+dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
+fyne.io/fyne/v2 v2.4.3 h1:v2wncjEAcwXZ8UNmTCWTGL9+sGyPc5RuzBvM96GcC78=
+fyne.io/fyne/v2 v2.4.3/go.mod h1:1h3BKxmQYRJlr2g+RGVxedzr6vLVQ/AJmFWcF9CJnoQ=
+fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e h1:Hvs+kW2VwCzNToF3FmnIAzmivNgrclwPgoUdVSrjkP8=
+fyne.io/systray v1.10.1-0.20231115130155-104f5ef7839e/go.mod h1:oM2AQqGJ1AMo4nNqZFYU8xYygSBZkW2hmdJ7n4yjedE=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
-github.com/BurntSushi/toml v0.4.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
-github.com/Kodeworks/golang-image-ico v0.0.0-20141118225523-73f0f4cfade9/go.mod h1:7uhhqiBaR4CpN0k9rMjOtjpcfGd6DG2m04zQxKnWQ0I=
-github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c=
+github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
+github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY=
+github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o=
+github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY=
+github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
github.com/aws/aws-sdk-go v1.40.6 h1:JCQfi5MD8cW0PCAzr88hj9tj4BdEJkAy8EyAJ6c8I/k=
github.com/aws/aws-sdk-go v1.40.6/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
+github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
+github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
-github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
+github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
+github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
+github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
+github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
+github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
+github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
+github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk=
+github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
+github.com/cpuguy83/go-md2man/v2 v2.0.0/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3 h1:FDqhDm7pcsLhhWl1QtD8vlzI4mm59llRvNzrFg6/LAA=
-github.com/fredbi/uri v0.0.0-20181227131451-3dcfdacbaaf3/go.mod h1:CzM2G82Q9BDUvMTGHnXf/6OExw/Dz2ivDj48nVg7Lg8=
-github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4=
+github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
+github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
+github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk=
+github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
+github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
+github.com/fredbi/uri v1.0.0 h1:s4QwUAZ8fz+mbTsukND+4V5f+mJ/wjaTokwstGUAemg=
+github.com/fredbi/uri v1.0.0/go.mod h1:1xC40RnIOGCaQzswaOvrzvG/3M3F0hyDVb3aO/1iGy0=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
-github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f h1:s0O46d8fPwk9kU4k1jj76wBquMVETx7uveQD9MCIQoU=
-github.com/go-gl/gl v0.0.0-20210813123233-e4099ee2221f/go.mod h1:wjpnOv6ONl2SuJSxqCPVaPZibGFdSci9HFocT9qtVYM=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb h1:T6gaWBvRzJjuOrdCtg8fXXjKai2xSDqWTcKFUPuw8Tw=
-github.com/go-gl/glfw/v3.3/glfw v0.0.0-20210410170116-ea3d685f79fb/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
-github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
-github.com/godbus/dbus/v5 v5.0.4 h1:9349emZab16e7zQvpmsbtjc18ykshndd8y2PG3sgJbA=
+github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY=
+github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw=
+github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe h1:A/wiwvQ0CAjPkuJytaD+SsXkPU0asQ+guQEIg1BJGX4=
+github.com/fyne-io/gl-js v0.0.0-20220119005834-d2da28d9ccfe/go.mod h1:d4clgH0/GrRwWjRzJJQXxT/h1TyuNSfF/X64zb/3Ggg=
+github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504 h1:+31CdF/okdokeFNoy9L/2PccG3JFidQT3ev64/r4pYU=
+github.com/fyne-io/glfw-js v0.0.0-20220120001248-ee7290d23504/go.mod h1:gLRWYfYnMA9TONeppRSikMdXlHQ97xVsPojddUv3b/E=
+github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2 h1:hnLq+55b7Zh7/2IRzWCpiTcAvjv/P8ERF+N7+xXbZhk=
+github.com/fyne-io/image v0.0.0-20220602074514-4956b0afb3d2/go.mod h1:eO7W361vmlPOrykIg+Rsh1SZ3tQBaOsfzZhsIOb/Lm0=
+github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
+github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6 h1:zDw5v7qm4yH7N8C8uWd+8Ii9rROdgWxQuGoJ9WDXxfk=
+github.com/go-gl/gl v0.0.0-20211210172815-726fda9656d6/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
+github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b h1:GgabKamyOYguHqHjSkDACcgoPIz3w0Dis/zJ1wyHHHU=
+github.com/go-gl/glfw/v3.3/glfw v0.0.0-20221017161538-93cebf72946b/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
+github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8 h1:VkKnvzbvHqgEfm351rfr8Uclu5fnwq8HP2ximUzJsBM=
+github.com/go-text/render v0.0.0-20230619120952-35bccb6164b8/go.mod h1:h29xCucjNsDcYb7+0rJokxVwYAq+9kQ19WiFuBKkYtc=
+github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a h1:VjN8ttdfklC0dnAdKbZqGNESdERUxtE3l8a/4Grgarc=
+github.com/go-text/typesetting v0.0.0-20230616162802-9c17dd34aa4a/go.mod h1:evDBbvNR/KaVFZ2ZlDSOWWXIUKq0wCOEtzLxRM8SG3k=
+github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22 h1:LBQTFxP2MfsyEDqSKmUBZaDuDHN1vpqDyOZjcqS7MYI=
+github.com/go-text/typesetting-utils v0.0.0-20230616150549-2a7df14b6a22/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
-github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff h1:W71vTCKoxtdXgnm1ECDFkfQnpdqAO00zzGXLA5yaEX8=
-github.com/goki/freetype v0.0.0-20181231101311-fa8a33aabaff/go.mod h1:wfqRWLHRBsRgkp5dmbG56SA0DmVtwrF5N3oPdI8t+Aw=
+github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
+github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
+github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
-github.com/jackmordaunt/icns v0.0.0-20181231085925-4f16af745526/go.mod h1:UQkeMHVoNcyXYq9otUupF7/h/2tmHlhrS2zw7ZVvUqc=
+github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
+github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
+github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
+github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y=
+github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
+github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
+github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8=
+github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
+github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
+github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk=
+github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
+github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
+github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
+github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
+github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
+github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
+github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM=
+github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ=
+github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
+github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
+github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
+github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0=
+github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
+github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM=
+github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE=
+github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
+github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
+github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
+github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
+github.com/gopherjs/gopherjs v0.0.0-20211219123610-ec9572f70e60/go.mod h1:cz9oNYuRUWGdHmLF2IodMLkAhcPtXeULvcBNagUrxTI=
+github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g=
+github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k=
+github.com/goxjs/gl v0.0.0-20210104184919-e3fafc6f8f2a/go.mod h1:dy/f2gjY09hwVfIyATps4G2ai7/hLwLkc5TrPqONuXY=
+github.com/goxjs/glfw v0.0.0-20191126052801-d2efb5f20838/go.mod h1:oS8P8gVOT4ywTcjV6wZlOU4GuVFQ8F5328KY3MJ79CY=
+github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw=
+github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
+github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
+github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
+github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
+github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
+github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM=
+github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU=
+github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU=
+github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4=
+github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro=
+github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90=
+github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64=
+github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ=
+github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I=
+github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
+github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
-github.com/josephspurrier/goversioninfo v0.0.0-20200309025242-14b0ab84c6ca/go.mod h1:eJTEwMjXb7kZ633hO3Ln9mBUCOjX2+FlTljvpl9SYdE=
+github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
+github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
+github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk=
+github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e h1:LvL4XsI70QxOGHed6yhQtAU34Kx3Qq2wwBzGFKY8zKk=
+github.com/jsummers/gobmp v0.0.0-20151104160322-e2ba15ffa76e/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
+github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
+github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
+github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
+github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
-github.com/lucor/goinfo v0.0.0-20210802170112-c078a2b0f08b/go.mod h1:PRq09yoB+Q2OJReAmwzKivcYyremnibWGbK7WfftHzc=
-github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
-github.com/nickjwhite/gofpdf v1.12.7-0.20210817123627-3cbaeb9797ef h1:Pq7OiIQ0gWrNQr12kRIvAH7qEr3i6hfSfd0smfwGgC0=
-github.com/nickjwhite/gofpdf v1.12.7-0.20210817123627-3cbaeb9797ef/go.mod h1:HHhz41M4FT6ogJi7KMknlu/sEJd6halot657/rJwzVM=
+github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60=
+github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
+github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
+github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
+github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
+github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
+github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg=
+github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY=
+github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
+github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
+github.com/neelance/sourcemap v0.0.0-20200213170602-2833bce08e4c/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
+github.com/nickjwhite/gofpdf v1.12.7-0.20240307131705-b017c7c7e41b h1:Td9/25hvPktBd9Z4hFjBvrCHwtbVRyr6AoLtTLFXqpg=
+github.com/nickjwhite/gofpdf v1.12.7-0.20240307131705-b017c7c7e41b/go.mod h1:HHhz41M4FT6ogJi7KMknlu/sEJd6halot657/rJwzVM=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
+github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
+github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI=
-github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
+github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
+github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
+github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts=
+github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
+github.com/shurcooL/go v0.0.0-20200502201357-93f07166e636/go.mod h1:TDJrrUr11Vxrven61rcy3hJMUqaf/CLWYhHNPmT14Lk=
+github.com/shurcooL/httpfs v0.0.0-20190707220628-8d4bc4ba7749/go.mod h1:ZY1cvUeJuFPAdZ/B6v7RHavJWZn2YPVFQ1OSXhCGOkg=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
-github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
-github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
-github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564 h1:HunZiaEKNGVdhTRQOVpMmj5MQnGnv+e8uZNu3xFLgyM=
-github.com/srwiley/oksvg v0.0.0-20200311192757-870daf9aa564/go.mod h1:afMbS0qvv1m5tfENCwnOdZGOF8RGR/FsZ7bvBxQGZG4=
-github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9 h1:m59mIOBO4kfcNCEzJNy71UkeF4XIx2EVmL9KLwDQdmM=
-github.com/srwiley/rasterx v0.0.0-20200120212402-85cb7272f5e9/go.mod h1:mvWM0+15UqyrFKqdRjY6LuAVJR0HOVhJlEgZ5JWtSWU=
+github.com/shurcooL/vfsgen v0.0.0-20200824052919-0d455de96546/go.mod h1:TrYk7fJVaAttu97ZZKrO9UbRa8izdowaMIZcxYMbVaw=
+github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
+github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
+github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
+github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v1.2.1/go.mod h1:ExllRjgxM/piMAM+3tAZvg8fsklGAf3tPfi+i8t68Nk=
+github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.8.1/go.mod h1:o0Pch8wJ9BVSWGQMbra6iw0oQ5oktSIBaujf1rJH9Ns=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
+github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
+github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
-github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
+github.com/tevino/abool v1.2.0 h1:heAkClL8H6w+mK5md9dzsuohKeXHUpY7Vw0ZCKW+huA=
+github.com/tevino/abool v1.2.0/go.mod h1:qc66Pna1RiIsPa7O4Egxxs9OqkuxDX55zznh9K07Tzg=
github.com/wcharczuk/go-chart/v2 v2.1.0 h1:tY2slqVQ6bN+yHSnDYwZebLQFkphK4WNrVwnt7CJZ2I=
github.com/wcharczuk/go-chart/v2 v2.1.0/go.mod h1:yx7MvAVNcP/kN9lKXM/NTce4au4DFN99j6i1OwDclNA=
+github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
+github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
-github.com/yuin/goldmark v1.3.8 h1:Nw158Q8QN+CPgTmVRByhVwapp8Mm1e2blinhmx4wx5E=
-github.com/yuin/goldmark v1.3.8/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
+github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+github.com/yuin/goldmark v1.5.5 h1:IJznPe8wOzfIKETmMkd06F8nXkmlhaHqFRM9l1hAGsU=
+github.com/yuin/goldmark v1.5.5/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
+go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs=
+go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g=
+go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=
+go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
+go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
+go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
+go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk=
+go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E=
+go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU=
+go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo=
+golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
+golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
+golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8=
+golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
+golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek=
+golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
+golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
+golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
+golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
+golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20200927104501-e162460cd6b5/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
-golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d h1:RNPAfi2nHY7C2srAV8A49jpsYr0ADedCk1wq6fTMTvs=
-golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM=
+golang.org/x/image v0.11.0 h1:ds2RoQvBvYTiJkwpSFDwCcDFNX7DqjL2WsUgTNk0Ooo=
+golang.org/x/image v0.11.0/go.mod h1:bglhjqbqVuEb9e9+eNR45Jfu7D+T4Qan+NhQk8Ck2P8=
+golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
+golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
+golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
+golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs=
+golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
+golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE=
+golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o=
+golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee/go.mod h1:pe2sM7Uk+2Su1y7u/6Z8KJ24D7lepUjFZbhFOrmDfuQ=
+golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda h1:O+EUvnBNPwI4eLthn8W5K+cS8zQZfgTABPLNm6Bna34=
+golang.org/x/mobile v0.0.0-20230531173138-3c911d8e3eda/go.mod h1:aAjjkJNdrh3PMckS4B10TGS2nag27cbKR1y2BpUxsiY=
+golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc=
+golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY=
+golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
+golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
+golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
+golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
+golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
+golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
+golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
+golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
-golang.org/x/net v0.0.0-20210614182718-04defd469f4e h1:XpT3nA5TvE525Ne3hInMh6+GETgn27Zfm9dxsThnX2Q=
golang.org/x/net v0.0.0-20210614182718-04defd469f4e/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
+golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
+golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
+golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
+golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
+golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/oauth2 v0.0.0-20210402161424-2e8d93401602/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
+golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70 h1:SeSEfdIxyvwGJliREIJhRPPXvW6sDlLT+UQ3B0hD0NA=
-golang.org/x/sys v0.0.0-20211023085530-d6a326fbbf70/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
+golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
+golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
+golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=
+golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
+golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
+golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
+golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
+golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
+golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
+golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
+golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
+golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
+golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
+golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc=
+golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
+golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw=
+golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
+golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
+golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA=
+golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE=
+golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
+golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
+golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.8-0.20211022200916-316ba0b74098/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
+golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE=
+google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M=
+google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg=
+google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI=
+google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE=
+google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE=
+google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM=
+google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc=
+google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg=
+google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE=
+google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8=
+google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU=
+google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94=
+google.golang.org/api v0.44.0/go.mod h1:EBOGZqzyhtvMDoxwS97ctnh0zUmYY6CxqXsc1AvkYD8=
+google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
+google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
+google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0=
+google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
+google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
+google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
+google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
+google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8=
+google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc=
+google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA=
+google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U=
+google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
+google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA=
+google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no=
+google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A=
+google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0=
+google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
+google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38=
+google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM=
+google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
+google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
+google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
+google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60=
+google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk=
+google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
+google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0=
+google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc=
+google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8=
+google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU=
+google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM=
+google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
+google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
+google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
+google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
+google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
+google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
+google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4=
+google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
+gopkg.in/ini.v1 v1.62.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
-rescribe.xyz/integral v0.6.0 h1:CLF3sQ6th/OuG+/rp/lLR+AGOT4R7tG3IiUjSLKsriw=
-rescribe.xyz/integral v0.6.0/go.mod h1:gKJq4UaVn17RsMsUasEMcJDkTkwqeb6AzPIJtwcUipg=
-rescribe.xyz/pdf v0.1.3 h1:Fl4HHQPfkIUJs8WIkpjCm8yGu6Wd1TIDLZgXhVy8Pdk=
-rescribe.xyz/pdf v0.1.3/go.mod h1:fIia5YlYagNbBARPP2JXDoXXR5zd14Us5RkaKXUz7Nw=
-rescribe.xyz/preproc v0.4.2 h1:aX6rOf6ha3UNcHM0oHuY1MQi7ZwYj+46OxhTcptAI4E=
-rescribe.xyz/preproc v0.4.2/go.mod h1:LJe+rQ9cAxn/29cVK5l6X1hH1ZWRAI1Bs73yDGjvT4A=
+gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
+gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2 h1:oomkgU6VaQDsV6qZby2uz1Lap0eXmku8+2em3A/l700=
+honnef.co/go/js/dom v0.0.0-20210725211120-f030747120f2/go.mod h1:sUMDUKNB2ZcVjt92UnLy3cdGs+wDAcrPdV3JP6sVgA4=
+honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
+honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
+honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
+rescribe.xyz/integral v0.6.1 h1:0+4lajevAWO33sxnS33+URUbSalAh2Diti7ZWV/DbqU=
+rescribe.xyz/integral v0.6.1/go.mod h1:gKJq4UaVn17RsMsUasEMcJDkTkwqeb6AzPIJtwcUipg=
+rescribe.xyz/pdf v0.1.6 h1:Fpa/xGCwh9jZfKZ48xUw6Pdpz4lhYm/WsfGVmUmtBOc=
+rescribe.xyz/pdf v0.1.6/go.mod h1:fIia5YlYagNbBARPP2JXDoXXR5zd14Us5RkaKXUz7Nw=
+rescribe.xyz/preproc v0.4.3 h1:DySrMD7uqJyd1dAw9eYbaTCHPMPFTE3cVcy05BmkLN4=
+rescribe.xyz/preproc v0.4.3/go.mod h1:wY+4/ZMEtMb2ifew57+0ClfMNH01ctiiiyGeh0zcfUY=
rescribe.xyz/utils v0.1.3 h1:2rlHbUjAGXy/xgtmUb6Y7Kbpxl3qkwtWzkFUQ/cOaIA=
rescribe.xyz/utils v0.1.3/go.mod h1:4L2vClYUFklsXggN0CUyP/alcgzLNRT0dMpMfEiVbX8=
+rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
+rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
+rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/internal/pipeline/get.go b/internal/pipeline/get.go
index 960c8f7..8fac060 100644
--- a/internal/pipeline/get.go
+++ b/internal/pipeline/get.go
@@ -12,7 +12,7 @@ import (
"strings"
)
-func DownloadBestPages(dir string, name string, conn Downloader, pluspngs bool) error {
+func DownloadBestPages(dir string, name string, conn Downloader) error {
key := filepath.Join(name, "best")
fn := filepath.Join(dir, "best")
err := conn.Download(conn.WIPStorageId(), key, fn)
@@ -35,12 +35,23 @@ func DownloadBestPages(dir string, name string, conn Downloader, pluspngs bool)
return fmt.Errorf("Failed to download file %s: %v", key, err)
}
}
+ return nil
+}
- if !pluspngs {
- return nil
+func DownloadBestPngs(dir string, name string, conn Downloader) error {
+ key := filepath.Join(name, "best")
+ fn := filepath.Join(dir, "best")
+ err := conn.Download(conn.WIPStorageId(), key, fn)
+ if err != nil {
+ return fmt.Errorf("Failed to download 'best' file: %v", err)
}
+ f, err := os.Open(fn)
+ if err != nil {
+ return fmt.Errorf("Failed to open best file: %v", err)
+ }
+ defer f.Close()
- s = bufio.NewScanner(f)
+ s := bufio.NewScanner(f)
for s.Scan() {
imgname := strings.Replace(s.Text(), ".hocr", ".png", 1)
key = filepath.Join(name, imgname)
@@ -55,14 +66,22 @@ func DownloadBestPages(dir string, name string, conn Downloader, pluspngs bool)
}
func DownloadPdfs(dir string, name string, conn Downloader) error {
- for _, suffix := range []string{".colour.pdf", ".binarised.pdf"} {
+ anydone := false
+ errmsg := ""
+ for _, suffix := range []string{".colour.pdf", ".binarised.pdf", ".original.pdf"} {
key := filepath.Join(name, name+suffix)
fn := filepath.Join(dir, name+suffix)
err := conn.Download(conn.WIPStorageId(), key, fn)
if err != nil {
- return fmt.Errorf("Failed to download PDF %s: %v", key, err)
+ _ = os.Remove(fn)
+ errmsg += fmt.Sprintf("Failed to download PDF %s: %v\n", key, err)
+ } else {
+ anydone = true
}
}
+ if anydone == false {
+ return fmt.Errorf("No PDFs could be downloaded, error(s): %v", errmsg)
+ }
return nil
}
@@ -71,7 +90,8 @@ func DownloadAnalyses(dir string, name string, conn Downloader) error {
key := filepath.Join(name, a)
fn := filepath.Join(dir, a)
err := conn.Download(conn.WIPStorageId(), key, fn)
- if err != nil {
+ // ignore errors with graph.png, as it will not exist in the case of a 1 page book
+ if err != nil && a != "graph.png" {
return fmt.Errorf("Failed to download analysis file %s: %v", key, err)
}
}
diff --git a/internal/pipeline/pipeline.go b/internal/pipeline/pipeline.go
index e1a2c40..d8beeb9 100644
--- a/internal/pipeline/pipeline.go
+++ b/internal/pipeline/pipeline.go
@@ -11,6 +11,7 @@ package pipeline
import (
"bytes"
+ "context"
"fmt"
"io/ioutil"
"log"
@@ -62,6 +63,7 @@ type Queuer interface {
DelFromQueue(url string, handle string) error
Log(v ...interface{})
OCRPageQueueId() string
+ PreNoWipeQueueId() string
PreQueueId() string
QueueHeartbeat(msg bookpipeline.Qmsg, qurl string, duration int64) (bookpipeline.Qmsg, error)
WipeQueueId() string
@@ -71,6 +73,7 @@ type UploadQueuer interface {
Log(v ...interface{})
Upload(bucket string, key string, path string) error
WIPStorageId() string
+ PreNoWipeQueueId() string
PreQueueId() string
WipeQueueId() string
OCRPageQueueId() string
@@ -92,6 +95,7 @@ type Pipeliner interface {
ListObjects(bucket string, prefix string) ([]string, error)
Log(v ...interface{})
OCRPageQueueId() string
+ PreNoWipeQueueId() string
PreQueueId() string
QueueHeartbeat(msg bookpipeline.Qmsg, qurl string, duration int64) (bookpipeline.Qmsg, error)
Upload(bucket string, key string, path string) error
@@ -129,8 +133,17 @@ func GetMailSettings() (mailSettings, error) {
// dir, putting each successfully downloaded file name into the
// process channel. If an error occurs it is sent to the errc channel
// and the function returns early.
-func download(dl chan string, process chan string, conn Downloader, dir string, errc chan error, logger *log.Logger) {
+func download(ctx context.Context, dl chan string, process chan string, conn Downloader, dir string, errc chan error, logger *log.Logger) {
for key := range dl {
+ select {
+ case <-ctx.Done():
+ for range dl {
+ } // consume the rest of the receiving channel so it isn't blocked
+ errc <- ctx.Err()
+ close(process)
+ return
+ default:
+ }
fn := filepath.Join(dir, filepath.Base(key))
logger.Println("Downloading", key)
err := conn.Download(conn.WIPStorageId(), key, fn)
@@ -151,8 +164,16 @@ func download(dl chan string, process chan string, conn Downloader, dir string,
// once it has been successfully uploaded. The done channel is
// then written to to signal completion. If an error occurs it
// is sent to the errc channel and the function returns early.
-func up(c chan string, done chan bool, conn Uploader, bookname string, errc chan error, logger *log.Logger) {
+func up(ctx context.Context, c chan string, done chan bool, conn Uploader, bookname string, errc chan error, logger *log.Logger) {
for path := range c {
+ select {
+ case <-ctx.Done():
+ for range c {
+ } // consume the rest of the receiving channel so it isn't blocked
+ errc <- ctx.Err()
+ return
+ default:
+ }
name := filepath.Base(path)
key := bookname + "/" + name
logger.Println("Uploading", key)
@@ -181,8 +202,16 @@ func up(c chan string, done chan bool, conn Uploader, bookname string, errc chan
// added to the toQueue once it has been uploaded. The done channel
// is then written to to signal completion. If an error occurs it
// is sent to the errc channel and the function returns early.
-func upAndQueue(c chan string, done chan bool, toQueue string, conn UploadQueuer, bookname string, training string, errc chan error, logger *log.Logger) {
+func upAndQueue(ctx context.Context, c chan string, done chan bool, toQueue string, conn UploadQueuer, bookname string, training string, errc chan error, logger *log.Logger) {
for path := range c {
+ select {
+ case <-ctx.Done():
+ for range c {
+ } // consume the rest of the receiving channel so it isn't blocked
+ errc <- ctx.Err()
+ return
+ default:
+ }
name := filepath.Base(path)
key := bookname + "/" + name
logger.Println("Uploading", key)
@@ -213,11 +242,19 @@ func upAndQueue(c chan string, done chan bool, toQueue string, conn UploadQueuer
done <- true
}
-func Preprocess(thresholds []float64) func(chan string, chan string, chan error, *log.Logger) {
- return func(pre chan string, up chan string, errc chan error, logger *log.Logger) {
+func Preprocess(thresholds []float64, nowipe bool) func(context.Context, chan string, chan string, chan error, *log.Logger) {
+ return func(ctx context.Context, pre chan string, up chan string, errc chan error, logger *log.Logger) {
for path := range pre {
+ select {
+ case <-ctx.Done():
+ for range pre {
+ } // consume the rest of the receiving channel so it isn't blocked
+ errc <- ctx.Err()
+ return
+ default:
+ }
logger.Println("Preprocessing", path)
- done, err := preproc.PreProcMulti(path, thresholds, "binary", 0, true, 5, 30, 120, 30)
+ done, err := preproc.PreProcMulti(path, thresholds, "binary", 0, !nowipe, 5, 30, 120, 30)
if err != nil {
for range pre {
} // consume the rest of the receiving channel so it isn't blocked
@@ -233,8 +270,16 @@ func Preprocess(thresholds []float64) func(chan string, chan string, chan error,
}
}
-func Wipe(towipe chan string, up chan string, errc chan error, logger *log.Logger) {
+func Wipe(ctx context.Context, towipe chan string, up chan string, errc chan error, logger *log.Logger) {
for path := range towipe {
+ select {
+ case <-ctx.Done():
+ for range towipe {
+ } // consume the rest of the receiving channel so it isn't blocked
+ errc <- ctx.Err()
+ return
+ default:
+ }
logger.Println("Wiping", path)
s := strings.Split(path, ".")
base := strings.Join(s[:len(s)-1], "")
@@ -251,15 +296,24 @@ func Wipe(towipe chan string, up chan string, errc chan error, logger *log.Logge
close(up)
}
-func Ocr(training string, tesscmd string) func(chan string, chan string, chan error, *log.Logger) {
- return func(toocr chan string, up chan string, errc chan error, logger *log.Logger) {
+func Ocr(training string, tesscmd string) func(context.Context, chan string, chan string, chan error, *log.Logger) {
+ return func(ctx context.Context, toocr chan string, up chan string, errc chan error, logger *log.Logger) {
if tesscmd == "" {
tesscmd = "tesseract"
}
for path := range toocr {
+ select {
+ case <-ctx.Done():
+ for range toocr {
+ } // consume the rest of the receiving channel so it isn't blocked
+ errc <- ctx.Err()
+ return
+ default:
+ }
logger.Println("OCRing", path)
name := strings.Replace(path, ".png", "", 1)
cmd := exec.Command(tesscmd, "-l", training, path, name, "-c", "tessedit_create_hocr=1", "-c", "hocr_font_info=0")
+ HideCmd(cmd)
var stdout, stderr bytes.Buffer
cmd.Stdout = &stdout
cmd.Stderr = &stderr
@@ -276,13 +330,21 @@ func Ocr(training string, tesscmd string) func(chan string, chan string, chan er
}
}
-func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Logger) {
- return func(toanalyse chan string, up chan string, errc chan error, logger *log.Logger) {
+func Analyse(conn Downloader, mkfullpdf bool) func(context.Context, chan string, chan string, chan error, *log.Logger) {
+ return func(ctx context.Context, toanalyse chan string, up chan string, errc chan error, logger *log.Logger) {
confs := make(map[string][]*bookpipeline.Conf)
bestconfs := make(map[string]*bookpipeline.Conf)
savedir := ""
for path := range toanalyse {
+ select {
+ case <-ctx.Done():
+ for range toanalyse {
+ } // consume the rest of the receiving channel so it isn't blocked
+ errc <- ctx.Err()
+ return
+ default:
+ }
if savedir == "" {
savedir = filepath.Dir(path)
}
@@ -316,6 +378,13 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
}
defer f.Close()
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
logger.Println("Finding best confidence for each page, and saving all confidences")
for base, conf := range confs {
var best float64
@@ -334,6 +403,13 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
f.Close()
up <- fn
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
logger.Println("Creating best file listing the best file for each page")
fn = filepath.Join(savedir, "best")
f, err = os.Create(fn)
@@ -354,6 +430,13 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
}
sort.Strings(pgs)
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
logger.Println("Downloading binarised and original images to create PDFs")
bookname, err := filepath.Rel(os.TempDir(), savedir)
if err != nil {
@@ -374,6 +457,13 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
}
binhascontent, colourhascontent := false, false
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
var colourimgs, binimgs []pageimg
for _, pg := range pgs {
@@ -393,6 +483,13 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
}
for _, pg := range binimgs {
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
logger.Println("Downloading binarised page to add to PDF", pg.img)
err := conn.Download(conn.WIPStorageId(), bookname+"/"+pg.img, filepath.Join(savedir, pg.img))
if err != nil {
@@ -412,6 +509,13 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
}
}
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
if binhascontent {
fn = filepath.Join(savedir, bookname+".binarised.pdf")
err = binarisedpdf.Save(fn)
@@ -423,6 +527,13 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
}
for _, pg := range colourimgs {
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
logger.Println("Downloading colour page to add to PDF", pg.img)
colourfn := pg.img
err = conn.Download(conn.WIPStorageId(), bookname+"/"+colourfn, filepath.Join(savedir, colourfn))
@@ -448,6 +559,14 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
}
}
}
+
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
if colourhascontent {
fn = filepath.Join(savedir, bookname+".colour.pdf")
err = colourpdf.Save(fn)
@@ -458,6 +577,71 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
up <- fn
}
+ if mkfullpdf {
+ fullsizepdf := new(bookpipeline.Fpdf)
+ err = fullsizepdf.Setup()
+ if err != nil {
+ errc <- fmt.Errorf("Failed to set up PDF: %s", err)
+ return
+ }
+ for _, pg := range colourimgs {
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
+ logger.Println("Downloading colour page to add to PDF", pg.img)
+ colourfn := pg.img
+ err = conn.Download(conn.WIPStorageId(), bookname+"/"+colourfn, filepath.Join(savedir, colourfn))
+ if err != nil {
+ colourfn = strings.Replace(pg.img, ".jpg", ".png", 1)
+ logger.Println("Download failed; trying", colourfn)
+ err = conn.Download(conn.WIPStorageId(), bookname+"/"+colourfn, filepath.Join(savedir, colourfn))
+ if err != nil {
+ logger.Println("Download failed; skipping page", pg.img)
+ }
+ }
+ if err == nil {
+ err = fullsizepdf.AddPage(filepath.Join(savedir, colourfn), filepath.Join(savedir, pg.hocr), false)
+ if err != nil {
+ errc <- fmt.Errorf("Failed to add page %s to PDF: %s", pg.img, err)
+ return
+ }
+ err = os.Remove(filepath.Join(savedir, colourfn))
+ if err != nil {
+ errc <- err
+ return
+ }
+ }
+ }
+
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
+ if colourhascontent {
+ fn = filepath.Join(savedir, bookname+".original.pdf")
+ err = fullsizepdf.Save(fn)
+ if err != nil {
+ errc <- fmt.Errorf("Failed to save full size pdf: %s", err)
+ return
+ }
+ up <- fn
+ }
+ }
+
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
logger.Println("Creating graph")
fn = filepath.Join(savedir, "graph.png")
f, err = os.Create(fn)
@@ -467,11 +651,24 @@ func Analyse(conn Downloader) func(chan string, chan string, chan error, *log.Lo
}
defer f.Close()
err = bookpipeline.Graph(bestconfs, filepath.Base(savedir), f)
+ if err != nil {
+ _ = os.Remove(fn)
+ }
if err != nil && err.Error() != "Not enough valid confidences" {
errc <- fmt.Errorf("Error rendering graph: %s", err)
return
}
- up <- fn
+
+ select {
+ case <-ctx.Done():
+ errc <- ctx.Err()
+ return
+ default:
+ }
+
+ if err == nil {
+ up <- fn
+ }
close(up)
}
@@ -541,7 +738,7 @@ func allOCRed(bookname string, conn Lister) bool {
// OcrPage OCRs a page based on a message. It may make sense to
// roll this back into processBook (on which it is based) once
// working well.
-func OcrPage(msg bookpipeline.Qmsg, conn Pipeliner, process func(chan string, chan string, chan error, *log.Logger), fromQueue string, toQueue string) error {
+func OcrPage(ctx context.Context, msg bookpipeline.Qmsg, conn Pipeliner, process func(context.Context, chan string, chan string, chan error, *log.Logger), fromQueue string, toQueue string) error {
dl := make(chan string)
msgc := make(chan bookpipeline.Qmsg)
processc := make(chan string)
@@ -565,19 +762,23 @@ func OcrPage(msg bookpipeline.Qmsg, conn Pipeliner, process func(chan string, ch
go heartbeat(conn, t, msg, fromQueue, msgc, errc)
// these functions will do their jobs when their channels have data
- go download(dl, processc, conn, d, errc, conn.GetLogger())
- go process(processc, upc, errc, conn.GetLogger())
- go up(upc, done, conn, bookname, errc, conn.GetLogger())
+ go download(ctx, dl, processc, conn, d, errc, conn.GetLogger())
+ go process(ctx, processc, upc, errc, conn.GetLogger())
+ go up(ctx, upc, done, conn, bookname, errc, conn.GetLogger())
dl <- msgparts[0]
close(dl)
- // wait for either the done or errc channel to be sent to
+ // wait for either the done or errc channels to be sent to
select {
case err = <-errc:
t.Stop()
_ = os.RemoveAll(d)
return err
+ case <-ctx.Done():
+ t.Stop()
+ _ = os.RemoveAll(d)
+ return ctx.Err()
case <-done:
}
@@ -619,7 +820,7 @@ func OcrPage(msg bookpipeline.Qmsg, conn Pipeliner, process func(chan string, ch
return nil
}
-func ProcessBook(msg bookpipeline.Qmsg, conn Pipeliner, process func(chan string, chan string, chan error, *log.Logger), match *regexp.Regexp, fromQueue string, toQueue string) error {
+func ProcessBook(ctx context.Context, msg bookpipeline.Qmsg, conn Pipeliner, process func(context.Context, chan string, chan string, chan error, *log.Logger), match *regexp.Regexp, fromQueue string, toQueue string) error {
dl := make(chan string)
msgc := make(chan bookpipeline.Qmsg)
processc := make(chan string)
@@ -645,12 +846,12 @@ func ProcessBook(msg bookpipeline.Qmsg, conn Pipeliner, process func(chan string
go heartbeat(conn, t, msg, fromQueue, msgc, errc)
// these functions will do their jobs when their channels have data
- go download(dl, processc, conn, d, errc, conn.GetLogger())
- go process(processc, upc, errc, conn.GetLogger())
+ go download(ctx, dl, processc, conn, d, errc, conn.GetLogger())
+ go process(ctx, processc, upc, errc, conn.GetLogger())
if toQueue == conn.OCRPageQueueId() {
- go upAndQueue(upc, done, toQueue, conn, bookname, training, errc, conn.GetLogger())
+ go upAndQueue(ctx, upc, done, toQueue, conn, bookname, training, errc, conn.GetLogger())
} else {
- go up(upc, done, conn, bookname, errc, conn.GetLogger())
+ go up(ctx, upc, done, conn, bookname, errc, conn.GetLogger())
}
conn.Log("Getting list of objects to download")
@@ -682,7 +883,7 @@ func ProcessBook(msg bookpipeline.Qmsg, conn Pipeliner, process func(chan string
// complete, and will fill the ocrpage queue with parts which succeeded
// on each run, so in that case it's better to delete the message from
// the queue and notify us.
- if fromQueue == conn.PreQueueId() || fromQueue == conn.WipeQueueId() {
+ if fromQueue == conn.PreQueueId() || fromQueue == conn.WipeQueueId() || fromQueue == conn.PreNoWipeQueueId() {
conn.Log("Deleting message from queue due to a bad error", fromQueue)
err2 := conn.DelFromQueue(fromQueue, msg.Handle)
if err2 != nil {
@@ -711,6 +912,10 @@ func ProcessBook(msg bookpipeline.Qmsg, conn Pipeliner, process func(chan string
}
}
return err
+ case <-ctx.Done():
+ t.Stop()
+ _ = os.RemoveAll(d)
+ return ctx.Err()
case <-done:
}
diff --git a/internal/pipeline/put.go b/internal/pipeline/put.go
index d44f74f..fed04f8 100644
--- a/internal/pipeline/put.go
+++ b/internal/pipeline/put.go
@@ -5,6 +5,7 @@
package pipeline
import (
+ "context"
"fmt"
"image"
_ "image/jpeg"
@@ -43,16 +44,25 @@ func (f fileWalk) Walk(path string, info os.FileInfo, err error) error {
// CheckImages checks that all files with a ".jpg" or ".png" suffix
// in a directory are images that can be decoded (skipping dotfiles)
-func CheckImages(dir string) error {
+func CheckImages(ctx context.Context, dir string) error {
checker := make(fileWalk)
go func() {
_ = filepath.Walk(dir, checker.Walk)
close(checker)
}()
+ n := 0
for path := range checker {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
suffix := filepath.Ext(path)
lsuffix := strings.ToLower(suffix)
+ if lsuffix == ".jpeg" {
+ lsuffix = ".jpg"
+ }
if lsuffix != ".jpg" && lsuffix != ".png" {
continue
}
@@ -64,6 +74,11 @@ func CheckImages(dir string) error {
if err != nil {
return fmt.Errorf("Decoding image %s failed: %v", path, err)
}
+ n++
+ }
+
+ if n == 0 {
+ return fmt.Errorf("No images found")
}
return nil
@@ -71,7 +86,10 @@ func CheckImages(dir string) error {
// DetectQueueType detects which queue to use based on the preponderance
// of files of a particular extension in a directory
-func DetectQueueType(dir string, conn Queuer) string {
+func DetectQueueType(dir string, conn Queuer, nowipe bool) string {
+ if nowipe {
+ return conn.PreNoWipeQueueId()
+ }
pngdirs, _ := filepath.Glob(dir + "/*.png")
jpgdirs, _ := filepath.Glob(dir + "/*.jpg")
pngcount := len(pngdirs)
@@ -89,7 +107,7 @@ func DetectQueueType(dir string, conn Queuer) string {
// slash. It also appends all file names with sequential numbers, like
// 0001, to ensure they are appropriately named for further processing
// in the pipeline.
-func UploadImages(dir string, bookname string, conn Uploader) error {
+func UploadImages(ctx context.Context, dir string, bookname string, conn Uploader) error {
files, err := ioutil.ReadDir(dir)
if err != nil {
fmt.Errorf("Failed to read directory %s: %v", dir, err)
@@ -97,11 +115,19 @@ func UploadImages(dir string, bookname string, conn Uploader) error {
filenum := 0
for _, file := range files {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
if file.IsDir() {
continue
}
origsuffix := filepath.Ext(file.Name())
lsuffix := strings.ToLower(origsuffix)
+ if lsuffix == ".jpeg" {
+ lsuffix = ".jpg"
+ }
if lsuffix != ".jpg" && lsuffix != ".png" {
continue
}
@@ -109,7 +135,8 @@ func UploadImages(dir string, bookname string, conn Uploader) error {
origbase := strings.TrimSuffix(origname, origsuffix)
origpath := filepath.Join(dir, origname)
- newname := fmt.Sprintf("%s_%04d%s", origbase, filenum, origsuffix)
+ safebase := strings.ReplaceAll(origbase, " ", "_")
+ newname := fmt.Sprintf("%s_%04d%s", safebase, filenum, lsuffix)
err = conn.Upload(conn.WIPStorageId(), filepath.Join(bookname, newname), origpath)
if err != nil {
return fmt.Errorf("Failed to upload %s: %v", origpath, err)
diff --git a/internal/pipeline/util.go b/internal/pipeline/util.go
new file mode 100644
index 0000000..092a9ee
--- /dev/null
+++ b/internal/pipeline/util.go
@@ -0,0 +1,16 @@
+// Copyright 2022 Nick White.
+// Use of this source code is governed by the GPLv3
+// license that can be found in the LICENSE file.
+
+// +build !windows
+
+package pipeline
+
+import (
+ "os/exec"
+)
+
+// HideCmd adds a flag to hide any console window from being
+// displayed, if necessary for the platform
+func HideCmd(cmd *exec.Cmd) {
+}
diff --git a/internal/pipeline/util_windows.go b/internal/pipeline/util_windows.go
new file mode 100644
index 0000000..08c321e
--- /dev/null
+++ b/internal/pipeline/util_windows.go
@@ -0,0 +1,16 @@
+// Copyright 2022 Nick White.
+// Use of this source code is governed by the GPLv3
+// license that can be found in the LICENSE file.
+
+package pipeline
+
+import (
+ "os/exec"
+ "syscall"
+)
+
+// HideCmd adds a flag to hide any console window from being
+// displayed, if necessary for the platform
+func HideCmd(cmd *exec.Cmd) {
+ cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
+}
diff --git a/local.go b/local.go
index 615f9a6..24c7562 100644
--- a/local.go
+++ b/local.go
@@ -16,6 +16,7 @@ import (
)
const qidPre = "queuePre"
+const qidPreNoWipe = "queuePreNoWipe"
const qidWipe = "queueWipe"
const qidOCR = "queueOCR"
const qidAnalyse = "queueAnalyse"
@@ -114,6 +115,10 @@ func (a *LocalConn) PreQueueId() string {
return qidPre
}
+func (a *LocalConn) PreNoWipeQueueId() string {
+ return qidPreNoWipe
+}
+
func (a *LocalConn) WipeQueueId() string {
return qidWipe
}