ePub package.opf generator

This is the first pass at a script to generate a basic package.opf file for epub3 ebooks using Python 2.X.

What it does

When you run it from the root of your ebook, the script will create a package.opf file, populate it with basic metadata as required by the epub3 specification, it will also create metadata and spine sections based on the content of the OEBPS directory.

How does the script do it

The script only uses modules from the default library. I want the a portable script and I don’t want to worry whether a module is compatible with 2.X and 3.X, compatible with either version or if uses a different syntax on each version.

At the top of the script we use the environment ‘shebang’ to declare the location of the Python executable without hard coding it.

We import the following modules, each for a specific purpose:

  • mimetypes to find the mime type of our files automatically
  • glob to create the list of files under OEBPS
  • os and os.path to create the items we populate our package file with

After loading the modules the first thing we do is initialize our mime-type database. This will make sure, as much as possible, that we match the file with the correct mime-type.

We then open our package.opf file in write mode.

The last step in this stage is to create the glob expression that will tell the rest of the scripts what files to work with.

We are now ready to create the content we’ll write to the file.

#!/usr/bin/env python 

import mimetypes
import glob
import os
import os.path

# Initialize the mimetypes database
mimetypes.init()
# Create the package.opf file
package = open('package.opf', 'w')

# WARNING: This glob will add all files and directories 
# to the variable. You will have to edit the file and remove
# empty directories and the package.opf file reference from
# both the manifest and the spine
package_content = glob.glob('OEBPS/**/*')

The second stage is to create the templates for the XML portions of the package. There are two things to notice with this part.

  • The XML elements are empty. I only create attributes as necessary
  • I create static templates and don’t use dynamic content because all the modules I found had issues when working with namespaces.

The three templates will be used when building the file.

template_top = '''<package xmlns="http://www.idpf.org/2007/opf"
  unique-identifier="book-id"
  version="3.0" xml:lang="en">
  <metadata >
    <!-- TITLE -->
    <dc:title></dc:title>
    <!-- AUTHOR, PUBLISHER AND PUBLICATION DATES-->
    <dc:creator></dc:creator>
    <dc:publisher></dc:publisher>
    <dc:date></dc:date>
    <meta property="dcterms:modified"></meta>
    <!-- MISC INFORMATION -->
    <dc:language>en</dc:language>
    <dc:identifier id="book-id"></dc:identifier>
    <meta name="cover" content="img-cov" />
  </metadata>
  <manifest>
  '''

template_transition = '''</manifest>
  <spine toc="ncx">'''

