Gatwood Publishing

Gatwood Publishing: EPUB Tips

EPUB Tips:

When producing EPUB content, you'll run into a number of bugs specific to particular readers. This page contains tips to help you work around them.

For additional tips, see the MobileRead Wiki's EPUB and MOBI pages.

iBooks and Kindle SVG Filter

Some versions of the iBooks EPUB reader choke on SVG when you have more than one <tspan> tag with positioning information inside a <text> tag. Additionally, some versions of iBooks have trouble dealing with fractional point sizes for fonts. These issues can cause erratic, nondeterministic rendering, with text appearing in the wrong place, often on top of other text. Sometimes, the problems even appear and disappear as you rotate the device.

Kindle seems to handle SVG fairly well, but KDP (the website that you use to actually publish Kindle books) does not. It incorrectly converts px units to rem units in styles that apply to SVG code, resulting in very tiny text that is only a pixel or two tall. (Fortunately, there are other ways to set the font size.)

For hand-crafted SVG, these bugs are largely a non-issue, because they're easy to avoid. For machine-generated SVG—particularly content converted from PDF—these bugs are a royal pain in the backside.

This Perl code snippet takes a block of SVG code and splits any <text> elements that contain more than one <tspan> element, making any id tags unique as it does so, and truncates any fractional point sizes to the nearest whole number. It also strips out a lot of noise that probably doesn't belong in your published output. Finally, it replaces font-size CSS declarations with font-size attributes on the tags themselves.

sub filtersvg($)
    my $string = shift;
    my $outstring = "";

    my $localDebug = 0;

    my $iBooksSVGHacks = 1;

    $string =~ s/.*?<\/rdf:RDF>//sg;

    my @parts = split(///s;
		$startoftag = "";

	$outstring .= $startoftag;

	my ($inside, $outside) = split(/>/, $part, 2);

	if ($inside =~ /^text(\s|$)/) {
		$lastTextTag = $inside;
		$firstTextSpanInText = 1;
	} elsif ($inside =~ /^\/text/) {
		$lastTextTag = "";
		$firstTextSpanInText = 0;

	# split multiple tspan tags into separate text tags (rdar://18040167)
	if ($inside =~ /^tspan(\s|$)/ && length($lastTextTag)) {
		if ($firstTextSpanInText) {
			$firstTextSpanInText = 0;
		} else {
			my $hackedLastTextTag = $lastTextTag;
			$hackedLastTextTag =~ s/id="[^"]*"/id="addedTextSpan_$idcounter"/s;

			$outstring .= "/text><$hackedLastTextTag><";

	if ($inside =~ /^svg(\s|$)/) {
		my $width = $inside;
		$width =~ s/^.*width="//s;
		$width =~ s/".*$//s;

		my $height = $inside;
		$height =~ s/^.*height="//s;
		$height =~ s/".*$//s;

		$inside .= " viewBox=\"0 0 $width $height\" preserveAspectRatio=\"xMidYMid\"";
		$inside =~ s/width="[^"]*"/width="100%"/;
		$inside =~ s/height="[^"]*"/height="100%"/;

	# Let CSS handle the text fill color to prevent problems with
        # night mode.  Note: Remove this line for images.
	$inside =~ s/\s*fill:\s*(#000000|black);//sg;

        if ($iBooksSVGHacks) {
                # Truncate any fractional font sizes (rdar://18729982)
                $inside =~ s/font-size:\s*(\d+)\.\d*px/font-size: $1px/sg;

        if ($kindlehacks) {
                # KDP (Amazon's publishing system) incorrectly converts
                # all font-size CSS properties from px sizes to rem
                # (root em) sizes whenever you submit the book.  This is,
                # of course, completely incorrect in the context of SVG.
                # To work around this bug in KDP, we convert the font-size
                # CSS declaration into an attribute on the element itself.

                my @insidetags = split(/([<>])/, $inside);
                my $newinside = "";
                if ($inside =~ s/font-size:\s*(\d+)px;?//) {
                        $inside = "$inside font-size=\"$1\"";

	# For maximum compatibility with older ADE-based readers, provide the font 
	# with an attribute, not with CSS.
	if ($inside =~ s/font-family:\s*(.*?);// ||
	    $inside =~ s/;\s*font-family:\s*(.*?)(["'])/$2/) {

	    my $font = $1;

	    $font =~ s/\s//sg;
	    $font = "useFont_$font";
	    $inside .= " class=\"$font\"";

	# Strip out Inkscape-specific content to avoid validation issues.
	if ($inside =~ s/-inkscape-font-specification:\s*(.*?);// ||
	    $inside =~ s/;\s*-inkscape-font-specification:\s*(.*?)(["'])/$2/) {
		$outstring .= "stripped inkscape font spec\n" if ($localDebug);

	# Strip out other unnecessary attributes to avoid validation issues.
	$inside =~ s/\sxmlns:rdf=\".*?\"//s;
	$inside =~ s/\sxmlns:cc=\".*?\"//s;
	$inside =~ s/\sxmlns:dc=\".*?\"//s;
	$inside =~ s/\sxmlns:inkscape=\".*?\"//s;
	$inside =~ s/\sinkscape:[^=]*=\".*?\"//s;

	if ($startoftag) {
		$outstring .= $inside.">".$outside;
	} else {
		$outstring .= $outside;

    return $outstring;