Skip to content

Commit 8b61f70

Browse files
authored
Image inference (#217)
1 parent 43c0638 commit 8b61f70

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

90 files changed

+4641
-789
lines changed

cmd/rwp/cmd/helpers/inference.go

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package helpers
2+
3+
import (
4+
"errors"
5+
6+
"github.com/readium/go-toolkit/pkg/streamer"
7+
)
8+
9+
type InferA11yMetadata streamer.InferA11yMetadata
10+
11+
// String is used both by fmt.Print and by Cobra in help text
12+
func (e *InferA11yMetadata) String() string {
13+
if e == nil {
14+
return "no"
15+
}
16+
switch *e {
17+
case InferA11yMetadata(streamer.InferA11yMetadataMerged):
18+
return "merged"
19+
case InferA11yMetadata(streamer.InferA11yMetadataSplit):
20+
return "split"
21+
default:
22+
return "no"
23+
}
24+
}
25+
26+
func (e *InferA11yMetadata) Set(v string) error {
27+
switch v {
28+
case "no":
29+
*e = InferA11yMetadata(streamer.InferA11yMetadataNo)
30+
case "merged":
31+
*e = InferA11yMetadata(streamer.InferA11yMetadataMerged)
32+
case "split":
33+
*e = InferA11yMetadata(streamer.InferA11yMetadataSplit)
34+
default:
35+
return errors.New(`must be one of "no", "merged", or "split"`)
36+
}
37+
return nil
38+
}
39+
40+
// Type is only used in help text.
41+
func (e *InferA11yMetadata) Type() string {
42+
return "string"
43+
}

cmd/rwp/cmd/helpers/inspector.go

+51
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
package helpers
2+
3+
import (
4+
"io/fs"
5+
6+
"github.com/pkg/errors"
7+
"github.com/readium/go-toolkit/pkg/analyzer"
8+
"github.com/readium/go-toolkit/pkg/manifest"
9+
)
10+
11+
type ImageInspector struct {
12+
Filesystem fs.FS
13+
Algorithms []manifest.HashAlgorithm
14+
err error
15+
}
16+
17+
func (n *ImageInspector) Error() error {
18+
return n.err
19+
}
20+
21+
// TransformHREF implements ManifestTransformer
22+
func (n *ImageInspector) TransformHREF(href manifest.HREF) manifest.HREF {
23+
// Identity
24+
return href
25+
}
26+
27+
// TransformLink implements ManifestTransformer
28+
func (n *ImageInspector) TransformLink(link manifest.Link) manifest.Link {
29+
if n.err != nil || link.MediaType == nil || !link.MediaType.IsBitmap() {
30+
return link
31+
}
32+
33+
newLink, err := analyzer.Image(n.Filesystem, link, n.Algorithms)
34+
if err != nil {
35+
n.err = errors.Wrap(err, "failed inspecting image "+link.Href.String())
36+
return link
37+
}
38+
return *newLink
39+
}
40+
41+
// TransformManifest implements ManifestTransformer
42+
func (n *ImageInspector) TransformManifest(manifest manifest.Manifest) manifest.Manifest {
43+
// Identity
44+
return manifest
45+
}
46+
47+
// TransformMetadata implements ManifestTransformer
48+
func (n *ImageInspector) TransformMetadata(metadata manifest.Metadata) manifest.Metadata {
49+
// Identity
50+
return metadata
51+
}

cmd/rwp/cmd/manifest.go

+45-39
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,38 @@
11
package cmd
22

33
import (
4+
"context"
45
"encoding/json"
5-
"errors"
66
"fmt"
77
"path/filepath"
88

9+
"github.com/pkg/errors"
10+
"github.com/readium/go-toolkit/cmd/rwp/cmd/helpers"
911
"github.com/readium/go-toolkit/pkg/asset"
12+
"github.com/readium/go-toolkit/pkg/fetcher"
13+
"github.com/readium/go-toolkit/pkg/manifest"
1014
"github.com/readium/go-toolkit/pkg/streamer"
15+
"github.com/readium/go-toolkit/pkg/util/url"
1116
"github.com/spf13/cobra"
1217
)
1318

1419
// Indentation used to pretty-print.
1520
var indentFlag string
1621

1722
// Infer accessibility metadata.
18-
var inferA11yFlag InferA11yMetadata
23+
var inferA11yFlag helpers.InferA11yMetadata
1924

2025
// Infer the number of pages from the generated position list.
2126
var inferPageCountFlag bool
2227

28+
/*var inferIgnoreImageHashesFlag []string
29+
30+
var inferIgnoreImageDirectoryFlag string*/
31+
32+
var hash []string
33+
34+
var inspectImagesFlag bool
35+
2336
var manifestCmd = &cobra.Command{
2437
Use: "manifest <pub-path>",
2538
Short: "Generate a Readium Web Publication Manifest for a publication",
@@ -53,17 +66,42 @@ Examples:
5366
// occurs.
5467
cmd.SilenceUsage = true
5568

56-
path := filepath.Clean(args[0])
69+
path, err := url.FromFilepath(filepath.Clean(args[0]))
70+
if err != nil {
71+
return fmt.Errorf("failed creating URL from filepath: %w", err)
72+
}
5773
pub, err := streamer.New(streamer.Config{
5874
InferA11yMetadata: streamer.InferA11yMetadata(inferA11yFlag),
5975
InferPageCount: inferPageCountFlag,
6076
}).Open(
77+
context.TODO(),
6178
asset.File(path), "",
6279
)
6380
if err != nil {
6481
return fmt.Errorf("failed opening %s: %w", path, err)
6582
}
6683

84+
if inspectImagesFlag {
85+
hashAlgorithms := make([]manifest.HashAlgorithm, len(hash))
86+
for i, h := range hash {
87+
hashAlgorithms[i] = manifest.HashAlgorithm(h)
88+
}
89+
inspector := &helpers.ImageInspector{
90+
Algorithms: hashAlgorithms,
91+
Filesystem: fetcher.ToFS(context.TODO(), pub.Fetcher),
92+
}
93+
94+
// Inspect publication files and overwrite the links
95+
pub.Manifest.ReadingOrder = pub.Manifest.ReadingOrder.Copy(inspector)
96+
if inspector.Error() != nil {
97+
return fmt.Errorf("failed inspecting images in reading order: %w", inspector.Error())
98+
}
99+
pub.Manifest.Resources = pub.Manifest.Resources.Copy(inspector)
100+
if inspector.Error() != nil {
101+
return fmt.Errorf("failed inspecting images in resources: %w", inspector.Error())
102+
}
103+
}
104+
67105
var jsonBytes []byte
68106
if indentFlag == "" {
69107
jsonBytes, err = json.Marshal(pub.Manifest)
@@ -84,40 +122,8 @@ func init() {
84122
manifestCmd.Flags().StringVarP(&indentFlag, "indent", "i", "", "Indentation used to pretty-print")
85123
manifestCmd.Flags().Var(&inferA11yFlag, "infer-a11y", "Infer accessibility metadata: no, merged, split")
86124
manifestCmd.Flags().BoolVar(&inferPageCountFlag, "infer-page-count", false, "Infer the number of pages from the generated position list.")
87-
}
88-
89-
type InferA11yMetadata streamer.InferA11yMetadata
90-
91-
// String is used both by fmt.Print and by Cobra in help text
92-
func (e *InferA11yMetadata) String() string {
93-
if e == nil {
94-
return "no"
95-
}
96-
switch *e {
97-
case InferA11yMetadata(streamer.InferA11yMetadataMerged):
98-
return "merged"
99-
case InferA11yMetadata(streamer.InferA11yMetadataSplit):
100-
return "split"
101-
default:
102-
return "no"
103-
}
104-
}
105-
106-
func (e *InferA11yMetadata) Set(v string) error {
107-
switch v {
108-
case "no":
109-
*e = InferA11yMetadata(streamer.InferA11yMetadataNo)
110-
case "merged":
111-
*e = InferA11yMetadata(streamer.InferA11yMetadataMerged)
112-
case "split":
113-
*e = InferA11yMetadata(streamer.InferA11yMetadataSplit)
114-
default:
115-
return errors.New(`must be one of "no", "merged", or "split"`)
116-
}
117-
return nil
118-
}
119-
120-
// Type is only used in help text.
121-
func (e *InferA11yMetadata) Type() string {
122-
return "string"
125+
manifestCmd.Flags().StringSliceVar(&hash, "hash", []string{string(manifest.HashAlgorithmSHA256), string(manifest.HashAlgorithmMD5)}, "Hashes to use when enhancing links, such as with image inspection. Note visual hashes are more computationally expensive. Acceptable values: sha256,md5,phash-dct,https://blurha.sh")
126+
manifestCmd.Flags().BoolVar(&inspectImagesFlag, "inspect-images", false, "Inspect images in the manifest. Their links will be enhanced with size, width and height, and hashes")
127+
// manifestCmd.Flags().StringSliceVar(&inferIgnoreImageHashesFlag, "infer-a11y-ignore-image-hashes", nil, "Ignore the given hashes when inferring textual accessibility. Hashes are in the format <algorithm>:<base64 value>, separated by commas.")
128+
// manifestCmd.Flags().StringVar(&inferIgnoreImageDirectoryFlag, "infer-a11y-ignore-image-dir", "", "Ignore the images in a given directory when inferring textual accessibility.")
123129
}

cmd/rwp/cmd/serve.go

+86-1
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,27 @@
11
package cmd
22

33
import (
4+
"context"
45
"errors"
56
"fmt"
7+
"log"
68
"net/http"
79
"os"
810
"path/filepath"
911
"time"
1012

1113
"log/slog"
1214

15+
"cloud.google.com/go/storage"
16+
"github.com/aws/aws-sdk-go-v2/aws"
17+
"github.com/aws/aws-sdk-go-v2/config"
18+
"github.com/aws/aws-sdk-go-v2/credentials"
19+
"github.com/aws/aws-sdk-go-v2/service/s3"
1320
"github.com/readium/go-toolkit/cmd/rwp/cmd/serve"
21+
"github.com/readium/go-toolkit/cmd/rwp/cmd/serve/client"
1422
"github.com/readium/go-toolkit/pkg/streamer"
1523
"github.com/spf13/cobra"
24+
"google.golang.org/api/option"
1625
)
1726

1827
var debugFlag bool
@@ -21,6 +30,19 @@ var bindAddressFlag string
2130

2231
var bindPortFlag uint16
2332

33+
// Cloud-related flags
34+
var s3EndpointFlag string
35+
var s3RegionFlag string
36+
var s3AccessKeyFlag string
37+
var s3SecretKeyFlag string
38+
39+
var httpAuthorizationFlag string
40+
41+
var remoteArchiveTimeoutFlag uint32
42+
var remoteArchiveCacheSize uint32
43+
var remoteArchiveCacheCount uint32
44+
var remoteArchiveCacheAll uint32
45+
2446
var serveCmd = &cobra.Command{
2547
Use: "serve <directory>",
2648
Short: "Start a local HTTP server, serving a specified directory of publications",
@@ -74,12 +96,64 @@ to the internet except for testing/debugging purposes.`,
7496
slog.SetLogLoggerLevel(slog.LevelInfo)
7597
}
7698

99+
// Set up remote publication retrieval clients
100+
remote := serve.Remote{}
101+
102+
// S3
103+
options := []func(*config.LoadOptions) error{
104+
config.WithRegion(s3RegionFlag),
105+
config.WithRequestChecksumCalculation(0),
106+
config.WithResponseChecksumValidation(0),
107+
// TODO: look into custom HTTP client, user-agent
108+
}
109+
if s3AccessKeyFlag != "" && s3SecretKeyFlag != "" {
110+
options = append(options, config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(s3AccessKeyFlag, s3SecretKeyFlag, "")))
111+
}
112+
cfg, err := config.LoadDefaultConfig(context.Background(), options...)
113+
if err != nil {
114+
log.Fatal(err)
115+
}
116+
_, err = cfg.Credentials.Retrieve(context.Background())
117+
if err == nil {
118+
remote.S3 = s3.NewFromConfig(cfg, func(o *s3.Options) {
119+
if s3EndpointFlag != "" {
120+
o.BaseEndpoint = aws.String(s3EndpointFlag)
121+
}
122+
})
123+
} else {
124+
slog.Warn("S3 credentials retrieval failed, S3 support will be disabled", "error", err)
125+
}
126+
127+
// GCS
128+
opts := []option.ClientOption{
129+
option.WithScopes(storage.ScopeReadOnly),
130+
storage.WithJSONReads(),
131+
// option.WithUserAgent(TODO),
132+
// TODO: look into more efficient transport (HTTP client)
133+
}
134+
remote.GCS, err = storage.NewClient(context.Background(), opts...)
135+
if err != nil {
136+
slog.Warn("GCS client creation failed, GCS support will be disabled", "error", err)
137+
}
138+
139+
remote.HTTP, err = client.NewHTTPClient(httpAuthorizationFlag)
140+
if err != nil {
141+
slog.Warn("HTTP client creation failed, HTTP support will be disabled", "error", err)
142+
}
143+
144+
// Remote archive streaming tweaks
145+
remote.Config.CacheCountThreshold = int64(remoteArchiveCacheCount)
146+
remote.Config.CacheSizeThreshold = int64(remoteArchiveCacheSize)
147+
remote.Config.Timeout = time.Duration(remoteArchiveTimeoutFlag) * time.Second
148+
remote.Config.CacheAllThreshold = int64(remoteArchiveCacheAll)
149+
150+
// Create server
77151
pubServer := serve.NewServer(serve.ServerConfig{
78152
Debug: debugFlag,
79153
BaseDirectory: path,
80154
JSONIndent: indentFlag,
81155
InferA11yMetadata: streamer.InferA11yMetadata(inferA11yFlag),
82-
})
156+
}, remote)
83157

84158
bind := fmt.Sprintf("%s:%d", bindAddressFlag, bindPortFlag)
85159
httpServer := &http.Server{
@@ -109,4 +183,15 @@ func init() {
109183
serveCmd.Flags().Var(&inferA11yFlag, "infer-a11y", "Infer accessibility metadata: no, merged, split")
110184
serveCmd.Flags().BoolVarP(&debugFlag, "debug", "d", false, "Enable debug mode")
111185

186+
serveCmd.Flags().StringVar(&s3EndpointFlag, "s3-endpoint", "", "Custom S3 endpoint URL")
187+
serveCmd.Flags().StringVar(&s3RegionFlag, "s3-region", "auto", "S3 region")
188+
serveCmd.Flags().StringVar(&s3AccessKeyFlag, "s3-access-key", "", "S3 access key")
189+
serveCmd.Flags().StringVar(&s3SecretKeyFlag, "s3-secret-key", "", "S3 secret key")
190+
191+
serveCmd.Flags().StringVar(&httpAuthorizationFlag, "http-authorization", "", "HTTP authorization header value (e.g. 'Bearer <token>' or 'Basic <base64-credentials>')")
192+
193+
serveCmd.Flags().Uint32Var(&remoteArchiveTimeoutFlag, "remote-archive-timeout", 60, "Timeout for remote archive requests (in seconds)")
194+
serveCmd.Flags().Uint32Var(&remoteArchiveCacheSize, "remote-archive-cache-size", 1024*1024, "Max size of items in an archive that can be cached (in bytes)")
195+
serveCmd.Flags().Uint32Var(&remoteArchiveCacheCount, "remote-archive-cache-count", 64, "Max number of items in an archive that can be cached")
196+
serveCmd.Flags().Uint32Var(&remoteArchiveCacheAll, "remote-archive-cache-all", 1024*1024, "Archives this size or less (in bytes) will be cached in full")
112197
}

0 commit comments

Comments
 (0)