Image wizardry in eBooks

Let’s be honest, images management in reflowable eBooks is absolutely terrible.

You can’t keep an image with its caption on every platform, portrait aspect ratio images are a pain in the neck, text can’t flood when images don’t fit and you end up with whitespace all over the place, etc.

Back to 2011.

You probably didn’t notice it at the time but Opera launched its Reader. Yes, you must use Opera 12 to feel the experience; if you can’t, here is a video demo and there lie the specs. The beauty of this implementation? It’s API. Paginating your web page is a simple switch, and you can add controls with one single CSS value.

To put it simply, it allows you to float elements on top or bottom of columns, make it span several columns, etc. It’s awesome, and it’s actually a living standard used in Prince for instance. So why don’t we enjoy that in eBooks? Because we can’t have nice things.

Problem is I’m a warrior and I’ve accepted that I’ll probably die trying to improve eBooks — a fight you don’t really want to fight.

So… I gave images a shot and here starts the hackery wizardry.


I guess a clarification is due as the point, indeed, may be hard to see at first sight.

It’s all about experimenting, which is the point of the Blitz Labs: see what’s possible, then share and improve the snippets so that we can try making them work well in the maximum number of RS and finally use them for progressive enhancement.

Let’s be honest, with EPUB 3.1 turning to “vanilla CSS”, it will be more and more difficult to manage CSS across the ecosystem. So either we’re cool with the idea CSS must be the lowest common denominator or we do progressive enhancement and take advantage of the “new and shiny” to achieve what was not possible earlier.

As far as I’m concerned, I think that we’ll have to deal with brutal/extreme fragmentation pretty soon and that, more and more, some RS will allow us to do things some others RS won’t. So yeah, if advanced stuff works in advanced RS but not others, so be it—as long as we have fallbacks, we’re good. And we probably won’t improve our situation by accepting the idea of the lowest common denominator as a fact. Think evolution, think Darwin.

And if you want to play a little bit, download this.


Portrait aspect ratio image with caption

If you want to help test and improve it, come & say hello on the gist.

Credit where credit is due. Joshua Tallent did it first.

It’s cool but I thought that somehow, it was worth extending it so that:

  1. you get better control (caption stays on the same page until a floor is reached);
  2. you don’t need to get the image to start at the top of the page.

Truth be told, a year has passed and should Joshua have revisited this, he would probably have extended it as well.

But anyway, here is the CSS snippet:

/* This in your fallback CSS (for ePub 2) */
.portrait-caption {
height: 99%;
margin: 1.5em 0;
text-align: center;
page-break-before: always;
}
.portrait-caption > img {
width: auto;
max-width: 100%;
height: 80%;
}


/* The following in another CSS (EPUB 3) */

/* EPUB 3 FALL-FALL-FALLBACK */

.portrait-caption {
/* I'm using bg-color to check which styles are applied in RS */
background-color: yellow;
}
.portrait-caption > img {
height: 80vh;
max-height: 800px;
/* to keep aspect-ratio of image. Thanks iBooks’ default CSS for that */
object-fit: contain;
}

/* Feature query using @supports = progressive enhancement */

@supports (page-break-before: always) and (height: calc(99vh - 5em)) {
.portrait-caption {
background-color: LightGreen;
/* fallback for the following line if anything goes wrong */
min-height: 100vh;
/* = 100vh but we make sure it is recomputed when doc is updated */
min-height: calc(100vh - 0px);
/* making ADE behaves since it tells you that yeah, it supports page-break-before: always while it doesn’t at all #FalsePositive */
-webkit-column-break-inside: avoid;
    page-break-before: always;
margin: 0;
}
.portrait-caption > img {
box-sizing: border-box; /* because padding */
max-width: 100%;
/* So that it doesn't become ridiculously tiny when huge font-size is set */
min-height: 60vh;
/* 98vh - caption’s height || Fallback is 80vh */
height: calc(98vh - 5em);
/* if for some reason it breaks inside, we make sure we got a padding at top of image, bottom for caption */
padding: 1vh 0;
}
}

/* For Readium scroll as the viewport is unreliable.
1. min-height depends on your documents’ sizes. Using `em`
makes sure it is recomputed when font-size is changed.
Maybe we could find something else?
2. FYI, if you just got an image in a xhtml file,
Readium’s default viewport in scroll is 300 x 150px
so your 100vh-height image will be 150px. */

