Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 15 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.

**Warning:** Features marked as *alpha* may change or be removed in a future release without notice. Use with caution.

## [0.12.0] - 2025-10-10

All services are now hidden by default. This mainly affects implementers creating webservers, but you also shouldn't need to manually remove services just to produce clean manifest output. To restore previous functionality, set the `streamer.Config`'s `AddServiceLinks` to `true`

### Added

- ServicesBuilder now has a convenience function `Services` to get the names of all services currently in the builder
- ServicesBuilder now has a `ExposeLinks` and `HideLinks` function to toggle the exposure of a service via the links that get added to the WebPub manifest, as well as access to the service via its well-known link path. By default, services are **private**
- `streamer.Config` has a new property, `AddServiceLinks`. Setting this property is equivalent to calling the aforementioned `ExposeLinks` function for every service

### Changed

- `ServiceFactory` now has a required `public` property. This lets a service expose itself via the `Get` and `Links` function if set to true. This also means that by default, all services are now "private". That means they will not be added to manifests as links, or callable by said link's path. They are still directly accessible in Go code using e.g. `Publication.FindService`, and then directly calling their functions by casting them to the correct service type (see e.g. `Publication.Positions`)

## [0.11.0] - 2025-07-30

The WebPub data the toolkit parses and provides has been updated to more closely match the latest WebPub spec. Pay close attention to these changes if you depend on the WebPub output in reading systems/libraries that use an older version of the spec!
Expand Down Expand Up @@ -77,7 +91,7 @@ The WebPub data the toolkit parses and provides has been updated to more closely

### Changed

- In order to support remote streaming, a lot of APIs have been altered to accept a `context.Context` as the first parameter, to provide implementors with the ability to e.g. cancel a request to fetch a resource.
- In order to support remote streaming, a lot of APIs have been altered to accept a `context.Context` as the first parameter, to provide implementers with the ability to e.g. cancel a request to fetch a resource.
- `ReadAsString`, `ReadAsJSON`, and `ReadAsXML` functions have been removed from `Resource` and are instead available as helper functions.

## [0.8.1] - 2025-02-24
Expand Down
10 changes: 9 additions & 1 deletion pkg/parser/epub/media_overlay_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import (
)