template_bottom = '''</spine>
</package>

The enumeration builds the dynamic section of the file. We first create two variables to hold the content and spine of the manifest.

For each element of our package_content (the content of the OEBPS directory) we do the following:

  • Set the basename variable to the part of the current item
  • Get the mime type for the item
  • Add the item XML tag to the manifest assigning it an ID, the base path and the mime type
  • Add the item to the spine by creating the idref element with an ID matching the one we used for the item tag above

When we complete this section, we have a list of all the files under OEBPS and are now ready to, finally, build the package file.

manifest = ""
spine = ""

for i, item in enumerate(package_content):
  basename = os.path.basename(item)
  mime = mimetypes.guess_type(item, strict=True)
  manifest += 't<item id="file_%s" href="%s" media-type="%s"/>n' % (i+1, basename, mime[0])
  spine += 'nt<itemref idref="file_%s" />' % (i+1)

After all the work, actually creating the file is almost anti climatic. We print each section in the following order:

  • template_top
  • manifest
  • template_transition
  • spine
  • template_bottom
# I don't remember my python all that well to remember 
# how to print the interpolated content. 
# This should do for now.
package.write(template_top)
package.write(manifest)
package.write(template_transition)
package.write(spine)
package.write(template_bottom)

An example of the complete file looks like this:

<package xmlns="http://www.idpf.org/2007/opf"
  unique-identifier="book-id"
  version="3.0" 
  xml:lang="en">
  <metadata >
    <!-- TITLE -->
    <dc:title></dc:title>
    <!-- AUTHOR, PUBLISHER AND PUBLICATION DATES-->
    <dc:creator></dc:creator>
    <dc:publisher></dc:publisher>
    <dc:date></dc:date>
    <meta property="dcterms:modified"></meta>
    <!-- MISC INFORMATION -->
    <dc:language>en</dc:language>
    <dc:identifier id="book-id"></dc:identifier>
    <meta name="cover" content="img-cov" />
  </metadata
  <manifest>
    <item id="file_1" href="styles.css" media-type="text/css"/>
    <item id="file_2" href="type" media-type="None"/>
    <item id="file_3" href="book_cover.jpg" media-type="image/jpeg"/>
  </manifest>

  <spine toc="ncx">
    <itemref idref="file_1" />
    <itemref idref="file_2" />
    <itemref idref="file_3" />
  </spine>
</package>

Thing to remember

This is not a complete solution. It is a starting point and it will require manual edits before it passes validation. It is still better than starting from scratch, at least in my opinion.

Things to work on

The first thing I need to figure out is how to skip or remove empty folders. In the example above the media folder needs to be removed manually before the package file will pass epubcheck validation.

Another thing I’ll have to research is whether the glob expression takes all the files we need. For geeks, how many levels deep does the glob expression go?

Exclusions

I’m sad to see the potential of exclusions not being used. No browser vendor supports the complete exclusion specification. IE 10+ is the one that comes the closes but doesn’t support the full set of exclusion features.

This is the best we have (and yes, it still feels weird to say that IE is the best we have in terms of a web feature)

The idea of exclusions is complementary to that of shapes. As a matter of fact, there was only one specification addressing both shapes and exclusions but they were split in 2012, I guess to ease development of at least one of the sections of the specification.

The spec has two primary CSS attributes: wrap-flow and wrap-through.

wrap-flow

Wrap-flow tells the browser how to wrap the content. One thing to notice is that instead of using left and right as attribute values it uses start and end to avoid confusions with right-to-left and top to bottom languages where the meaning of start and end is different.

I based each attribute definition on the how the specification defines it.

  • wrap-flow: auto;

This will not create an exclusion for floated elements. It has no effect on other, not floated, elements. This is the default value for wrap-flow

exclusion_wrap_side_auto

  • wrap-flow: both;

Flows content on both sides of the element

exclusion_wrap_side_both

  • wrap-flow: start;

Inline content can wrap on the start edge of the exclusion area (this would be the left edge for LTR languages.) It must leave the end edge clear

exclusion_wrap_side_left

  • wrap-flow: end;

Inline flow content can wrap on the end side of the exclusion area but must leave the area to the start edge of the exclusion area empty. This is the reverse of the start value.

exclusion_wrap_side_right

  • wrap-flow: maximum;

Inline flow content wraps on the side of the exclusion with the largest available space for the given line, and must leave the other side of the exclusion empty. The space can happen on either side of the content, as shown in the examples below:

Example of wrap-flow: maximum wrapped from the left side
Example of wrap-flow: maximum wrapped from the right side
Example of wrap-flow: maximum wrapped from the left side
Example of wrap-flow: maximum wrapped from the left side
  • wrap-flow: clear;

Inline content flows top and bottom of the exclusion, leaving the start and end sides clear.

exclusion_wrap_side_clear

wrap-through

This property controls whether content wraps around this particular element or not. According to the specification, if the value of the wrap-through property is to wrap:

The element inherits its parent node’s wrapping context. Its descendant inline content wraps around exclusions defined outside the element.

If the value is to none content will not wrap around the element

Combination of exclusions and shapes

Examples taken from the CSS WG use case wiki

One of the best things about exclusions is that they work almost intuitively with shapes as in the examples below. Note that because exclusions are a working draft, the syntax, is not finalized and, most likely, not be supported by your browser (even IE 10+)

I still chose to include the examples as an illustration of what, I hope, is to come

Basic shaped exclusion example

csswg_exclusions_v1

In a two column text frame we create a circle shape at the center and use the shape as an exclusion where we flow the content around both sides using wrap-flow: both;

Padding and margins in exclusions

csswg_exclusions_v7

Adding background to a shaped exclusion

csswg_exclusions_v8

Tutorials and Examples

Using media queries to handle HDPI screens

One of the things that I find infuriating about multi device development is that people can’t seem to agree on a resolution or a device aspect ratio. This has become even more complicated with the advent of retina devices (both hand-held, laptops and desktop devices).

Trying to keep an identical layout in different screen sizes is impossible. Even if we could create identical experiences, the amount of image replacement we have to do to accommodate the different resolutions so our images will not look like crap just is more than I’d like to deal with.

A compromise solution is to use Media Queries to target the work to the devices that we are planning to work with. This post will discuss a few ways in which we can use media queries to target devices based on resolution and orientation.

Before we start

Before we start it’s a good idea to gather references and start looking for a solution generic enough to work across our target devices. I found the following resources to start with:

NOTE: I used SASS to write all the queries below and included them in SASS stylesheets. I don’t like reinventing the wheel so these queries go into a SASS partial to use when needed.

The basic query: Device Pixel Ratio

I based the first query on bourbon hidpi-media-queries file and designed it as a generic query for hdpi browsers. Because it is generic we can’t really use it for specific devices or orientations but it provides a good starting point when tied to information from bjango (see link above).

// HiDPI mixin. 
@mixin hdpi($ratio: 1.3) {
  @media only screen and (-webkit-min-device-pixel-ratio: $ratio),
  only screen and (min--moz-device-pixel-ratio: $ratio),
  only screen and (-o-min-device-pixel-ratio: #{$ratio}/1),
  only screen and (min-resolution: #{round($ratio*96)}dpi),
  only screen and (min-resolution: #{$ratio}dppx)
  {
    @content;
  }
}

One of the things this will allow you to do is substitute images for the proper 2x or 3x resolution. The query will only match one value at a time so there shouldn’t be a problem in using multiple versions of the query with the DPI we want (1, 1.3, 1.5, 2 and maybe 3 if we’re targeting the really high-resolution devices.)

Apple and other hardware vendors have introduced retina / high DPI screens on desktop and laptop systems. We take this into account by introducing prefixed values for our media query corresponding to the vendors that need the prefix (-webkit for Safari, Chrome and Opera (after the blink adoption); -moz for Firefox and -o for older versions of Opera using the Presto rendering engine.)

Where this query falls short is when we try to target specific sizes or devices. That’s where our queries get a little more complicated and need more research.

Device specific queries: iOS devices

Where our first query worried only about DPI, our next set of queries address the following elements of the mobile experience:

  • DPI
  • Device size
  • Device orientation

For each query we create the SASS mixin using a default orientation value of all to use indepeendent of the device orientation. We can also create device specific portrait and landscape rules for each device.

Here are some queries specific to iPhones from 3 to 6+. Peculiarities for each device will be noted below the query.

iPhone 3

@mixin iphone3($orientation: all) {
  $deviceMinWidth: 320px;
  $deviceMaxWidth: 480px;
  $devicePixelRatio: 1;

  @if $orientation == all {
    @media only screen and (min-device-width: $deviceMinWidth) 
      and (max-device-width: $deviceMaxWidth)
      and (-webkit-device-pixel-ratio: $devicePixelRatio) {

      @content;
    }
  }
  @else {
    @media only screen and (min-device-width: $deviceMinWidth) 
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio) 
    and (orientation:#{$orientation}) {
      @content;
    }
  }
}

Since the iPhone 3 is standard DPI I debated whether to include it on the list. Decided to add it because it uses both landscape and portrait orientations issue and because it’s as far back as I wanted to go with this

iPhone 4 and iPhone 5

// iphone4
@mixin iphone4($orientation: all)
{
  $deviceMinWidth: 320px;
  $deviceMaxWidth: 480px;
  $devicePixelRatio: 2;
  $deviceAspectRatio: '2/3';

  @if $orientation == all
  {
    @media only screen 
    and (min-device-width: $deviceMinWidth) 
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio) 
    and (device-aspect-ratio: $deviceAspectRatio) {
      @content;
    }
  }
  @else {
    @media only screen and (min-device-width: $deviceMinWidth) 
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio) 
    and (device-aspect-ratio: $deviceAspectRatio) 
    and (orientation:#{$orientation})
    {
      @content;
    }
  }
}
/* iphone-5 */
@mixin iphone5($orientation: all)
{
  $deviceMinWidth: 320px;
  $deviceMaxWidth: 568px;
  $devicePixelRatio: 2;
  $deviceAspectRatio: '2/3';

  @if $orientation == all {
    @media only screen 
    and (min-device-width: $deviceMinWidth) 
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio) 
    and (device-aspect-ratio: $deviceAspectRatio) {
      @content;
    }
  }
  @else {
    @media only screen 
    and (min-device-width: $deviceMinWidth) 
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio) 
    and (device-aspect-ratio: $deviceAspectRatio) 
    and (orientation:#{$orientation}) {
      @content;
    }
  }
}

The main difference between the iPhone 4 and iPhone 5 is the device’s speed. Screen size and device pixel ratio are the same for both devices.

iPhone 6 and 6+

// iphone 6
@mixin iphone6($orientation: all)
{
//1334 ×750
  $deviceMinWidth: 750px;
  $deviceMaxWidth: 1334px;
  $devicePixelRatio: 2;
  $deviceAspectRatio: '40/71';

  @if $orientation == all
  {
    @media only screen 
      and (min-device-width: $deviceMinWidth) 
      and (max-device-width: $deviceMaxWidth)
      and (-webkit-device-pixel-ratio: $devicePixelRatio) 
      and (device-aspect-ratio: $deviceAspectRatio) {

      @content;
    }
  }
  @else {
    @media only screen 
      and (min-device-width: $deviceMinWidth) 
      and (max-device-width: $deviceMaxWidth)
      and (-webkit-device-pixel-ratio: $devicePixelRatio) 
      and (device-aspect-ratio: $deviceAspectRatio) 
      and (orientation:#{$orientation})
    {
      @content;
    }
  }
}
// iPhone 6+
// 1242 × 2208 px

@mixin iphone6plus($orientation: all) {
//1334 ×750
  $deviceMinWidth: 1242px;
  $deviceMaxWidth: 2208px;
  $devicePixelRatio: 3;
  $deviceAspectRatio: '40/71';

  @if $orientation == all
  {
    @media only screen 
      and (min-device-width: $deviceMinWidth) 
      and (max-device-width: $deviceMaxWidth)
      and (-webkit-device-pixel-ratio: $devicePixelRatio) 
      and (device-aspect-ratio: $deviceAspectRatio) {

      @content;
    }
  }
  @else {
    @media only screen 
      and (min-device-width: $deviceMinWidth) 
      and (max-device-width: $deviceMaxWidth)
      and (-webkit-device-pixel-ratio: $devicePixelRatio) 
      and (device-aspect-ratio: $deviceAspectRatio) 
      and (orientation:#{$orientation})
    {
      @content;
    }
  }
}

Unlike the iPhone 4 and 5 there is a real difference between the iPhone 6 and 6+. They have different device pixel ratio and different sizes. If we will support both devices then we need to use both queries.

Non Retina iPad

/* non-retina ipads (1 and 2) */
@mixin ipad($orientation: all) {
  $deviceMinWidth: 768px;
  $deviceMaxWidth: 1024px;

  @if ($orientation == all) {
    @media only screen and (min-device-width: $deviceMinWidth)
      and (max-device-width: $deviceMaxWidth) {

      @content;
    }
  }
  @else {
    @media only screen and (min-device-width: $deviceMinWidth)
    and (max-device-width: $deviceMaxWidth)
    and (orientation:#{$orientation}) {
      @content;
    }
  }
}

iPad 1 and 2 use the same size display, the same device pixel ratio and device aspect ratio. We don’t need specialized queries for each.

Retina iPad (iPad 3 and 4)

/* ipad-retina */
@mixin ipad-retina($orientation: all) {
$deviceMinWidth: 768px;
$deviceMaxWidth: 1024px;
$devicePixelRatio: 2;
$deviceAspectRatio: '4/3';

@if ($orientation == all)   {
  @media only screen and (min-device-width: $deviceMinWidth)
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio) {
      @content;
  }
}
@else   {
  @media only screen and (min-device-width: $deviceMinWidth)
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio)
    and (orientation:#{$orientation}) {

    @content;
  }
}

iPad 3 and 4 are retina devices

Device specific queries: Kindle Fire devices

I’m also working on queries specific to the Kindle Fire line of devices. These queries have not been tested as extensively as the iOS queries. If you use them and they work, please let me know via email or by posting an issue to report the results.

Non HDPI Kindle Fire

As with iPhones and iPad we use the non hdpi Kindle Fire as our baseline device.

/* Kindle fire*/
@mixin kindle-fire($orientation: all){
//Model           resolution      PPCM (PPI)  Pixel Ratio
//Kindle Fire     1024x600        67 (170)    1.0 (notHDPI)
$deviceMinWidth: 600px;
$deviceMaxWidth: 1024px;


@if ($orientation == all) {
@media only screen 
  and (min-device-width: $deviceMinWidth)
  and (max-device-width: $deviceMaxWidth) {

  @content;
  }
}

@else {
@media only screen 
  and (min-device-width: $deviceMinWidth)
  and (max-device-width: $deviceMaxWidth)
  and (orientation:#{$orientation}) {
  @content;
  }
}

Kindle Fire 7 Inch

@mixin kindlef-fire7in($orientation:all){
//                     resolution      PPCM (PPI)  Pixel Ratio
//Kindle Fire HD 7"    1280x800        85 (216)    1.5 hdpi
//Kindle Fire HDX 7"   1920x1200       127 (323)   1.5 xhdpi
$deviceMinWidth: 800px;
$deviceMaxWidth: 1200px;
$devicePixelRatio: 1.5;

@if ($orientation == all)   {
  @media only screen and (min-device-width: $deviceMinWidth)
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio) {

    @content;
  }
}
@else   {
  @media only screen and (min-device-width: $deviceMinWidth)
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio)
    and (orientation:#{$orientation}) {

    @content;
  }
}

There are two Kindle devices with 7″ screens. One uses HDPI and one uses as an XHDPI device. Need to do further testing to see if they both work well with the same resolution image or if we need to split the query into specific PPI devices

Kindle Fire 8.9 Inch

@mixin kindle-fire89in($orientation:all) {
//Model                   resolution  PPCM (PPI)  Pixel Ratio
//Kindle Fire HD 8.9"     1920x1200   100 (254)   1.5 hdpi
//Kindle Fire HDX 8.9"    2560x1600   133 (339)   1.5 xhdpi
$deviceMinWidth: 1200px;
$deviceMaxWidth: 1600px;
$deviceAspectRatio: '40/71';
$devicePixelRatio: 1.5;

@if ($orientation == all)   {
  @media only screen and (min-device-width: $deviceMinWidth)
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio) {

    @content;
  }
}
@else   {
  @media only screen and (min-device-width: $deviceMinWidth)
    and (max-device-width: $deviceMaxWidth)
    and (-webkit-device-pixel-ratio: $devicePixelRatio)
    and (orientation:#{$orientation}) {

    @content;
}
}

So how do we use these queries?

As of SASS 3.2 we can use the @content attribute to pass values to the function, not just parameters.

Using the mixins as defines above. We can do something like this to take into account for different pixel densities:

body {
/* Add other attributes as needed */

@include iphone5() { 
  /* styles independent of orientation*/
}
@include iphone5(portrait) {
  background-image: url(http://example.com/bck-iphone-portrait-x2.png);
}
@include iphone5(landscape) {
  background-image: url(http://www.example.com/bck-iphone-portrait-x2.png);
}

@include iphone6plus() {
  background-image: url(http://www.example.com/bck-iphonex3.png);
}


@include ipad(){ 
  background-image: url(http://www.example.com/bck-ipadx1.png);
}

If we’re only worried about pixel ratio, we can use our generic query to just replace some aspects of our content without worrying about device size or other device specific aspects. The sass rule looks like this:

body {

  /* generic content goes here */

  @include hdpi(1.3) {
    /* content for 1.3 HDPI devices */
  }

  @include hdpi(1.5) {
    /* content for 1.5 HDPI devices */
  }

  @include hdpi(2.0) {
    /* content for 2.0 HDPI devices */
  }

  @include hdpi(3.0) {
    /* content for 3.0 HDPI devices */
  }
}

Outstanding questions

Trying to future proof the queries I found out that the current editor draft of Media Queries Level 4 deprecates device-aspect-ratio as a valid media query.

I’m exploring whether to replace it with resolution as the CSS working group wants to use moving forward (as indicated in the email thread starting here) or wait until Media Queries Level 4 moves to Candidate Recommendation, at which point there will be several interoperable implementations (meaning Apple and Microsoft will get on with the program and adopt the same attributes as everyone else.

It is important to note that for iPhone and iPad queries we are ok with using device-pixel-ratio as it’s an Apple device and Safari supports it, where it will become problematic moving forward is non-Apple devices.

Creating Print CSS stylesheets

Rather than adopt a paged media model for printing HTML5/CSS pages let’s look at how to move our content to make for a better printing experience. The idea for this stylesheet came from Evernote’s simplified article capture mode.

This will be very dependent on the layout of your page but there are some common rules we can use everywhere, both css3 and HTML.

NOTE: I wrote all the material below using SASS SCSS syntax. This will allow me to use them with other SASS-based projects and stylesheets while remaining compatible with CSS.
Also note that this is by no means a complete print stylesheet. It definitely can be improved. If you have any ideas, please let me know via Twitter (@elrond25)or in a comment below

Defining the print stylesheet

At the most basic level, creating a print specific stylesheet is as simple as indicating its media type. This has been available since CSS 2.1 and it works like this:

@media print {
  /* style sheet for print goes here */
}

We can then use this shortened media query to style the printed output in the way we want. For example, if we want a different font size margins and line height for our printed output, we can do something like this:

body {
  font-size: 16px;
  line-height: 1.5;
}

@media print {
  body {
    margin: 1in;
    font-size: 12pt;
    line-height: 2;
  }
}

Now that we have a basic understanding of how to use the stylesheet let’s plan on what do we need to do to get a better print result for our pages.

What do we need to change?

Because we cannot predict the changes needed for all pages, I will just work on generic aspects of the printed styles.

Margins and base font sizes (body and headings)

Printed web pages usually go to the border of the paper. We can avoid that by adding margins to the body element. How much depends on how you want the page to look.

For this example we’ll set top, right and left margins to be an inch and the bottom margin to be an 1.5 inch. Most of the time I would leave my margins at 1 inch, but research into layout has made me add the extra space at the bottom.

We’ve also explicitly set the font size of the body element to be 16px (even thought this may or may not be necessary). Since I’m setting it to what the default is, I don’t think it causes too much of a problem.

To better size the headings I created a modular scale using Scott Brown’s website modularscale.com

One final note. I only styled h1, h2 and h3. I’ve never used headings beyond h3 and I don’t really see the need to have more than 3 levels of heading.

body {
  margin: 1in 1in 1.5in;
  line-height: 1.5;
}

h1 {
  font-size: 2em;
}

h2 {
  font-size: 1.75em;
}

h3 {
  font-size: 1.5em;
}

Removing background images and text color

Background images can be a royal pain. They use a lot of ink / toner and they may impact the way the rest of the content displays when printed.

Text color may also impact the way content appears when printed, particularly in black and white printers.

Our simple solution is to remove background and set the text color to black

body {
  background-image: none;
  color: #000;
}

Removing multimedia objects

Beyond images we don’t really need any more multimedia, audio, video and objects elements; printed pages can play them and they will use ink/toner unnecessarily, so we’ll hide them for our printed style sheet.

If you want your readers to see the poster images for video, the first frame of your object or the audio player ignore this rule

video,audio, object {
  display: none
}

Creating columns

Rather than have one long page of text we can create columns. The mixin below creates columns based on number of columns, gutter, text balance between columns and text span between the columns.

After the mixin we define classes for 2 and 3 column layouts spanning 100% of the available width of the document. We can use them as a model for more columns with different information.

We could code each column setup manually but why reinvent the wheel?

@mixin column-attribs ($cols, $gap, $fill: balance, $span: none){
  // How many columns
  -moz-column-count: $cols;
  -webkit-column-count: $cols;
  column-count: $cols;

  // Space between columns
  -moz-column-gap: $gap;
  -webkit-column-gap: $gap;
  column-gap: $gap;

  // How do we fill  our columns, 
  // default is to balance
  -moz-column-fill: $fill;
  -webkit-column-fill: $fill;
  column-fill: $fill;

  // Column span, default is not to span columns
  -moz-column-span: $span;
  -webkit-column-span: $span;
  column-span: $span;
}

.columns2 {
  width: 100%;
  @include column-attribs (2, 20px);
}

.columns3 {
  width: 100%;
  @include column-attribs (3, 10px);
}

Putting it all together

Putting all the elements together the style sheet may look something like this.

@media print {
  body {
    margin: 1in 1in 1.5in;
    font-size: 12pt;
    line-height: 1.5;
    background-image: none;
    color: #000;
  }

  h1 {
    font-size: 2em;
  }

  h2 {
    font-size: 1.75em;
  }

  h3 {
    font-size: 1.5em;
  }

  video, audio, object {
    display: none
  }

  .columns2 {
    width: 100%;
    @include column-attribs (2, 20px);
  }

  .columns3 {
    width: 100%;
    @include column-attribs (3, 10px);
  }
}

Where do the print stylesheets fall short?

First of all, this is a simplified view of the web page to make printing easier. It is not a dedicated printing solution like CSS Paged Media using generated content and it will not work with Antenna House or PrinceXML.

If you want to see what that type of stylesheet looks like see my CSS Paged Media update

Second, these is a generic exercise. A lot of the more detailed conversions will depend on your specific page structure and how much fidelity you want to the screen version of the site.

CSS shapes: an update and an expansion

CSS Shapes (spec) allow you a finer grained control of how you flow the text around shapes and images.

Only Chrome and Opera (both using the Blink rendering engine) and the beta version of Safari (iOS 8 and OS X 10.10) support css shapes.

To illustrate the idea of shapes, look at the following image, taken from Sara Souedan’s A List Apart article:

Examples of CSS Shapes

Look at the way the list of ingredients on the second and third recipes. That effect is achieved with shapes.

The HTML is fairly simple, we create the shape by placing the element we want to float (the paragraph) and the element we want to float around (the image)

In the CSS portion we define three elements:

  • The container for the document (#circle-shape-example)
  • paragraph text (#circle-shape-example p)
  • image (#circle-shape-example .curve).

The result can be see in the Codepen below:

See the Pen Wrapping Text Around A Circular Shape by Carlos Araya (@caraya) on CodePen.

The specification has one shape type:

  • shape-inside()
  • shape-outside()

Only shape outside is currently supported. The original specification included both shape-inside and shape-outside but the complexity of shape-inside made the CSS working group defer shape-inside until the level 2 specification is released.

There are 5 shape functions defined in the specification for outside shapes:

  • circle()
    • shape-outside: circle();

    The default version will just create a circle centererd around the object being shaped around.

    • shape-outside: circle(250px);
    • shape-outside: circle(100%);
    • shape-outside: circle(closest-side);

    • shape-outside: circle(farthest-side);

The value inside the circle function indicates the radius of the circle being used. This will in turn affect the way the shape wraps around the content, particularly if it’s an image.

You can use a percentage value instead of an absolute measurement. In the case of a circle the value is computed from the used width and height of the reference box as sqrt(width2+height2)/sqrt(2) (from the specification)

Closest and farthest side refer to two predefined values of (shape radius).

Closest side will make the content fit into the box assigned to it. The content will not clip but will be shrunk to keep within the bounding box.

Farthest side uses the length from the center of the shape to the farthest side of the reference box. For circles, this is the farthest side in any dimension.

  • shape-outside: circle(250px at center);
  • shape-outside: circle(farthest-side at center);
  • shape-outside: circle(closest-side at center);

Position values are defined in the CSS background level 3 specification. You can specify this position by using the at {position} syntax.

  • ellipse()
    • shape-outside: ellipse();
    • shape-outside: ellipse(25%);
    • shape-outside: ellipse(25% 10%)
    • shape-outside: ellipse(closest-side);
    • shape-outside: ellipse(closest-side closest-side);

Ellipse is very similar to circle in terms of syntax but it work with ellipses rather than circles and it sues 2 values to define the shape. An example can be seen below:

See the Pen CSS Shapes Demo #6 by Carlos Araya (@caraya) on CodePen.

If the floated element is a circle then an ellipse works the same way as a circle shape.

  • inset()
    • shape-outside: inset(0px round 50px) border-box;
    • shape-outside: inset(10px 10px round 50px) border-box;
    • shape-outside: inset(10px 10px 10px round 50px);
    • shape-outside: inset(10px 10px 10px 10px round 50px);

Inset allows you to create rectangular areas to float content around. This works with either for images that are a rectangles or where we want to overlap text over a rectangular shape.

Inset shapes also allow us to create rounded corners for the text to float around as show in examples 2, 3 and 4 above and in the pen below:

See the Pen CSS Shape inset demo by Carlos Araya (@caraya) on CodePen.

Pen forked from http://codepen.io/SaraSoueidan/pen/05e7894a0a7dbffed0a1c9f5e0840ec9

  • polygon()

Polygon values for shape-outside allow designers to create layouts wraping text around irregularly shaped closed polygons.

The minimum number of vertices for a polygon shape is three (a triangle).

  • shape-outside: polygon(0 0, 0 300px, 300px 600px);

Using percentages makes our polygon responsive.

  • shape-outside: polygon(0 0, 0 100%, 100% 100%);

Another value imported from SVG is fill-rule which handles how to handle self-intersecting paths or enclosed shapes. Joni Trythall wrote a tutorial about Understanding the SVG fill-rule Property.

Posible values are non-zero and even-odd. Thedefault value if you choose not to fill it, is non-zero

The pen below shows one possible use for polygons where we create a trapezoid-shaped image and then float the text ouside the image which gives the full page the appearance of a magazine layout.

See the Pen CSS Shapes Demo #11 by Carlos Araya (@caraya) on CodePen.

  • url(), images and thresholds
    • shape-outside: url(path/to/image-with-shape.png);
    • shape-image-threshold: 0.5;

To me, one of the coolest features of the shape specification is the ability to use images as the object we wrap text around.

The URL to the image specified an arbitrary image that we can wrap our content around.

The image-threshold attribute indicates the minimal opacity of the pixels making up the shape that content will flow around.

See the Pen CSS Shapes Demo #9 by Carlos Araya (@caraya) on CodePen.

See the SVG section for a way to use masks in addition to the images to create more nuanced shapes to wrap the text around.

Positioning the shape

There are addition values for the shape-outside element: margin-box, border-box, padding-box and content-box can be used to place the shape within the different margins associated with an object.

According to HTML 5 Rocks’ Getting started with CSS Shapes you can also use the bounding boxes on their own or with other CSS rules to create shapes.

The author gives two examples:

The first one creates a circle shape with border-radius and shape-outside: border-box;

The second example creates a pullquote-type effect using shape-outside: content-box;

They may not be as flashy as having text wrap around a 20 vertice polygon but the effects are just as useful.

Razvan Caliman explain the different boxes, their location and how they interact with the different types of shapes currently implemented.

See the Pen CSS Shapes Demo #10 by Carlos Araya (@caraya) on CodePen.

Simulating shape-inside

When first released the shapes specification included both shape-inside and shape-outside. Shape inside has been deferred to the next level of the specification.

Even shape-inside is not part of the current specification, it can be simulated using a couple non semantic elements and two elements styled with shape-outside placed in opposite sides of the window.

To see an example of this type of use of shapes, look at Adobe’s Alice in Wonderland demo.

Be aware that performance is definitely below ideal. The way the example implements the text scrolling triggers a lot of relayout and paint events. This will reduce performance. This performance issue will be solved when native implementations of shape-inside appear in browsers. Until then we need to be aware of the performance hit the current technique implies.

CSS masks and shapes working together

The CSS Masking specification provides two additional rules we can use in working with our shapes: clip-path and mask-image.

Fortunately for us the syntax for clip-path is identical to the syntax for shape-outside so you can do the following to make sure that only the shape you select appears on screen:

img {
  -webkit-shape-outside: circle(50%);
  shape-outside: circle(50%);
  -webkit-clip-path: circle(50%);
  clip-path: circle(50%);
}

I code deffensively. The unprefixed versions of the code may not be necessary but I add it anyways to make sure that when/if browser vendors decide to drop prefixes my code will not break down right away.

Using masks in SVG and CSS talks about general use of masks and clip paths both generated directly from CSS and masks generated with SVG.

Sara Soueidan further explains how to use clipping in CSS and what effects you can achieve with the technique.

Rebecca Hauck shows how to use Adobe Photoshop to create more refined masking image.

Limitations

Shape implementations only work with floated content. Updates to the specifications will work with non-floated content but we’re not there yet, it is expected for the second edition of the shape specification.

You must specify dimensions for the object you’re floating around. The browser will use those dimensions to establish a coordinate set for the element. This may or may not be accurate but I’ve always been a fan of coding defensively so I add explicit dimensions in the css class(es) assigned to regions, if needed, change them with Javascript later.

Browser support

Data obtained from caniuse.com accessed 09/15/2014

  • Firefox: Not suported
  • IE: Under Consideration (per status.modern.ie)
  • Safari / Safari Mobile: 8 (using -webkit prefix)
  • Opera: Since version 24
  • Opera Mini: Not supported
  • Chrome / Chrome for Android: Since version 37 (must enable “experimental Web Platform features” flag in chrome://flags)
  • Android Browser: Not supported

Working examples and sources of inspiration (both print and online)

Tools to create CSS shapes

Looking at the future of shapes

CSS Shapes Level 2 editor draft from the W3C CSS working group is the next iterations of the shapes specifications.

Unless it is deferred again, the next specification should contain both shape-outside and shape-inside which will make even better shape related models possible. Until then let’s play with what we have.

Theory and Practice of Digital Publishing

%d bloggers like this: