Skip to content

Conversation

@Protected
Copy link
Contributor

This adds <foliate-view smart> that makes use of a getDimensions() asynchronous method that may be present in book sections. Currently, comic-book adds this method for JPG, PNG, GIF and WEBP pages as long as the smart attribute is set.

The purpose of smart spreads mode is to use the aspect ratio of a page to determine whether it should be placed in the center position of a spread rather than left or right. This allows double page spreads in comics to fill the entire viewport even when there is enough space for two pages.

Before:

image

After:

image

I made this an attribute because of course there is a performance tradeoff. The getDimensions code in comic-book efficiently reads the dimensions of the pages from their headers without loading the entire file. However, all pages must still be extracted from the zip on load for this to work.

I'm not caching the pages themselves because I want low memory devices to be able to free up the memory used by each page when loading the next one during the dimensions extraction.

@Protected
Copy link
Contributor Author

Another benefit of this mode is that if the comic contains a double page spread, it will serve as an anchor that conveys the comic creator's intent for whether subsequent pages should slot into left or right (unfortunately, for pages before the first double page spread, you can't reliably make any assumptions, as I've seen it both ways; I've opted not to shift them even if there's a gap).

I haven't made any changes to the documentation yet since I don't know if you want to keep this or if you want to make any changes in how it's enabled/the interface for the book sections.

@johnfactotum
Copy link
Owner

If you're getting the dimensions for every page before rendering anything, there shouldn't be any need for changing the renderer. You should be able just specify the pageSpread property on a section object (which it seems I forgot to document) when building the book object.

And to avoid having to do this every time, a better approach might be to cache the dimensions, or the resulting spreads, somewhere in your application. One can even put the spreads in a package document, that is, basically converting the book into EPUB.

@Protected
Copy link
Contributor Author

So you don't want the feature in foliate at all?

@johnfactotum
Copy link
Owner

No, what I mean is that if it's something that is done completely before the book is rendered, then you should be able to add it without touching fixed-layout.js. Just make comic-book.js output the desired spreads when creating the book object.

For the same reason, there shouldn't be any need to add the attribute to foliate-view, either. Though I'm not sure how to best expose such a feature there.

One option might be to beef up the makeBook function a bit. For example, turn it into a class called BookMaker or BookLoader, and make it an option when constructing the class, though this does not really scale (in terms of different possible options for different types of books). In light of #71, this class could also be the natural place where type could be exposed as a property, though this doesn't seem very clean to me, either.

Another option would be to simply say that if you need to access advanced options, you need to make your own equivalent of the makeBook function (or to catch any special case yourself before calling it). That's actually the intended design, and is precisely the reason why, for example, makeBook is a separate function rather than a part of foliate-view.

Copy link

@github-advanced-security github-advanced-security bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ESLint found more than 20 potential problems in the proposed changes. Check the Files changed tab for more details.

@Protected Protected force-pushed the smart-comic-spreads branch from 7a27ce9 to 6d7027b Compare June 2, 2025 17:27
@Protected
Copy link
Contributor Author

Protected commented Jun 2, 2025

Changed based on my interpretation of what you said. In this commit:

  • The smart spread logic is moved out of the renderer (no changes in the renderer);
  • The attribute logic is removed from foliate-view;
  • View.open (in view.js) now takes an optional options argument. This may contain a fileToBook entry with a function that replaces makeBook, as well as additional options that are passed to the fileToBook function;
  • The default makeBook will understand the option smartSpreads;
  • All the smart spread logic is in comic-book.js.

This seems fairly clean (only changes in 2 files, spreads are calculated beforehand and use pageSpread), extendable, and doesn't force people to keep a copy of the whole makeBook function just to use built in functionality.

With this solution, there is no change in how .type is set on the generated Book, so the other pull request remains unchanged.

@johnfactotum
Copy link
Owner

It does seem much cleaner. It just occurred to me, however, that you can actually do it after the book object is created, and even externally, by directly mutating the sections. That could be a way of loading the spreads from cache, if you want, similar to generating locations in Epub.js. There's also the possibility of enabling/disabling it even after the book is opened, without having to reload everything, though you'd need to call renderer.open(book) again. And there would be no need to change makeBook().

Another thing to consider is how to support dynamically overriding the spreads and the page progression direction. Probably, you'd need to implement that in the same place where you're re-spreading the book smartly. It's not clear to me, though, if one goes down that route, what it should look like. Probably, there needs to be an interface between the renderer and the book implementation after all.