@media screen and (min-height: 150em) {
.portrait-caption {
margin: 1.5em 0;
min-height: 0; /* reset */
height: auto; /* reset */
background-color: red;
}
.portrait-caption > img {
min-height: 300px; /* reset */
height: auto; /* reset as we rely on max-width */
max-height: 100%;
max-width: 100%; /* Pic will scale based on its width */
padding: 0;
}
}

/* If you do print, switch to anything else than vh because there are nasty bugs with it (like 98vh = 98 pages) */
@media print {
/* Styles */
}

This is terribly over-engineered. Or not.

Actually, we’re taking advantage of “calc()”, which is awesome. It’s just a mathematical expression you ask the RS to solve. In CSS.

So when the user changes font-size, the height of the image is recomputed. Which means that everytime the user increments the font-size, the height of the image i.e. (98vh − 5em) will shrink accordingly. And once it reaches 60vh (min-height) i.e. half the height of the screen + 10%, then the caption will overflow on the following page.

And oh yeah, we’re using a feature request so that if calc() is not supported, we don’t screw everything up. That’s not in the specs but hey, it works — you can’t set CSS in stone, you’d better accept it because as soon as we see something works, we’ll use it.

And that’s it, really. Everything else is because of bugs and workarounds.

The caveats:

  1. we’re relying on the vh unit so should a RS mess with the viewport’s height to achieve pagination, bad things would happen;
  2. it won’t work in Kobo iOS but styles in the feature query are applied so my best guess is they actually override dimensions;
  3. it won’t work in Kindle but I didn’t explore a fix yet;
  4. it won’t work in Google Play Books since it doesn’t support “page-break-before: always” so it will fall back to 80vh;
  5. max-height won’t work in iBooks and I can’t understand why since the value is being overridden by iBooks’ default CSS while the inspector says I’m overriding it… and even if I get rid of min-height and object-fit, iBooks’ 95% will stick… so that might be a bug;
  6. there may be missing styles in the snippet since my experiments were done using a real eBook and not a test-case file so styles were inherited and I may not have paid attention;
  7. if the user enables the two-column view in ADE, he’ll end up with the following…

It’s not that terrible but well, it sucks a little bit—and the only CSS declaration preventing the image from losing its aspect ratio is object-fit: contain. Column queries anyone?

On the other hand, that could also be useful when your landscape portrait ratio images are displayed on a 16:9 device in landscape if you think about it.

Float and flood

If you want to help test and improve it, come & say hello on the gist.

It all started as something I witnessed on FastCoDesign: the image floated inside a paragraph.

H.O.L.Y.S.H.I.T.

How is this possible? Oo

It was actually sort of a bug.

But a bug I could turn into GODLY POWER.

Actually, I re-implemented that 2 years ago and I somehow lost my ePub file at that time so yeah, you’re right, I’m stupid.

To sum up, this is just about floating images… yeah, it’s as easy as float: left. Or is it?

.float-image {
width: 100%;
float: left;
padding: 0;
display: block;
text-align: center;
margin: 0.75em 0;
/* will collapse if float + margin-top and -bottom because your entire CSS will be ignored by legacy RMSDK if you’re using margin */
margin-top: 1vh;
margin-bottom: 1vh;
}

.float-image > img {
/* Look ma, no additional spacing as the line-height now exactly matches the image’s height */
vertical-align: top;
    width: auto;
max-width: 100%;
height: auto;
}

.float-image + * {
widows: 1; /* prevent text cut off if image doesn’t float */
}
/* Since bullets don’t play well with this trick, we must clear lists (consequently, they won't flood) */
.float-image ~ ul, .float-image ~ ol {
clear: both;
}
/* Basically, the following are margin-top and margin-bottom */

/* This is the trigger */
.float-image:before {
display: block;
content: '';
width: 1px;
/* Don’t ask me why, you get text cut off if the image floats and you’ve got any other value, even 1px */
height: 0;
    margin-top: 1vh; /* will collapse if float */
float: right;
clear: right;
}

/* This should be the clearer—except it currently doesn’t do its job when bullets (either span or list-style-type) are involved. */
.float-image:after {
display: block;
content: '';
width: 100%;
height: 1vh;
float: none;
}

So no, it definitely isn’t.