func MediaOverlayFactory() pub.ServiceFactory {
return func(context pub.Context) pub.Service {
return func(context pub.Context, public bool) pub.Service {
// Process reading order to find and replace SMIL alternates
smilMap := make(map[string]manifest.Link)
var smilIndexes []string
Expand Down Expand Up @@ -49,11 +49,13 @@ func MediaOverlayFactory() pub.ServiceFactory {
fetcher: context.Fetcher,
originalSmilAlternates: smilMap,
originalSmilIndexes: smilIndexes,
public: public,
}
}
}

type MediaOverlayService struct {
public bool
fetcher fetcher.Fetcher
originalSmilAlternates map[string]manifest.Link
originalSmilIndexes []string
Expand All @@ -66,6 +68,9 @@ func (s *MediaOverlayService) Close() {
}

func (s *MediaOverlayService) Links() manifest.LinkList {
if !s.public {
return nil
}
return manifest.LinkList{pub.GuidedNavigationLink}
}

Expand Down Expand Up @@ -120,5 +125,8 @@ func (s *MediaOverlayService) GuideForResource(ctx context.Context, href string)
}

func (s *MediaOverlayService) Get(ctx context.Context, link manifest.Link) (fetcher.Resource, bool) {
if !s.public {
return nil, false
}
return pub.GetForGuidedNavigationService(ctx, s, link)
}
20 changes: 14 additions & 6 deletions pkg/parser/epub/positions_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,27 @@ import (
// https://github.com/readium/architecture/blob/master/models/locators/best-practices/format.md#epub
// https://github.com/readium/architecture/issues/101
type PositionsService struct {
readingOrder manifest.LinkList
layout manifest.Layout
fetcher fetcher.Fetcher
reflowableStrategy ReflowableStrategy
positions [][]manifest.Locator
public bool // Whether the service exposes itself via Links() and Get()
readingOrder manifest.LinkList // The reading order of the publication
layout manifest.Layout // The publication's layout
fetcher fetcher.Fetcher // The publication's fetcher
reflowableStrategy ReflowableStrategy // How to compute positions in reflowable resources
positions [][]manifest.Locator // Cached calculated positions
}

func (s *PositionsService) Close() {}

func (s *PositionsService) Links() manifest.LinkList {
if !s.public {
return nil
}
return manifest.LinkList{pub.PositionsLink}
}

func (s *PositionsService) Get(ctx context.Context, link manifest.Link) (fetcher.Resource, bool) {
if !s.public {
return nil, false
}
return pub.GetForPositionsService(ctx, s, link)
}

Expand Down Expand Up @@ -132,8 +139,9 @@ func PositionsServiceFactory(reflowableStrategy ReflowableStrategy) pub.ServiceF
reflowableStrategy = RecommendedReflowableStrategy
}

return func(context pub.Context) pub.Service {
return func(context pub.Context, public bool) pub.Service {
return &PositionsService{
public: public,
readingOrder: context.Manifest.ReadingOrder,
layout: context.Manifest.Metadata.Layout,
fetcher: context.Fetcher,
Expand Down
10 changes: 9 additions & 1 deletion pkg/parser/pdf/positions_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

// Positions Service for an PDF.
type PositionsService struct {
public bool // Whether the service exposes itself via Links() and Get()
link manifest.Link // The [Link] to the PDF document in the [Publication].
pageCount uint // Total page count in the PDF document.
tableOfContents manifest.LinkList // Table of contents used to compute the position titles.
Expand All @@ -22,10 +23,16 @@ type PositionsService struct {
func (s *PositionsService) Close() {}

func (s *PositionsService) Links() manifest.LinkList {
if !s.public {
return nil
}
return manifest.LinkList{pub.PositionsLink}
}

func (s *PositionsService) Get(ctx context.Context, link manifest.Link) (fetcher.Resource, bool) {
if !s.public {
return nil, false
}
return pub.GetForPositionsService(ctx, s, link)
}

Expand Down Expand Up @@ -86,7 +93,7 @@ func (s *PositionsService) computePositions() [][]manifest.Locator {
}

func PositionsServiceFactory() pub.ServiceFactory {
return func(context pub.Context) pub.Service {
return func(context pub.Context, public bool) pub.Service {
if len(context.Manifest.ReadingOrder) == 0 {
return nil
}
Expand All @@ -97,6 +104,7 @@ func PositionsServiceFactory() pub.ServiceFactory {
}

return &PositionsService{
public: public,
link: context.Manifest.ReadingOrder[0],
pageCount: count,
tableOfContents: context.Manifest.TableOfContents,
Expand Down
20 changes: 4 additions & 16 deletions pkg/pub/publication.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,27 +104,15 @@ func (p Publication) BaseURL() url.URL {
return nil
}

// Find a service in the publication by its name.
func (p Publication) FindService(serviceName ServiceName) Service {
for k, v := range p.services {
if k != serviceName {
continue
}
return v
svc, ok := p.services[serviceName]
if ok {
return svc
}
return nil
}

func (p Publication) FindServices(serviceName ServiceName) []Service {
var services []Service
for k, v := range p.services {
if k != serviceName {
continue
}
services = append(services, v)
}
return services
}

// Returns the resource targeted by the given non-templated [link].
func (p Publication) Get(ctx context.Context, link manifest.Link) fetcher.Resource {
for _, service := range p.services {
Expand Down
25 changes: 23 additions & 2 deletions pkg/pub/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,12 +39,13 @@ func NewContext(manifest manifest.Manifest, fetcher fetcher.Fetcher) Context {
}
}

type ServiceFactory func(context Context) Service
type ServiceFactory func(context Context, public bool) Service

// Builds a list of [Service] from a collection of service factories.
// Provides helpers to manipulate the list of services of a [pub.Publication].
type ServicesBuilder struct {
serviceFactories map[ServiceName]ServiceFactory
publicFlags map[ServiceName]bool
}

/*
Expand All @@ -62,6 +63,7 @@ func NewServicesBuilder(fcs map[ServiceName]ServiceFactory) *ServicesBuilder {

return &ServicesBuilder{
serviceFactories: fcs,
publicFlags: map[ServiceName]bool{},
}
}

Expand All @@ -71,15 +73,26 @@ func (s *ServicesBuilder) Build(context Context) map[ServiceName]Service {
for k, v := range s.serviceFactories {
// Allow service factories to be nil
if v != nil {
public := s.publicFlags[k]

// Allow service factories to return nil
if service := v(context); service != nil {
if service := v(context, public); service != nil {
services[k] = service
}
}
}
return services
}

// Gets the names of all services currently in the builder
func (s *ServicesBuilder) Services() []ServiceName {
keys := make([]ServiceName, 0, len(s.serviceFactories))
for k := range s.serviceFactories {
keys = append(keys, k)
}
return keys
}

// Gets the publication service factory for the given service type.
func (s *ServicesBuilder) Get(name ServiceName) *ServiceFactory {
if v, ok := s.serviceFactories[name]; ok {
Expand Down Expand Up @@ -138,3 +151,11 @@ func (s *ServicesBuilder) Decorate(name ServiceName, transform func(*ServiceFact
s.serviceFactories[name] = transform(nil)
}
}

func (s *ServicesBuilder) ExposeLinks(name ServiceName) {
s.publicFlags[name] = true
}

func (s *ServicesBuilder) HideLinks(name ServiceName) {
delete(s.publicFlags, name)
}
12 changes: 10 additions & 2 deletions pkg/pub/service_content.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ type ContentService interface {
type DefaultContentService struct {
context Context
resourceContentIteratorFactories []iterator.ResourceContentIteratorFactory
public bool
}

func GetForContentService(ctx context.Context, service ContentService, link manifest.Link) (fetcher.Resource, bool) {
if link.Href != ContentLink.Href {
if !link.URL(nil, nil).Equivalent(ContentLink.URL(nil, nil)) {
return nil, false
}

Expand All @@ -55,10 +56,16 @@ func GetForContentService(ctx context.Context, service ContentService, link mani
func (s DefaultContentService) Close() {}

func (s DefaultContentService) Links() manifest.LinkList {
if !s.public {
return nil
}
return manifest.LinkList{ContentLink}
}

func (s DefaultContentService) Get(ctx context.Context, link manifest.Link) (fetcher.Resource, bool) {
if !s.public {
return nil, false
}
return GetForContentService(ctx, s, link)
}

Expand Down Expand Up @@ -94,10 +101,11 @@ func (c contentImplementation) Text(ctx context.Context, separator *string) (str
}

func DefaultContentServiceFactory(resourceContentIteratorFactories []iterator.ResourceContentIteratorFactory) ServiceFactory {
return func(context Context) Service {
return func(context Context, public bool) Service {
return DefaultContentService{
context: context,
resourceContentIteratorFactories: resourceContentIteratorFactories,
public: public,
}
}
}
10 changes: 9 additions & 1 deletion pkg/pub/service_positions.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ type PositionsService interface {
type PerResourcePositionsService struct {
readingOrder manifest.LinkList
fallbackMediaType mediatype.MediaType
public bool
}

func GetForPositionsService(ctx context.Context, service PositionsService, link manifest.Link) (fetcher.Resource, bool) {
Expand All @@ -48,10 +49,16 @@ func GetForPositionsService(ctx context.Context, service PositionsService, link
func (s PerResourcePositionsService) Close() {}

func (s PerResourcePositionsService) Links() manifest.LinkList {
if !s.public {
return nil
}
return manifest.LinkList{PositionsLink}
}

func (s PerResourcePositionsService) Get(ctx context.Context, link manifest.Link) (fetcher.Resource, bool) {
if !s.public {
return nil, false
}
return GetForPositionsService(ctx, s, link)
}

Expand Down Expand Up @@ -86,10 +93,11 @@ func (s PerResourcePositionsService) PositionsByReadingOrder(ctx context.Context
}

func PerResourcePositionsServiceFactory(fallbackMediaType mediatype.MediaType) ServiceFactory {
return func(context Context) Service {
return func(context Context, public bool) Service {
return PerResourcePositionsService{
readingOrder: context.Manifest.ReadingOrder,
fallbackMediaType: fallbackMediaType,
public: public,
}
}
}
9 changes: 9 additions & 0 deletions pkg/streamer/streamer.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ type Streamer struct {
// TODO pdfFactory
httpClient *http.Client
onCreatePublication OnCreatePublicationFunc
addServicelinks bool
}

type OnCreatePublicationFunc func(builder *pub.Builder) error
Expand All @@ -43,6 +44,7 @@ type Config struct {
ArchiveFactory archive.ArchiveFactory // Opens an archive (e.g. ZIP, RAR), optionally protected by credentials.
HttpClient *http.Client // Service performing HTTP requests.
OnCreatePublication OnCreatePublicationFunc // Called on every parsed [pub.Builder]. It can be used to modify the manifest, the root container or the list of service factories of a [pub.Publication]
AddServiceLinks bool // When true, services will add their links to the publication manifest. This can also be adjusted for individual services in `OnCreatePublication`
}

type InferA11yMetadata uint8
Expand Down Expand Up @@ -86,6 +88,7 @@ func New(config Config) Streamer { // TODO contentProtections
archiveFactory: config.ArchiveFactory,
httpClient: config.HttpClient,
onCreatePublication: config.OnCreatePublication,
addServicelinks: config.AddServiceLinks,
}
}

Expand Down Expand Up @@ -117,6 +120,12 @@ func (s Streamer) Open(ctx context.Context, a asset.PublicationAsset, credential
return nil, errors.New("cannot find a parser for this asset")
}

if s.addServicelinks {
for _, name := range builder.ServicesBuilder.Services() {
builder.ServicesBuilder.ExposeLinks(name)
}
}

if s.onCreatePublication != nil {
if err := s.onCreatePublication(builder); err != nil {
return nil, errors.Wrap(err, "failed creating publication")
Expand Down