So I'm thinking perhaps attributes on the renderer element, something like odd-pages-verso (boolean), page-progression-direction (ltr, rtl), and smart-spreads? And changing them would result in a call to a respread method of the book (or perhaps a default implementation if there's no such method), which will handle the actual task of reassigning the spread properties.

@Protected
Copy link
Contributor Author

Are you sure about the design this time?

Also I believe the direction should be implicit from the standard HTML attribute dir rather than using a custom attribute (that's already what I'm doing and it works fine).

@johnfactotum
Copy link
Owner

johnfactotum commented Jun 16, 2025

Are you sure about the design this time?

Not really, which is why I used "probably", "perhaps", "It's not clear to me..." and so on in my comment.

The main reason for this is that Foliate and foliate-js have been developed mainly for reflowable EPUBs. Fixed-layout is something of an after thought, and non-EPUB/Kindle fixed-layout even more so. So the whole thing is quite poorly thought out and worked out in general compared to the paginator.

Also I believe the direction should be implicit from the standard HTML attribute dir rather than using a custom attribute (that's already what I'm doing and it works fine).

I guess you're right. Currently, it would only work for books that are rendered LTR by default, though, because RTL books (when the book object has the dir property set to "rtl") are actually rendered by swapping the left and right pages, rather than using the dir attribute, which is arguably a bug.

@Protected
Copy link
Contributor Author

So to be clear, the only page direction issue to be corrected (in the current implementation) would be, specifically, fixed layout EPUBs with embedded RTL in HTML "rtl" mode? Since comic books and pdfs don't have built in reading direction, so they're always "default" (ltr).

@Protected
Copy link
Contributor Author

Protected commented Jun 20, 2025

I made a test epub to look into this. Unfortunately I'm not sure how to properly read the epub (if it's at all possible) to correctly lay out the pages such that these attributes would always work. For example, is it safe to assume that the designer of the pub intended the first page to always be recto (absent a toggle like your proposed odd-page-verso)?

Having something like this:

<meta property="rendition:layout">pre-paginated</meta>
<meta property="rendition:orientation">landscape</meta>
<meta property="rendition:spread">landscape</meta>
<spine toc="ncx" page-progression-direction="rtl">
    <itemref idref="CoverPage_xhtml" properties="rendition:spread-landscape page-spread-left"/>
    <itemref idref="section-0001_xhtml" properties="rendition:spread-landscape page-spread-right"/>
    <itemref idref="section-0001_b_xhtml" properties="rendition:spread-landscape page-spread-left"/>
    ...
</spine>

As a human I can tell that CoverPage is supposed to be on the left, then a page break, then the other two are part of the same spread. But how can the reader tell it's not supposed to create a spread with CoverPage on the left and section-0001 on the right? We need to make an assumption about recto and verso, right?

Then the way foliate currently behaves has the opposite issue. If CoverPage is tagged as right and section-0001 is tagged as left it aggregates them together even though that's not necessarily desirable.

It's complicated to me because epubs themselves are assigning fixed positions to the pages in their metadata instead of reading direction based positions. So the only solution I can see that will make everything work is:

  • Within foliate-fxl, "left" will now become "start" and "right" will become "end" from a semantically perspective (that is, "left" is always read before "right");
  • Always assume a default of odd-page-recto;
  • Spread function creates the spreads based on these assumptions, which means it flips all page positions if the page-progression-direction is "rtl" within the epub;
  • Changing the dir doesn't require respreading;
  • Changing odd-pages-recto to odd-pages-verso requires respreading (this can happen automatically)

Respreading logic can stay within the renderer. I think it's enough to expose it so it can be called if someone wants to make changes to the spread property in each section after the initial loading. Smart spreads would remain outside the renderer and interface with the renderer through the spread attribute.

@Protected
Copy link
Contributor Author

I guess instead of always assuming odd page recto, we can make it a little smarter by assuming odd page verso if the first listed page is right in rtl or if the first listed page is left in ltr...

@Protected Protected force-pushed the smart-comic-spreads branch from 6d7027b to 048cc9e Compare June 25, 2025 17:36
@Protected Protected force-pushed the smart-comic-spreads branch from 048cc9e to c856eb8 Compare June 25, 2025 17:42
@Protected Protected changed the title Smart attribute for foliate-view adds smart spreads to comic books. Dynamic spreads in fixed-layout and smart spreads in comic-books Jun 25, 2025
@Protected
Copy link
Contributor Author

Protected commented Jun 25, 2025