To sum up, using this snippet, the image will float to the next column if it doesn’t fit. But since it is outside the flow (because float), it won’t prevent text from flooding the whitespace it doesn’t fit. Else, it will behave as your typical image.

It ships with a few caveats though:

  • float is really expensive in legacy RMSDK: too many floats in one XHTML file and it takes up to 4 seconds to render the page on eInk Readers so you should probably put that snippet behind a feature query;
  • it will only work in RS using columns to paginate;
  • you’ve got a pretty visible “flash of unfloated image” when changing font-size in some configurations (like say scroll in iBooks iOS);
  • if the text is really small and the viewport too big, your image may be pushed really far in the flow so if you need to keep some relationship between the image and the text, you’re screwed;
  • widows and orphans can’t be used because text may be cut off;
  • it’s just terrible when you apply “page-break-inside: avoid” to your figure so you can’t use it for images with captions;
  • it’s a float so… watch your bullets and lists and shit as they are likely to float inside the image’s wrap.

On the other hand, it even works with “full-bleed images” (i.e. without caption since we can’t use page-break-inside: avoid).

It somehow works not that badly with the worst test suite I can throw at this but well, the float should ideally be added using JavaScript if some conditions are met.

But since JavaScript is optional…

I’m a fucking psychopath, let’s experiment with CSS regions

If you want to help test and improve it… No, wait, actually you don’t.

If you are not familiar with CSS regions, let me introduce its specs. Then check this demo (beware, you must use a browser which supports grid and CSS regions so basically, that’s Safari Technology Preview), this one and this one. Now read this announcement from the Chromium team, this from Sara Soueidan and cry.

To be fair, CSS Regions indeed imply a huge performance hit — my Mac Mini can testify. After all, it’s like having InDesign in your browser. But there’s a polyfill if you want to experiment with it everyfuckingwhere.

To sum things up, CSS regions allow you to change the flow of the document; the logical order is not the displayed one.

In other words, it allows you to manage how contents flow and flood.

It is as easy as…

#contents {
-webkit-flow-into: section;
flow-into: section;
}

/* We’ll need this later */
.region {
width: 100%;
height: 100vh;
-webkit-flow-from: section;
flow-from: section;
}

#region-1 {
height: 100vh;
-webkit-flow-from: section;
flow-from: section;
}

#my-image {
-webkit-flow-into: image;
flow-into: image;
}

#region-2 {
height: 50vh;
-webkit-flow-from: image;
flow-from: image;
}

#region-3 {
height: 50vh;
-webkit-flow-from: section;
flow-from: section;
}
/* So that’s how you style inside regions */
@-webkit-region #region-2 {
figure {
height: 100%;
width: auto;
max-width: 100%;
object-fit: contain;
}
}
@region #region-2 {
figure {
height: 100%;
width: auto;
max-width: 100%;
object-fit: contain;
}

To explain that in details:

  1. first page will be text;
  2. top half of the second page will be the figure;
  3. bottom half of the second page will be text (continued from first page).

So you can actually manage where your figure should be displayed.

But… and that’s a capital BUT since it’s terribly unsemantic, you must add empty divs in your xhtml file. Like that…

<section id="contents">my contents</section>
<figure id="my-image"><img/></figure>
<div class="region" id="region-1"></div>
<div class="region" id="region-2"></div>
<div class="region" id="region-3"></div>

And oh yeah, you can’t stop at some point and tell the last region to “height: auto” because you’ll get text cut off, as usual in columns. So you must have 100vh regions (1 full-height div or the sum of 2, 3, etc.) every damned time. In other words, you must absolutely fill every column with one or multiple regions.

Soooooooooooooo this is a nightmare because you can’t do that by hand, especially for something as long as a book.

So once again, the solution lies in JavaScript. And here is the quick and dirty script I threw at RS:

<script type="text/javascript">
<![CDATA[
r(function() {

var flows = document.webkitGetNamedFlows(),
flow = flows.namedItem('section'),
i = 0;

// We basically tell the RS to check if contents overflow from the last region
    lastRegion = document.querySelector("#region-3")

checkOverset();

// We add an event listener for when the region overset change, e.g. user changes font-size for instance
flow.addEventListener('webkitregionoversetchange', change);
function change(e) {
checkOverset();
}

function checkOverset() {

// if content doesn’t fit in the last region, add extra dynamically
  if (flow.overset == true) {
i += 1;
var newRegion = document.createElement('div');
newRegion.className = "region";
document.body.appendChild(newRegion);
checkOverset();
} else {
var regions = flow.getRegions();

// if there are empty regions, remove them (or else blank pages)
    if (flow.firstEmptyRegionIndex > -1) {
for (var j = flow.firstEmptyRegionIndex; j < regions.length; j += 1) {
document.body.removeChild(regions[j]);
}
}
}
}
});

// This last function you need so that the script runs on first rendering in iBooks. Else it won’t and yeah I know this is OLD. But it was just about making it work quickly at 11pm so please feel free to improve that.
// See http://www.dustindiaz.com/smallest-domready-ever/ for further info and https://github.com/ded/domready for newer version

function r(f){/in/.test(document.readyState)?setTimeout('r('+f+')',9):f()}

]]>
</script>

For your information, this was adapted from the following codepen and it’s just a quick and dirty proof of concept that doesn’t necessarily account for the fact XHTML sucks for scripting.

It works in iBooks but won’t in ADE. And oh yeah, performance is so terrible you’d better not resize iBooks’ window on desktop, unless you want to make sure your Mac’s fan is working properly — or witness iBooks crash because it simply gives up at some point.

Truth is you’d better add those divs depending on pagination and dimensions of the actual page; it should be in sync with the Reading System. As a bonus, you then would be able to keep the relationship between the figure and the text contents since the proof of concept is just about placing an image relative to the column/page.

Now, considering simple JavaScript to check overset is already a huge performance hog on a desktop, it should probably be managed at the RS level. But that would not solve performance hoggery, just move it higher in the food chain.

So the caveats are 1. numerous and 2. huge. And if we are being honest, it’s just reimplementing the living CSS Figures spec somehow crappily with a technology Google has dropped because it was too heavy. What could go wrong?

Will that blend in Blitz?

I don’t know. Really.

It’s all about responsibility. So, basically, we must test it, improve it, fix the current issues and make sure it is bulletproof.

But if we achieve that, “portrait & caption” + “float & flood” might be added at some point (v2). And it would be super simple to create parametric mixins automating the creation of customized snippets (height of figure, image and caption as arguments, etc.).

So, it’s up to you now. I’ve spent days designing and refining those tricks, it’s just a starting point and you can put a different spin on it. That’s how open source works after all.

And here’s an EPUB file including the 3 tricks if needed.

Subito Presto

2011: Opera solves all of the floating/flooding issues.

2016: I’m using WTF CSS Tricks to achieve only a third of what Opera Reader was able to display “natively”.

Question is why iBooks, Readium & al. can’t solve this? After all, Opera made that happen for mobile in the first place…

And this is sad.

It should not be wizardry, it should be #eprdctn as usual.

We shouldn’t try to make “break-inside: avoid” work everywhere anymore, we should be wondering if a figure is best floated on the bottom left corner or the top right corner, relative to the primary chunk of contents it extends. Cos’ yeah, from an editorial design point of view, asides, figures and stuff are all about this relationship and not that much about pixel-perfect positioning.

eBook design is not about drawing the precise coordinates of two elements in a matrix, it’s about expressing those two elements are linked. And this could be an image floating on the top right-corner of a spread, it could also be a pop-up/modal you display by clicking on a word/icon, or a floated panel you can drag around.

We all know that when extra contents are displayed on a page, reading is not a linear process anymore: we scan, we are drawn, we dig in, etc. Contents are UI and as long as we, eBook designers, are limited in what we can do in paginated environments, eBooks UI and UX will remain dumb.

It’s time we care about the Reading Experience in non-fiction. It’s time we find ways to improve that dramatically. It’s time we all agree we should design and build a reflowable ecosystem in which authors can easily add features they deem appropriate.

We shouldn’t have to create awfully over-engineered and unmaintainable scripts to achieve something as easy as a fixed modal because columns (used for pagination) turn it into a melodramatic mess. We should be able to do that with simplistic scripts we can share, scale, improve and bulletproof.

It’s high time.

Even if reflowable and pagination are hard.

It’s high time we tackle eBook design.

A single golf clap? Or a long standing ovation?

By clapping more or less, you can signal to us which stories really stand out.