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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. Take a look

## [Unreleased]

### Added

#### Navigator

* The EPUB navigator supports publications mixing reflowable and fixed-layout resources, rendering each resource according to its own layout.

### Changed

#### Shared
Expand Down
13 changes: 0 additions & 13 deletions Sources/Navigator/EPUB/EPUBExtensions.swift

This file was deleted.

31 changes: 31 additions & 0 deletions Sources/Navigator/EPUB/EPUBLayouts.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright 2026 Readium Foundation. All rights reserved.
// Use of this source code is governed by the BSD-style license
// available in the top-level LICENSE file of the project.
//

import Foundation
import ReadiumShared

/// The set of EPUB layouts used by the resources of a reading order.
///
/// A publication can mix reflowable and fixed-layout resources, so knowing
/// which layouts are actually rendered is what determines whether a preference
/// is effective.
struct EPUBLayouts {
/// Layout used by the resources which don't override it.
let `default`: EPUBLayout

private let layouts: Set<EPUBLayout>

init(readingOrder: [Link], metadata: Metadata) {
`default` = metadata.epubLayout
layouts = Set(readingOrder.map { metadata.epubLayout(of: $0) })
}

/// Indicates whether the reading order contains at least one resource
/// rendered with the given `layout`.
func contains(_ layout: EPUBLayout) -> Bool {
layouts.contains(layout)
}
}
4 changes: 2 additions & 2 deletions Sources/Navigator/EPUB/EPUBNavigatorViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1036,7 +1036,7 @@ extension EPUBNavigatorViewController: EPUBSpreadViewDelegate {
// the application's bars.
var insets = view.window?.safeAreaInsets ?? .zero

switch publication.metadata.epubLayout {
switch spreadView.spread.layout(in: publication) {
case .fixed:
// With iPadOS and macOS, we aim to display content edge-to-edge
// since there are no physical notches or Dynamic Island like on the
Expand Down Expand Up @@ -1258,7 +1258,7 @@ extension EPUBNavigatorViewController: EditingActionsControllerDelegate {
extension EPUBNavigatorViewController: PaginationViewDelegate {
func paginationView(_ paginationView: PaginationView, pageViewAtIndex index: Int) -> (UIView & PageView)? {
let spread = spreads[index]
let spreadViewType = (publication.metadata.layout == .fixed) ? EPUBFixedSpreadView.self : EPUBReflowableSpreadView.self
let spreadViewType = (spread.layout(in: publication) == .fixed) ? EPUBFixedSpreadView.self : EPUBReflowableSpreadView.self
let spreadView = spreadViewType.init(
viewModel: viewModel,
spread: spread,
Expand Down
5 changes: 3 additions & 2 deletions Sources/Navigator/EPUB/EPUBNavigatorViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,8 @@ enum EPUBScriptScope {
func editor(of preferences: EPUBPreferences) -> EPUBPreferencesEditor {
EPUBPreferencesEditor(
initialPreferences: preferences,
metadata: publication.metadata,
publication: publication,
readingOrder: readingOrder,
defaults: config.defaults
)
}
Expand Down Expand Up @@ -313,7 +314,7 @@ enum EPUBScriptScope {
guard
let link = publication.linkWithHREF(href),
link.mediaType?.isHTML == true,
publication.metadata.epubLayout == .reflowable
publication.metadata.epubLayout(of: link) == .reflowable
else {
return resource
}
Expand Down
30 changes: 22 additions & 8 deletions Sources/Navigator/EPUB/EPUBSpread.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,19 @@ enum EPUBSpread: EPUBSpreadProtocol {
}
}

/// Returns the layout used to render this spread.
///
/// A double spread is always fixed-layout. A single spread uses the layout
/// of its resource, resolved from the publication `metadata`.
func layout(in publication: Publication) -> EPUBLayout {
switch self {
case let .single(spread):
return publication.metadata.epubLayout(of: spread.resource.link)
case .double:
return .fixed
}
}

private var spread: EPUBSpreadProtocol {
switch self {
case let .single(spread):
Expand Down Expand Up @@ -133,13 +146,14 @@ enum EPUBSpread: EPUBSpreadProtocol {
var first = readingOrder[index]

// The first resource (often the cover) has special rules for its
// position in the spread.
if index == 0 {
// position in the spread. They only apply to fixed-layout
// resources, as a reflowable one is never paired with another.
if index == 0, publication.metadata.epubLayout(of: first) == .fixed {
if let offsetFirstPage = offsetFirstPage {
// User explicitly chose to offset (or not) the first page.
first.properties.page = offsetFirstPage ? .center : nil
} else if first.properties.page == nil, publication.metadata.layout == .fixed {
// For FXL publications, default to displaying the first
} else if first.properties.page == nil {
// For FXL resources, default to displaying the first
// page (typically a cover) on its own when the publication
// doesn't provide an explicit page position. This is the
// behavior of Apple Books, so it's expected by publishers.
Expand All @@ -152,12 +166,12 @@ enum EPUBSpread: EPUBSpreadProtocol {

let nextIndex = index + 1

// To be displayed together, two pages must be part of a fixed
// layout publication and have consecutive position hints
// (Properties.Page).
// To be displayed together, two pages must both be fixed-layout
// resources and have consecutive position hints (Properties.Page).
if
let second = readingOrder.getOrNil(nextIndex),
publication.metadata.layout == .fixed,
publication.metadata.epubLayout(of: first) == .fixed,
publication.metadata.epubLayout(of: second) == .fixed,
areConsecutive(first, second, readingProgression: publication.metadata.readingProgression)
{
spreads.append(.double(
Expand Down
4 changes: 2 additions & 2 deletions Sources/Navigator/EPUB/Preferences/EPUBPreferences.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ public struct EPUBPreferences: ConfigurablePreferences, Sendable {
/// spread).
public var columnCount: ColumnCount?

/// Method for fitting the content of a fixed-layout publication within the
/// Method for fitting the content of fixed-layout resources within the
/// viewport.
///
/// - `auto` or `page`: Fit entire page within viewport (default).
Expand Down Expand Up @@ -79,7 +79,7 @@ public struct EPUBPreferences: ConfigurablePreferences, Sendable {
/// scrolling instead of synthetic pagination.
public var scroll: Bool?

/// Indicates if the fixed-layout publication should be rendered with a
/// Indicates if the fixed-layout resources should be rendered with a
/// synthetic spread (dual-page).
public var spread: Spread?

Expand Down
Loading