Now fixes #66 .

After taking some time today to wrap my head around this I believe this solution is working correctly in all cases (I haven't spotted any issues so far).

These changes may modify previous foliate behavior. Specifically, it's not possible in this version to fill in a spread "out of order" when compared to the book's (not HTML's) page-progression-direction. So for example in a ltr book, if a right page creates a new spread during spreading, a left page can't then be added to the same spread, as that would mean the page was declared against pagination order. I researched this and believe this is the correct behavior.

Notable changes and other notes

respread() function is now available (exposed). It re-spreads and re-renders. This function can be used to re-spread when a re-spread is not automatically triggered (normally, when one or more section.pageSpread are modified in the book.)

odd-pages attribute is now available. It can be used to force the book to start on the front (recto) or back (verso) page. This behavior shifts every page during spreading as needed, until the end of the book or a center section is encountered (subsequent pages are spread from the center section such that there are no unnecessary gaps). Changes always trigger a re-spread. If not present, it is determined automatically based on the book, which can be more versatile. The ultimate default behavior is recto.

The spread attribute can now be used to dynamically override rendition:spread and trigger a re-spread.

rtl is no longer exposed. #rtl is a property of the book layout, to be used together with the embedded left and right page-spread values provided with the book. To reverse the rendering direction, the HTML dir attribute should be used on the renderer or anywhere in its containing hierarchy as needed.

left and right pageSpread are normally kept as they are provided and are not changed depending on the page progression direction. It is normally assumed (I believe this is in accordance with the standard) that the left/right positions embedded in the ebook already account for the page progression order intended by the author...

...however, they still may be flipped (left to right and right to left) if odd-pages is being forced such that the position provided is not otherwise compatible with the requested layout.

I didn't add this yet, but it might also be interesting to have an attribute that inhibits the book's synthetic spreads entirely and "forces" the renderer to use its page progression direction assumptions on all pages.

@johnfactotum
Copy link
Owner

Sorry for the late reply.

Then the way foliate currently behaves has the opposite issue. If CoverPage is tagged as right and section-0001 is tagged as left it aggregates them together even though that's not necessarily desirable.

My understanding is that this is indeed the expected behavior if the page progression is RTL. If the page progression is LTR, on the other hand, it should create a new spread.

These changes may modify previous foliate behavior. Specifically, it's not possible in this version to fill in a spread "out of order" when compared to the book's (not HTML's) page-progression-direction. So for example in a ltr book, if a right page creates a new spread during spreading, a left page can't then be added to the same spread, as that would mean the page was declared against pagination order. I researched this and believe this is the correct behavior.

Do you have an example? I believe this is already how it behaves. If it doesn't, it would indeed be a bug.

odd-pages attribute is now available. It can be used to force the book to start on the front (recto) or back (verso) page.

My idea was that rather than always forcing it, it would be something like a hint for when no information is available. The spreads in EPUB, for example, should probably not be overridden. So the idea was that only the Book object knows whether the spreads it gives are "true spreads" or merely guesses that can be influenced by the hint. The same applies to the page progression direction.

Another idea that just occurred to me is that perhaps ideally the control should flow from the Book object to the renderer because conceptually, what the user is really doing is modifying the book's contents on the fly. This can be implemented as a custom event on the Book object (perhaps its eventTarget property, which is currently used for another purpose) where the renderer can listen to when the book needs to be respread. In this approach, the reading system can query the Book object to see whether to expose settings to override page direction and spreads, if they aren't specified by the book, and then ask the Book object to override them, and the renderer will react automatically. More generally, it can do the same with the rendition properties for reflowable books as well.

Alternatively, and more simply, there would be an odd-pages attribute, but it should accept and default to auto.

rtl is no longer exposed. #rtl is a property of the book layout, to be used together with the embedded left and right page-spread values provided with the book. To reverse the rendering direction, the HTML dir attribute should be used on the renderer or anywhere in its containing hierarchy as needed.

The dir attributes needs to be set to rtl by the renderer whenever the dir property of the Book object is "rtl". Currently, foliate-js doesn't do this, which results in renditions that are visually correct but incorrect in terms of semantics and accessibility.

Given this, the dir attribute is "owned" by the renderer, in which case it seems more natural to me that one would find another way of overriding the direction. I guess it's also possible for the renderer to observe its own dir attribute and respread when it changes. As I said above, though, I kind of feel that it might be better if it goes through the Book object, to make it have some say in the matter, or to control it entirely from the Book object's side.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants