diff mupdf-source/source/html/css-apply.c @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/mupdf-source/source/html/css-apply.c	Mon Sep 15 11:43:07 2025 +0200
@@ -0,0 +1,1835 @@
+// Copyright (C) 2004-2024 Artifex Software, Inc.
+//
+// This file is part of MuPDF.
+//
+// MuPDF is free software: you can redistribute it and/or modify it under the
+// terms of the GNU Affero General Public License as published by the Free
+// Software Foundation, either version 3 of the License, or (at your option)
+// any later version.
+//
+// MuPDF is distributed in the hope that it will be useful, but WITHOUT ANY
+// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
+// FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
+// details.
+//
+// You should have received a copy of the GNU Affero General Public License
+// along with MuPDF. If not, see <https://www.gnu.org/licenses/agpl-3.0.en.html>
+//
+// Alternative licensing terms are available from the licensor.
+// For commercial licensing, see <https://www.artifex.com/> or contact
+// Artifex Software, Inc., 39 Mesa Street, Suite 108A, San Francisco,
+// CA 94129, USA, for further information.
+
+#include "mupdf/fitz.h"
+#include "html-imp.h"
+
+#include <string.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <assert.h>
+
+static const char *border_width_kw[] = {
+	"medium",
+	"thick",
+	"thin",
+};
+
+static const char *border_style_kw[] = {
+	"dashed",
+	"dotted",
+	"double",
+	"groove",
+	"hidden",
+	"inset",
+	"none",
+	"outset",
+	"ridge",
+	"solid",
+};
+
+static const char *color_kw[] = {
+	"aqua",
+	"black",
+	"blue",
+	"fuchsia",
+	"gray",
+	"green",
+	"lime",
+	"maroon",
+	"navy",
+	"olive",
+	"orange",
+	"purple",
+	"red",
+	"silver",
+	"teal",
+	"transparent",
+	"white",
+	"yellow",
+};
+
+static const char *list_style_type_kw[] = {
+	"armenian",
+	"circle",
+	"decimal",
+	"decimal-leading-zero",
+	"disc",
+	"georgian",
+	"lower-alpha",
+	"lower-greek",
+	"lower-latin",
+	"lower-roman",
+	"none",
+	"square",
+	"upper-alpha",
+	"upper-greek",
+	"upper-latin",
+	"upper-roman",
+};
+
+static const char *list_style_position_kw[] = {
+	"inside",
+	"outside",
+};
+
+static const char *font_style_kw[] = {
+	"italic",
+	"oblique",
+};
+
+static const char *font_variant_kw[] = {
+	"small-caps",
+};
+
+static const char *font_weight_kw[] = {
+	"bold",
+	"bolder",
+	"lighter",
+};
+
+static const char *font_size_kw[] = {
+	"large",
+	"larger",
+	"medium",
+	"small",
+	"smaller",
+	"x-large",
+	"x-small",
+	"xx-large",
+	"xx-small",
+};
+
+/* Properties to ignore when scanning through font-family. We set font-family
+ * to the full font shorthand value list because Adobe generates DS strings
+ * where the font-family comes before the font-size (and not at the end as it's
+ * supposed to). This lets us scan the font shorthand list without trying to
+ * look up fonts named "bold", etc.
+ */
+static const char *font_family_ignore[] = {
+	",",
+	"/",
+	"bold",
+	"bolder",
+	"italic",
+	"large",
+	"larger",
+	"lighter",
+	"medium",
+	"oblique",
+	"small",
+	"small-caps",
+	"smaller",
+	"x-large",
+	"x-small",
+	"xx-large",
+	"xx-small",
+};
+
+static int
+keyword_in_list(const char *name, const char **list, int n)
+{
+	int l = 0;
+	int r = n - 1;
+	while (l <= r)
+	{
+		int m = (l + r) >> 1;
+		int c = strcmp(name, list[m]);
+		if (c < 0)
+			r = m - 1;
+		else if (c > 0)
+			l = m + 1;
+		else
+			return 1;
+	}
+	return 0;
+}
+
+static int
+is_bold_from_font_weight(const char *weight)
+{
+	return !strcmp(weight, "bold") || !strcmp(weight, "bolder") || atoi(weight) > 400;
+}
+
+static int
+is_italic_from_font_style(const char *style)
+{
+	return !strcmp(style, "italic") || !strcmp(style, "oblique");
+}
+
+/*
+ * Compute specificity
+ */
+
+static int
+count_condition_ids(fz_css_condition *cond)
+{
+	int n = 0;
+	while (cond)
+	{
+		if (cond->type == '#')
+			n ++;
+		cond = cond->next;
+	}
+	return n;
+}
+
+static int
+count_selector_ids(fz_css_selector *sel)
+{
+	int n = count_condition_ids(sel->cond);
+	if (sel->left && sel->right)
+	{
+		n += count_selector_ids(sel->left);
+		n += count_selector_ids(sel->right);
+	}
+	return n;
+}
+
+static int
+count_condition_atts(fz_css_condition *cond)
+{
+	int n = 0;
+	while (cond)
+	{
+		if (cond->type != '#' && cond->type != ':')
+			n ++;
+		cond = cond->next;
+	}
+	return n;
+}
+
+static int
+count_selector_atts(fz_css_selector *sel)
+{
+	int n = count_condition_atts(sel->cond);
+	if (sel->left && sel->right)
+	{
+		n += count_selector_atts(sel->left);
+		n += count_selector_atts(sel->right);
+	}
+	return n;
+}
+
+static int
+count_condition_names(fz_css_condition *cond)
+{
+	int n = 0;
+	while (cond)
+	{
+		if (cond->type == ':')
+			n ++;
+		cond = cond->next;
+	}
+	return n;
+}
+
+static int
+count_selector_names(fz_css_selector *sel)
+{
+	int n = count_condition_names(sel->cond);
+	if (sel->left && sel->right)
+	{
+		n += count_selector_names(sel->left);
+		n += count_selector_names(sel->right);
+	}
+	else if (sel->name)
+	{
+		n ++;
+	}
+	return n;
+}
+
+#define INLINE_SPECIFICITY 10000
+
+static int
+selector_specificity(fz_css_selector *sel, int important)
+{
+	int b = count_selector_ids(sel);
+	int c = count_selector_atts(sel);
+	int d = count_selector_names(sel);
+	return important * 1000 + b * 100 + c * 10 + d;
+}
+
+/*
+ * Selector matching
+ */
+
+static int
+match_att_exists_condition(fz_xml *node, const char *key)
+{
+	const char *s = fz_xml_att(node, key);
+	return s != NULL;
+}
+
+static int
+match_att_is_condition(fz_xml *node, const char *key, const char *val)
+{
+	const char *att = fz_xml_att(node, key);
+	return att && !strcmp(val, att);
+}
+
+static int
+match_att_has_condition(fz_xml *node, const char *att, const char *needle)
+{
+	const char *haystack = fz_xml_att(node, att);
+	const char *ss;
+	size_t n;
+	if (haystack) {
+		ss = strstr(haystack, needle);
+		if (ss)
+		{
+			n = strlen(needle);
+
+			/* Look for exact matches or matching words. */
+			if ((ss[n] == ' ' || ss[n] == 0) && (ss == haystack || ss[-1] == ' '))
+				return 1;
+		}
+	}
+	return 0;
+}
+
+static int
+match_condition(fz_css_condition *cond, fz_xml *node)
+{
+	if (!cond)
+		return 1;
+
+	switch (cond->type) {
+	default: return 0;
+	case ':': return 0; /* don't support pseudo-classes */
+	case '#': if (!match_att_is_condition(node, "id", cond->val)) return 0; break;
+	case '.': if (!match_att_has_condition(node, "class", cond->val)) return 0; break;
+	case '[': if (!match_att_exists_condition(node, cond->key)) return 0; break;
+	case '=': if (!match_att_is_condition(node, cond->key, cond->val)) return 0; break;
+	case '~': if (!match_att_has_condition(node, cond->key, cond->val)) return 0; break;
+	case '|': if (!match_att_is_condition(node, cond->key, cond->val)) return 0; break;
+	}
+
+	return match_condition(cond->next, node);
+}
+
+static int
+match_selector(fz_css_selector *sel, fz_xml *node)
+{
+	if (!node)
+		return 0;
+
+	if (sel->combine)
+	{
+		/* descendant */
+		if (sel->combine == ' ')
+		{
+			fz_xml *parent = fz_xml_up(node);
+			if (!parent || !match_selector(sel->right, node))
+				return 0;
+
+			while (parent)
+			{
+				if (match_selector(sel->left, parent))
+					return 1;
+				parent = fz_xml_up(parent);
+			}
+			return 0;
+		}
+
+		/* child */
+		if (sel->combine == '>')
+		{
+			fz_xml *parent = fz_xml_up(node);
+			if (!parent)
+				return 0;
+			if (!match_selector(sel->left, parent))
+				return 0;
+			if (!match_selector(sel->right, node))
+				return 0;
+		}
+
+		/* adjacent */
+		if (sel->combine == '+')
+		{
+			fz_xml *prev = fz_xml_prev(node);
+			while (prev && !fz_xml_tag(prev))
+				prev = fz_xml_prev(prev);
+			if (!prev)
+				return 0;
+			if (!fz_xml_tag(prev))
+				return 0;
+			if (!match_selector(sel->left, prev))
+				return 0;
+			if (!match_selector(sel->right, node))
+				return 0;
+		}
+	}
+
+	if (sel->name)
+	{
+		if (!fz_xml_is_tag(node, sel->name))
+			return 0;
+	}
+
+	if (sel->cond)
+	{
+		if (!match_condition(sel->cond, node))
+			return 0;
+	}
+
+	return 1;
+}
+
+/*
+ * Annotating nodes with properties and expanding shorthand forms.
+ */
+
+static int
+count_values(fz_css_value *value)
+{
+	int n = 0;
+	while (value)
+	{
+		n++;
+		value = value->next;
+	}
+	return n;
+}
+
+static void add_property(fz_css_match *match, int name, fz_css_value *value, int spec);
+
+static void
+add_shorthand_trbl(fz_css_match *match, fz_css_value *value, int spec,
+	int name_t, int name_r, int name_b, int name_l)
+{
+	int n = count_values(value);
+
+	if (n == 1)
+	{
+		add_property(match, name_t, value, spec);
+		add_property(match, name_r, value, spec);
+		add_property(match, name_b, value, spec);
+		add_property(match, name_l, value, spec);
+	}
+
+	if (n == 2)
+	{
+		fz_css_value *a = value;
+		fz_css_value *b = value->next;
+
+		add_property(match, name_t, a, spec);
+		add_property(match, name_r, b, spec);
+		add_property(match, name_b, a, spec);
+		add_property(match, name_l, b, spec);
+	}
+
+	if (n == 3)
+	{
+		fz_css_value *a = value;
+		fz_css_value *b = value->next;
+		fz_css_value *c = value->next->next;
+
+		add_property(match, name_t, a, spec);
+		add_property(match, name_r, b, spec);
+		add_property(match, name_b, c, spec);
+		add_property(match, name_l, b, spec);
+	}
+
+	if (n == 4)
+	{
+		fz_css_value *a = value;
+		fz_css_value *b = value->next;
+		fz_css_value *c = value->next->next;
+		fz_css_value *d = value->next->next->next;
+
+		add_property(match, name_t, a, spec);
+		add_property(match, name_r, b, spec);
+		add_property(match, name_b, c, spec);
+		add_property(match, name_l, d, spec);
+	}
+}
+
+static void
+add_shorthand_margin(fz_css_match *match, fz_css_value *value, int spec)
+{
+	add_shorthand_trbl(match, value, spec,
+		PRO_MARGIN_TOP,
+		PRO_MARGIN_RIGHT,
+		PRO_MARGIN_BOTTOM,
+		PRO_MARGIN_LEFT);
+}
+
+static void
+add_shorthand_padding(fz_css_match *match, fz_css_value *value, int spec)
+{
+	add_shorthand_trbl(match, value, spec,
+		PRO_PADDING_TOP,
+		PRO_PADDING_RIGHT,
+		PRO_PADDING_BOTTOM,
+		PRO_PADDING_LEFT);
+}
+
+static void
+add_shorthand_border_width(fz_css_match *match, fz_css_value *value, int spec)
+{
+	add_shorthand_trbl(match, value, spec,
+		PRO_BORDER_TOP_WIDTH,
+		PRO_BORDER_RIGHT_WIDTH,
+		PRO_BORDER_BOTTOM_WIDTH,
+		PRO_BORDER_LEFT_WIDTH);
+}
+
+static void
+add_shorthand_border_color(fz_css_match *match, fz_css_value *value, int spec)
+{
+	add_shorthand_trbl(match, value, spec,
+		PRO_BORDER_TOP_COLOR,
+		PRO_BORDER_RIGHT_COLOR,
+		PRO_BORDER_BOTTOM_COLOR,
+		PRO_BORDER_LEFT_COLOR);
+}
+
+static void
+add_shorthand_border_style(fz_css_match *match, fz_css_value *value, int spec)
+{
+	add_shorthand_trbl(match, value, spec,
+		PRO_BORDER_TOP_STYLE,
+		PRO_BORDER_RIGHT_STYLE,
+		PRO_BORDER_BOTTOM_STYLE,
+		PRO_BORDER_LEFT_STYLE);
+}
+
+static void
+add_shorthand_border(fz_css_match *match, fz_css_value *value, int spec, int T, int R, int B, int L)
+{
+	while (value)
+	{
+		if (value->type == CSS_HASH)
+		{
+			if (T) add_property(match, PRO_BORDER_TOP_COLOR, value, spec);
+			if (R) add_property(match, PRO_BORDER_RIGHT_COLOR, value, spec);
+			if (B) add_property(match, PRO_BORDER_BOTTOM_COLOR, value, spec);
+			if (L) add_property(match, PRO_BORDER_LEFT_COLOR, value, spec);
+		}
+		else if (value->type == CSS_KEYWORD)
+		{
+			if (keyword_in_list(value->data, border_width_kw, nelem(border_width_kw)))
+			{
+				if (T) add_property(match, PRO_BORDER_TOP_WIDTH, value, spec);
+				if (R) add_property(match, PRO_BORDER_RIGHT_WIDTH, value, spec);
+				if (B) add_property(match, PRO_BORDER_BOTTOM_WIDTH, value, spec);
+				if (L) add_property(match, PRO_BORDER_LEFT_WIDTH, value, spec);
+			}
+			else if (keyword_in_list(value->data, border_style_kw, nelem(border_style_kw)))
+			{
+				if (T) add_property(match, PRO_BORDER_TOP_STYLE, value, spec);
+				if (R) add_property(match, PRO_BORDER_RIGHT_STYLE, value, spec);
+				if (B) add_property(match, PRO_BORDER_BOTTOM_STYLE, value, spec);
+				if (L) add_property(match, PRO_BORDER_LEFT_STYLE, value, spec);
+			}
+			else if (keyword_in_list(value->data, color_kw, nelem(color_kw)))
+			{
+				if (T) add_property(match, PRO_BORDER_TOP_COLOR, value, spec);
+				if (R) add_property(match, PRO_BORDER_RIGHT_COLOR, value, spec);
+				if (B) add_property(match, PRO_BORDER_BOTTOM_COLOR, value, spec);
+				if (L) add_property(match, PRO_BORDER_LEFT_COLOR, value, spec);
+			}
+		}
+		else
+		{
+			if (T) add_property(match, PRO_BORDER_TOP_WIDTH, value, spec);
+			if (R) add_property(match, PRO_BORDER_RIGHT_WIDTH, value, spec);
+			if (B) add_property(match, PRO_BORDER_BOTTOM_WIDTH, value, spec);
+			if (L) add_property(match, PRO_BORDER_LEFT_WIDTH, value, spec);
+		}
+		value = value->next;
+	}
+}
+
+static void
+add_shorthand_list_style(fz_css_match *match, fz_css_value *value, int spec)
+{
+	while (value)
+	{
+		if (value->type == CSS_KEYWORD)
+		{
+			if (keyword_in_list(value->data, list_style_type_kw, nelem(list_style_type_kw)))
+			{
+				add_property(match, PRO_LIST_STYLE_TYPE, value, spec);
+			}
+			else if (keyword_in_list(value->data, list_style_position_kw, nelem(list_style_position_kw)))
+			{
+				add_property(match, PRO_LIST_STYLE_POSITION, value, spec);
+			}
+		}
+		value = value->next;
+	}
+}
+
+static fz_css_value static_value_normal = { CSS_KEYWORD, "normal", NULL, NULL };
+
+static fz_css_value *
+add_shorthand_font_size(fz_css_match *match, fz_css_value *value, int spec)
+{
+	/* font-size */
+	add_property(match, PRO_FONT_SIZE, value, spec);
+
+	/* / line-height */
+	if (value->next && value->next->next && !strcmp(value->next->data, "/"))
+	{
+		value = value->next->next;
+		add_property(match, PRO_LINE_HEIGHT, value, spec);
+	}
+
+	return value;
+}
+
+static void
+add_shorthand_font(fz_css_match *match, fz_css_value *value, int spec)
+{
+	fz_css_value *font_style = NULL;
+	fz_css_value *font_variant = NULL;
+	fz_css_value *font_weight = NULL;
+
+	/* add the start as font-family for most robust scanning of matching font names */
+	add_property(match, PRO_FONT_FAMILY, value, spec);
+
+	/* then look for known style/variant/weight keywords and font-size/line-height */
+	for (; value; value = value->next)
+	{
+		/* style/variant/weight/size */
+		if (value->type == CSS_KEYWORD)
+		{
+			if (keyword_in_list(value->data, font_style_kw, nelem(font_style_kw)))
+				font_style = value;
+			else if (keyword_in_list(value->data, font_variant_kw, nelem(font_variant_kw)))
+				font_variant = value;
+			else if (keyword_in_list(value->data, font_weight_kw, nelem(font_weight_kw)))
+				font_weight = value;
+			else if (keyword_in_list(value->data, font_size_kw, nelem(font_size_kw)))
+				value = add_shorthand_font_size(match, value, spec);
+		}
+		else if (value->type == CSS_NUMBER)
+			font_weight = value;
+		else if (value->type == CSS_LENGTH || value->type == CSS_PERCENT)
+			value = add_shorthand_font_size(match, value, spec);
+	}
+
+	/* set all properties to their initial values if not specified! */
+	if (font_style)
+		add_property(match, PRO_FONT_STYLE, font_style, spec);
+	else
+		add_property(match, PRO_FONT_STYLE, &static_value_normal, spec);
+
+	if (font_variant)
+		add_property(match, PRO_FONT_VARIANT, font_variant, spec);
+	else
+		add_property(match, PRO_FONT_VARIANT, &static_value_normal, spec);
+
+	if (font_weight)
+		add_property(match, PRO_FONT_WEIGHT, font_weight, spec);
+	else
+		add_property(match, PRO_FONT_WEIGHT, &static_value_normal, spec);
+}
+
+static void
+add_property(fz_css_match *match, int name, fz_css_value *value, int spec)
+{
+	/* shorthand expansions: */
+	switch (name)
+	{
+	case PRO_MARGIN:
+		add_shorthand_margin(match, value, spec);
+		return;
+	case PRO_PADDING:
+		add_shorthand_padding(match, value, spec);
+		return;
+	case PRO_BORDER_WIDTH:
+		add_shorthand_border_width(match, value, spec);
+		return;
+	case PRO_BORDER_COLOR:
+		add_shorthand_border_color(match, value, spec);
+		return;
+	case PRO_BORDER_STYLE:
+		add_shorthand_border_style(match, value, spec);
+		return;
+	case PRO_BORDER:
+		add_shorthand_border(match, value, spec, 1, 1, 1, 1);
+		return;
+	case PRO_BORDER_TOP:
+		add_shorthand_border(match, value, spec, 1, 0, 0, 0);
+		return;
+	case PRO_BORDER_RIGHT:
+		add_shorthand_border(match, value, spec, 0, 1, 0, 0);
+		return;
+	case PRO_BORDER_BOTTOM:
+		add_shorthand_border(match, value, spec, 0, 0, 1, 0);
+		return;
+	case PRO_BORDER_LEFT:
+		add_shorthand_border(match, value, spec, 0, 0, 0, 1);
+		return;
+	case PRO_LIST_STYLE:
+		add_shorthand_list_style(match, value, spec);
+		return;
+	case PRO_FONT:
+		add_shorthand_font(match, value, spec);
+		return;
+	/* TODO: background */
+	}
+
+	if (name < NUM_PROPERTIES && match->spec[name] <= spec)
+	{
+		match->value[name] = value;
+		match->spec[name] = spec;
+	}
+}
+
+void
+fz_match_css(fz_context *ctx, fz_css_match *match, fz_css_match *up, fz_css *css, fz_xml *node)
+{
+	fz_css_rule *rule;
+	fz_css_selector *sel;
+	fz_css_property *prop;
+	const char *s;
+	int i;
+
+	match->up = up;
+	for (i = 0; i < NUM_PROPERTIES; ++i)
+	{
+		match->spec[i] = -1;
+		match->value[i] = NULL;
+	}
+
+	for (rule = css->rule; rule; rule = rule->next)
+	{
+		sel = rule->selector;
+		while (sel)
+		{
+			if (match_selector(sel, node))
+			{
+				for (prop = rule->declaration; prop; prop = prop->next)
+					add_property(match, prop->name, prop->value, selector_specificity(sel, prop->important));
+				break;
+			}
+			sel = sel->next;
+		}
+	}
+
+	if (fz_use_document_css(ctx))
+	{
+		s = fz_xml_att(node, "style");
+		if (s)
+		{
+			fz_try(ctx)
+			{
+				prop = fz_parse_css_properties(ctx, css->pool, s);
+				while (prop)
+				{
+					add_property(match, prop->name, prop->value, INLINE_SPECIFICITY);
+					prop = prop->next;
+				}
+				/* We can "leak" the property here, since it is freed along with the pool allocator. */
+			}
+			fz_catch(ctx)
+			{
+				fz_rethrow_if(ctx, FZ_ERROR_SYSTEM);
+				fz_report_error(ctx);
+				fz_warn(ctx, "ignoring style attribute");
+			}
+		}
+	}
+}
+
+void
+fz_match_css_at_page(fz_context *ctx, fz_css_match *match, fz_css *css)
+{
+	fz_css_rule *rule;
+	fz_css_selector *sel;
+	fz_css_property *prop;
+	int i;
+
+	match->up = NULL;
+	for (i = 0; i < NUM_PROPERTIES; ++i)
+	{
+		match->spec[i] = -1;
+		match->value[i] = NULL;
+	}
+
+	for (rule = css->rule; rule; rule = rule->next)
+	{
+		sel = rule->selector;
+		while (sel)
+		{
+			if (sel->name && !strcmp(sel->name, "@page"))
+			{
+				for (prop = rule->declaration; prop; prop = prop->next)
+					add_property(match, prop->name, prop->value, selector_specificity(sel, prop->important));
+				break;
+			}
+			sel = sel->next;
+		}
+	}
+}
+
+void
+fz_add_css_font_face(fz_context *ctx, fz_html_font_set *set, fz_archive *zip, const char *base_uri, fz_css_property *declaration)
+{
+	fz_html_font_face *custom;
+	fz_css_property *prop;
+	fz_font *font = NULL;
+	fz_buffer *buf = NULL;
+	fz_stream *stm = NULL;
+	int is_bold, is_italic, is_small_caps;
+	char path[2048];
+
+	const char *family = "serif";
+	const char *weight = "normal";
+	const char *style = "normal";
+	const char *variant = "normal";
+	const char *src = NULL;
+
+	for (prop = declaration; prop; prop = prop->next)
+	{
+		if (prop->name == PRO_FONT_FAMILY) family = prop->value->data;
+		if (prop->name == PRO_FONT_WEIGHT) weight = prop->value->data;
+		if (prop->name == PRO_FONT_STYLE) style = prop->value->data;
+		if (prop->name == PRO_FONT_VARIANT) variant = prop->value->data;
+		if (prop->name == PRO_SRC) src = prop->value->data;
+	}
+
+	if (!src)
+		return;
+
+	is_bold = is_bold_from_font_weight(weight);
+	is_italic = is_italic_from_font_style(style);
+	is_small_caps = !strcmp(variant, "small-caps");
+
+	fz_strlcpy(path, base_uri, sizeof path);
+	fz_strlcat(path, "/", sizeof path);
+	fz_strlcat(path, src, sizeof path);
+	fz_urldecode(path);
+
+	for (custom = set->custom; custom; custom = custom->next)
+		if (!strcmp(custom->src, path) && !strcmp(custom->family, family) &&
+				custom->is_bold == is_bold &&
+				custom->is_italic == is_italic &&
+				custom->is_small_caps == is_small_caps)
+			return; /* already loaded */
+
+	fz_var(buf);
+	fz_var(font);
+	fz_var(stm);
+
+	fz_try(ctx)
+	{
+		if (fz_has_archive_entry(ctx, zip, path))
+			buf = fz_read_archive_entry(ctx, zip, path);
+		else
+		{
+			stm = fz_try_open_file(ctx, src);
+			if (stm == NULL)
+				fz_throw(ctx, FZ_ERROR_FORMAT, "cannot locate font '%s' specified by css", src);
+			buf = fz_read_all(ctx, stm, 0);
+		}
+		font = fz_new_font_from_buffer(ctx, NULL, buf, 0, 0);
+		fz_add_html_font_face(ctx, set, family, is_bold, is_italic, is_small_caps, path, font);
+	}
+	fz_always(ctx)
+	{
+		fz_drop_buffer(ctx, buf);
+		fz_drop_stream(ctx, stm);
+		fz_drop_font(ctx, font);
+	}
+	fz_catch(ctx)
+	{
+		fz_rethrow_if(ctx, FZ_ERROR_TRYLATER);
+		fz_rethrow_if(ctx, FZ_ERROR_SYSTEM);
+		fz_report_error(ctx);
+		fz_warn(ctx, "cannot load font-face: %s", src);
+	}
+}
+
+void
+fz_add_css_font_faces(fz_context *ctx, fz_html_font_set *set, fz_archive *zip, const char *base_uri, fz_css *css)
+{
+	fz_css_rule *rule;
+	fz_css_selector *sel;
+
+	for (rule = css->rule; rule; rule = rule->next)
+	{
+		if (!rule->loaded)
+		{
+			rule->loaded = 1;
+			sel = rule->selector;
+			while (sel)
+			{
+				if (sel->name && !strcmp(sel->name, "@font-face"))
+				{
+					fz_add_css_font_face(ctx, set, zip, base_uri, rule->declaration);
+					break;
+				}
+				sel = sel->next;
+			}
+		}
+	}
+}
+
+static int
+is_inheritable_property(int name)
+{
+	return
+		name == PRO_COLOR ||
+		name == PRO_DIRECTION ||
+		name == PRO_FONT_FAMILY ||
+		name == PRO_FONT_STYLE ||
+		name == PRO_FONT_VARIANT ||
+		name == PRO_FONT_WEIGHT ||
+		name == PRO_LEADING ||
+		name == PRO_LETTER_SPACING ||
+		name == PRO_LINE_HEIGHT ||
+		name == PRO_LIST_STYLE_IMAGE ||
+		name == PRO_LIST_STYLE_POSITION ||
+		name == PRO_LIST_STYLE_TYPE ||
+		name == PRO_ORPHANS ||
+		name == PRO_OVERFLOW_WRAP ||
+		name == PRO_QUOTES ||
+		name == PRO_TEXT_ALIGN ||
+		name == PRO_TEXT_INDENT ||
+		name == PRO_TEXT_TRANSFORM ||
+		name == PRO_VISIBILITY ||
+		name == PRO_WHITE_SPACE ||
+		name == PRO_WIDOWS ||
+		name == PRO_WORD_SPACING ||
+		// Strictly speaking, text-decoration is not an inherited property,
+		// but since when drawing an underlined element, all children are also underlined,
+		// we may as well make it inherited.
+		name == PRO_TEXT_DECORATION;
+}
+
+static fz_css_value *
+value_from_inheritable_property(fz_css_match *match, int name)
+{
+	fz_css_value *value = match->value[name];
+	if (match->up)
+	{
+		if (value && !strcmp(value->data, "inherit"))
+			return value_from_inheritable_property(match->up, name);
+		if (!value)
+			return value_from_inheritable_property(match->up, name);
+	}
+	return value;
+}
+
+static fz_css_value *
+value_from_property(fz_css_match *match, int name)
+{
+	fz_css_value *value = match->value[name];
+	if (match->up)
+	{
+		if (value && !strcmp(value->data, "inherit"))
+			if (name != PRO_FONT_SIZE) /* never inherit 'font-size' textually */
+				return value_from_property(match->up, name);
+		if (!value && is_inheritable_property(name))
+			return value_from_inheritable_property(match->up, name);
+	}
+	return value;
+}
+
+static const char *
+string_from_property(fz_css_match *match, int name, const char *initial)
+{
+	fz_css_value *value = value_from_property(match, name);
+	if (!value)
+		return initial;
+	return value->data;
+}
+
+static fz_css_number
+make_number(float v, int u)
+{
+	fz_css_number n;
+	n.value = v;
+	n.unit = u;
+	return n;
+}
+
+static fz_css_number
+make_undefined_number(void)
+{
+	fz_css_number n;
+	n.value = 0;
+	n.unit = N_UNDEFINED;
+	return n;
+}
+
+/* Fast but inaccurate strtof. */
+static float
+fz_css_strtof(char *s, char **endptr)
+{
+	float sign = 1;
+	float v = 0;
+	float n = 0;
+	float d = 1;
+
+	if (*s == '-')
+	{
+		sign = -1;
+		++s;
+	}
+
+	while (*s >= '0' && *s <= '9')
+	{
+		v = v * 10 + (*s - '0');
+		++s;
+	}
+
+	if (*s == '.')
+	{
+		++s;
+		while (*s >= '0' && *s <= '9')
+		{
+			n = n * 10 + (*s - '0');
+			d = d * 10;
+			++s;
+		}
+		v += n / d;
+	}
+
+	if (endptr)
+		*endptr = s;
+
+	return sign * v;
+}
+
+static fz_css_number
+number_from_value(fz_css_value *value, float initial, int initial_unit)
+{
+	char *p;
+
+	if (!value)
+		return make_number(initial, initial_unit);
+
+	if (value->type == CSS_PERCENT)
+		return make_number(fz_css_strtof(value->data, NULL), N_PERCENT);
+
+	if (value->type == CSS_NUMBER)
+		return make_number(fz_css_strtof(value->data, NULL), N_NUMBER);
+
+	if (value->type == CSS_LENGTH)
+	{
+		float x = fz_css_strtof(value->data, &p);
+
+		if (p[0] == 'e' && p[1] == 'm' && p[2] == 0)
+			return make_number(x, N_SCALE);
+		if (p[0] == 'e' && p[1] == 'x' && p[2] == 0)
+			return make_number(x / 2, N_SCALE);
+
+		if (p[0] == 'i' && p[1] == 'n' && p[2] == 0)
+			return make_number(x * 72, N_LENGTH);
+		if (p[0] == 'c' && p[1] == 'm' && p[2] == 0)
+			return make_number(x * 7200 / 254, N_LENGTH);
+		if (p[0] == 'm' && p[1] == 'm' && p[2] == 0)
+			return make_number(x * 720 / 254, N_LENGTH);
+		if (p[0] == 'p' && p[1] == 'c' && p[2] == 0)
+			return make_number(x * 12, N_LENGTH);
+
+		if (p[0] == 'p' && p[1] == 't' && p[2] == 0)
+			return make_number(x, N_LENGTH);
+		if (p[0] == 'p' && p[1] == 'x' && p[2] == 0)
+			return make_number(x, N_LENGTH);
+
+		/* FIXME: 'rem' should be 'em' of root element. This is a bad approximation. */
+		if (p[0] == 'r' && p[1] == 'e' && p[2] == 'm' && p[3] == 0)
+			return make_number(x * 16, N_LENGTH);
+
+		/* FIXME: 'ch' should be width of '0' character. This is an approximation. */
+		if (p[0] == 'c' && p[1] == 'h' && p[2] == 0)
+			return make_number(x / 2, N_LENGTH);
+
+		return make_number(x, N_LENGTH);
+	}
+
+	if (value->type == CSS_KEYWORD)
+	{
+		if (!strcmp(value->data, "auto"))
+			return make_number(0, N_AUTO);
+	}
+
+	return make_number(initial, initial_unit);
+}
+
+static fz_css_number
+number_from_property(fz_css_match *match, int property, float initial, int initial_unit)
+{
+	return number_from_value(value_from_property(match, property), initial, initial_unit);
+}
+
+static fz_css_number
+border_width_from_property(fz_css_match *match, int property)
+{
+	fz_css_value *value = value_from_property(match, property);
+	if (value)
+	{
+		if (!strcmp(value->data, "thin"))
+			return make_number(1, N_LENGTH);
+		if (!strcmp(value->data, "medium"))
+			return make_number(2, N_LENGTH);
+		if (!strcmp(value->data, "thick"))
+			return make_number(4, N_LENGTH);
+		return number_from_value(value, 0, N_LENGTH);
+	}
+	return make_number(2, N_LENGTH); /* initial: 'medium' */
+}
+
+static int
+border_style_from_property(fz_css_match *match, int property)
+{
+	fz_css_value *value = value_from_property(match, property);
+	if (value)
+	{
+		if (!strcmp(value->data, "none")) return BS_NONE;
+		else if (!strcmp(value->data, "hidden")) return BS_NONE;
+		else if (!strcmp(value->data, "solid")) return BS_SOLID;
+	}
+	return BS_NONE;
+}
+
+int fz_css_number_defined(fz_css_number number)
+{
+	return number.unit != N_UNDEFINED;
+}
+
+float
+fz_from_css_number(fz_css_number number, float em, float percent_value, float auto_value)
+{
+	switch (number.unit) {
+	default:
+	case N_NUMBER: return number.value;
+	case N_LENGTH: return number.value;
+	case N_SCALE: return number.value * em;
+	case N_PERCENT: return number.value * 0.01f * percent_value;
+	case N_AUTO: return auto_value;
+	}
+}
+
+float
+fz_from_css_number_scale(fz_css_number number, float scale)
+{
+	switch (number.unit) {
+	default:
+	case N_NUMBER: return number.value * scale;
+	case N_LENGTH: return number.value;
+	case N_SCALE: return number.value * scale;
+	case N_PERCENT: return number.value * 0.01f * scale;
+	case N_AUTO: return scale;
+	}
+}
+
+static fz_css_color
+make_color(int r, int g, int b, int a)
+{
+	fz_css_color c;
+	c.r = r < 0 ? 0 : r > 255 ? 255 : r;
+	c.g = g < 0 ? 0 : g > 255 ? 255 : g;
+	c.b = b < 0 ? 0 : b > 255 ? 255 : b;
+	c.a = a < 0 ? 0 : a > 255 ? 255 : a;
+	return c;
+}
+
+static int tohex(int c)
+{
+	if (c - '0' < 10)
+		return c - '0';
+	return (c | 32) - 'a' + 10;
+}
+
+static fz_css_color
+color_from_value(fz_css_value *value, fz_css_color initial)
+{
+	if (!value)
+		return initial;
+
+	if (value->type == CSS_HASH)
+	{
+		int r, g, b, a;
+		size_t n;
+hex_color:
+		n = strlen(value->data);
+		if (n == 3)
+		{
+			r = tohex(value->data[0]) * 16 + tohex(value->data[0]);
+			g = tohex(value->data[1]) * 16 + tohex(value->data[1]);
+			b = tohex(value->data[2]) * 16 + tohex(value->data[2]);
+			a = 255;
+		}
+		else if (n == 4)
+		{
+			r = tohex(value->data[0]) * 16 + tohex(value->data[0]);
+			g = tohex(value->data[1]) * 16 + tohex(value->data[1]);
+			b = tohex(value->data[2]) * 16 + tohex(value->data[2]);
+			a = tohex(value->data[3]) * 16 + tohex(value->data[3]);
+		}
+		else if (n == 6)
+		{
+			r = tohex(value->data[0]) * 16 + tohex(value->data[1]);
+			g = tohex(value->data[2]) * 16 + tohex(value->data[3]);
+			b = tohex(value->data[4]) * 16 + tohex(value->data[5]);
+			a = 255;
+		}
+		else if (n == 8)
+		{
+			r = tohex(value->data[0]) * 16 + tohex(value->data[1]);
+			g = tohex(value->data[2]) * 16 + tohex(value->data[3]);
+			b = tohex(value->data[4]) * 16 + tohex(value->data[5]);
+			a = tohex(value->data[6]) * 16 + tohex(value->data[7]);
+		}
+		else
+		{
+			r = g = b = 0;
+			a = 255;
+		}
+		return make_color(r, g, b, a);
+	}
+
+	if (value->type == '(' && !strcmp(value->data, "rgb"))
+	{
+		fz_css_value *vr, *vg, *vb;
+		int r, g, b;
+		vr = value->args;
+		vg = vr && vr->next ? vr->next->next : NULL; /* skip the ',' nodes */
+		vb = vg && vg->next ? vg->next->next : NULL; /* skip the ',' nodes */
+		r = fz_from_css_number(number_from_value(vr, 0, N_NUMBER), 255, 255, 0);
+		g = fz_from_css_number(number_from_value(vg, 0, N_NUMBER), 255, 255, 0);
+		b = fz_from_css_number(number_from_value(vb, 0, N_NUMBER), 255, 255, 0);
+		return make_color(r, g, b, 255);
+	}
+
+	if (value->type == '(' && !strcmp(value->data, "rgba"))
+	{
+		fz_css_value *vr, *vg, *vb, *va;
+		int r, g, b, a;
+		vr = value->args;
+		vg = vr && vr->next ? vr->next->next : NULL; /* skip the ',' nodes */
+		vb = vg && vg->next ? vg->next->next : NULL; /* skip the ',' nodes */
+		va = vb && vb->next ? vb->next->next : NULL; /* skip the ',' nodes */
+		r = fz_from_css_number(number_from_value(vr, 0, N_NUMBER), 255, 255, 0);
+		g = fz_from_css_number(number_from_value(vg, 0, N_NUMBER), 255, 255, 0);
+		b = fz_from_css_number(number_from_value(vb, 0, N_NUMBER), 255, 255, 0);
+		a = fz_from_css_number(number_from_value(va, 0, N_NUMBER), 255, 255, 255);
+		return make_color(r, g, b, a);
+	}
+
+	if (value->type == CSS_KEYWORD)
+	{
+		if (!strcmp(value->data, "transparent"))
+			return make_color(0, 0, 0, 0);
+		if (!strcmp(value->data, "maroon"))
+			return make_color(0x80, 0x00, 0x00, 255);
+		if (!strcmp(value->data, "red"))
+			return make_color(0xFF, 0x00, 0x00, 255);
+		if (!strcmp(value->data, "orange"))
+			return make_color(0xFF, 0xA5, 0x00, 255);
+		if (!strcmp(value->data, "yellow"))
+			return make_color(0xFF, 0xFF, 0x00, 255);
+		if (!strcmp(value->data, "olive"))
+			return make_color(0x80, 0x80, 0x00, 255);
+		if (!strcmp(value->data, "purple"))
+			return make_color(0x80, 0x00, 0x80, 255);
+		if (!strcmp(value->data, "fuchsia"))
+			return make_color(0xFF, 0x00, 0xFF, 255);
+		if (!strcmp(value->data, "white"))
+			return make_color(0xFF, 0xFF, 0xFF, 255);
+		if (!strcmp(value->data, "lime"))
+			return make_color(0x00, 0xFF, 0x00, 255);
+		if (!strcmp(value->data, "green"))
+			return make_color(0x00, 0x80, 0x00, 255);
+		if (!strcmp(value->data, "navy"))
+			return make_color(0x00, 0x00, 0x80, 255);
+		if (!strcmp(value->data, "blue"))
+			return make_color(0x00, 0x00, 0xFF, 255);
+		if (!strcmp(value->data, "aqua"))
+			return make_color(0x00, 0xFF, 0xFF, 255);
+		if (!strcmp(value->data, "teal"))
+			return make_color(0x00, 0x80, 0x80, 255);
+		if (!strcmp(value->data, "black"))
+			return make_color(0x00, 0x00, 0x00, 255);
+		if (!strcmp(value->data, "silver"))
+			return make_color(0xC0, 0xC0, 0xC0, 255);
+		if (!strcmp(value->data, "gray"))
+			return make_color(0x80, 0x80, 0x80, 255);
+		goto hex_color; /* last ditch attempt: maybe it's a #XXXXXX color without the # */
+	}
+	return initial;
+}
+
+static fz_css_color
+color_from_property(fz_css_match *match, int property, fz_css_color initial)
+{
+	return color_from_value(value_from_property(match, property), initial);
+}
+
+static fz_css_color
+color_from_properties(fz_css_match *match, int property, int property2, fz_css_color initial)
+{
+	fz_css_value *value = value_from_property(match, property);
+
+	if (value == NULL)
+		value = value_from_property(match, property2);
+
+	return color_from_value(value, initial);
+}
+
+int
+fz_get_css_match_display(fz_css_match *match)
+{
+	fz_css_value *value = value_from_property(match, PRO_DISPLAY);
+	if (value)
+	{
+		if (!strcmp(value->data, "none"))
+			return DIS_NONE;
+		if (!strcmp(value->data, "inline"))
+			return DIS_INLINE;
+		if (!strcmp(value->data, "block"))
+			return DIS_BLOCK;
+		if (!strcmp(value->data, "list-item"))
+			return DIS_LIST_ITEM;
+		if (!strcmp(value->data, "inline-block"))
+			return DIS_INLINE_BLOCK;
+		if (!strcmp(value->data, "table"))
+			return DIS_TABLE;
+		if (!strcmp(value->data, "table-row"))
+			return DIS_TABLE_ROW;
+		if (!strcmp(value->data, "table-cell"))
+			return DIS_TABLE_CELL;
+		if (!strcmp(value->data, "table-row-group"))
+			return DIS_TABLE_GROUP;
+		if (!strcmp(value->data, "table-header-group"))
+			return DIS_TABLE_GROUP;
+		if (!strcmp(value->data, "table-footer-group"))
+			return DIS_TABLE_GROUP;
+		if (!strcmp(value->data, "table-column-group"))
+			return DIS_NONE;
+		if (!strcmp(value->data, "table-column"))
+			return DIS_NONE;
+	}
+	return DIS_INLINE;
+}
+
+static int
+white_space_from_property(fz_css_match *match)
+{
+	fz_css_value *value = value_from_property(match, PRO_WHITE_SPACE);
+	if (value)
+	{
+		if (!strcmp(value->data, "normal")) return WS_NORMAL;
+		else if (!strcmp(value->data, "pre")) return WS_PRE;
+		else if (!strcmp(value->data, "nowrap")) return WS_NOWRAP;
+		else if (!strcmp(value->data, "pre-wrap")) return WS_PRE_WRAP;
+		else if (!strcmp(value->data, "pre-line")) return WS_PRE_LINE;
+	}
+	return WS_NORMAL;
+}
+
+static int
+text_decoration_from_property(fz_css_match *match)
+{
+	fz_css_value *value = value_from_property(match, PRO_TEXT_DECORATION);
+	if (value)
+	{
+		if (!strcmp(value->data, "underline")) return TD_UNDERLINE;
+		if (!strcmp(value->data, "line-through")) return TD_LINE_THROUGH;
+	}
+	return TD_NONE;
+}
+
+static int
+visibility_from_property(fz_css_match *match)
+{
+	fz_css_value *value = value_from_property(match, PRO_VISIBILITY);
+	if (value)
+	{
+		if (!strcmp(value->data, "visible")) return V_VISIBLE;
+		else if (!strcmp(value->data, "hidden")) return V_HIDDEN;
+		else if (!strcmp(value->data, "collapse")) return V_COLLAPSE;
+	}
+	return V_VISIBLE;
+}
+
+static int
+page_break_from_property(fz_css_match *match, int prop)
+{
+	fz_css_value *value = value_from_property(match, prop);
+	if (value)
+	{
+		if (!strcmp(value->data, "auto")) return PB_AUTO;
+		else if (!strcmp(value->data, "always")) return PB_ALWAYS;
+		else if (!strcmp(value->data, "avoid")) return PB_AVOID;
+		else if (!strcmp(value->data, "left")) return PB_LEFT;
+		else if (!strcmp(value->data, "right")) return PB_RIGHT;
+	}
+	return PB_AUTO;
+}
+
+void
+fz_default_css_style(fz_context *ctx, fz_css_style *style)
+{
+	memset(style, 0, sizeof *style);
+	style->visibility = V_VISIBLE;
+	style->text_align = TA_LEFT;
+	style->vertical_align = VA_BASELINE;
+	style->white_space = WS_NORMAL;
+	style->list_style_type = LST_DISC;
+	style->font_size = make_number(1, N_SCALE);
+	style->width = make_number(0, N_AUTO);
+	style->height = make_number(0, N_AUTO);
+	style->leading = make_undefined_number();
+}
+
+void
+fz_apply_css_style(fz_context *ctx, fz_html_font_set *set, fz_css_style *style, fz_css_match *match)
+{
+	fz_css_value *value;
+
+	fz_css_color black = { 0, 0, 0, 255 };
+	fz_css_color transparent = { 0, 0, 0, 0 };
+
+	fz_default_css_style(ctx, style);
+
+	style->visibility = visibility_from_property(match);
+	style->white_space = white_space_from_property(match);
+	style->text_decoration = text_decoration_from_property(match);
+	style->page_break_before = page_break_from_property(match, PRO_PAGE_BREAK_BEFORE);
+	style->page_break_after = page_break_from_property(match, PRO_PAGE_BREAK_AFTER);
+
+	value = value_from_property(match, PRO_TEXT_ALIGN);
+	if (value)
+	{
+		if (!strcmp(value->data, "left")) style->text_align = TA_LEFT;
+		else if (!strcmp(value->data, "right")) style->text_align = TA_RIGHT;
+		else if (!strcmp(value->data, "center")) style->text_align = TA_CENTER;
+		else if (!strcmp(value->data, "justify")) style->text_align = TA_JUSTIFY;
+	}
+
+	value = value_from_property(match, PRO_VERTICAL_ALIGN);
+	if (value)
+	{
+		if (!strcmp(value->data, "baseline")) style->vertical_align = VA_BASELINE;
+		else if (!strcmp(value->data, "sub")) style->vertical_align = VA_SUB;
+		else if (!strcmp(value->data, "super")) style->vertical_align = VA_SUPER;
+		else if (!strcmp(value->data, "top")) style->vertical_align = VA_TOP;
+		else if (!strcmp(value->data, "bottom")) style->vertical_align = VA_BOTTOM;
+		else if (!strcmp(value->data, "text-top")) style->vertical_align = VA_TEXT_TOP;
+		else if (!strcmp(value->data, "text-bottom")) style->vertical_align = VA_TEXT_BOTTOM;
+	}
+
+	value = value_from_property(match, PRO_FONT_SIZE);
+	if (value)
+	{
+		/* absolute-size */
+		if (!strcmp(value->data, "xx-large")) style->font_size = make_number(1.73f, N_SCALE);
+		else if (!strcmp(value->data, "x-large")) style->font_size = make_number(1.44f, N_SCALE);
+		else if (!strcmp(value->data, "large")) style->font_size = make_number(1.2f, N_SCALE);
+		else if (!strcmp(value->data, "medium")) style->font_size = make_number(1.0f, N_SCALE);
+		else if (!strcmp(value->data, "small")) style->font_size = make_number(0.83f, N_SCALE);
+		else if (!strcmp(value->data, "x-small")) style->font_size = make_number(0.69f, N_SCALE);
+		else if (!strcmp(value->data, "xx-small")) style->font_size = make_number(0.69f, N_SCALE);
+		/* relative-size */
+		else if (!strcmp(value->data, "larger")) style->font_size = make_number(1.2f, N_SCALE);
+		else if (!strcmp(value->data, "smaller")) style->font_size = make_number(1/1.2f, N_SCALE);
+		/* percentage */
+		else if (value->type == CSS_PERCENT) style->font_size = number_from_value(value, 12, N_LENGTH);
+		/* length */
+		else if (value->type == CSS_LENGTH) style->font_size = number_from_value(value, 12, N_LENGTH);
+		/* default to 1em */
+		else style->font_size = make_number(1, N_SCALE);
+	}
+	else
+	{
+		style->font_size = make_number(1, N_SCALE);
+	}
+
+	value = value_from_property(match, PRO_LIST_STYLE_TYPE);
+	if (value)
+	{
+		if (!strcmp(value->data, "none")) style->list_style_type = LST_NONE;
+		else if (!strcmp(value->data, "disc")) style->list_style_type = LST_DISC;
+		else if (!strcmp(value->data, "circle")) style->list_style_type = LST_CIRCLE;
+		else if (!strcmp(value->data, "square")) style->list_style_type = LST_SQUARE;
+		else if (!strcmp(value->data, "decimal")) style->list_style_type = LST_DECIMAL;
+		else if (!strcmp(value->data, "decimal-leading-zero")) style->list_style_type = LST_DECIMAL_ZERO;
+		else if (!strcmp(value->data, "lower-roman")) style->list_style_type = LST_LC_ROMAN;
+		else if (!strcmp(value->data, "upper-roman")) style->list_style_type = LST_UC_ROMAN;
+		else if (!strcmp(value->data, "lower-greek")) style->list_style_type = LST_LC_GREEK;
+		else if (!strcmp(value->data, "upper-greek")) style->list_style_type = LST_UC_GREEK;
+		else if (!strcmp(value->data, "lower-latin")) style->list_style_type = LST_LC_LATIN;
+		else if (!strcmp(value->data, "upper-latin")) style->list_style_type = LST_UC_LATIN;
+		else if (!strcmp(value->data, "lower-alpha")) style->list_style_type = LST_LC_ALPHA;
+		else if (!strcmp(value->data, "upper-alpha")) style->list_style_type = LST_UC_ALPHA;
+		else if (!strcmp(value->data, "armenian")) style->list_style_type = LST_ARMENIAN;
+		else if (!strcmp(value->data, "georgian")) style->list_style_type = LST_GEORGIAN;
+	}
+
+	value = value_from_property(match, PRO_OVERFLOW_WRAP);
+	if (value)
+	{
+		if (!strcmp(value->data, "break-word")) style->overflow_wrap = OVERFLOW_WRAP_BREAK_WORD;
+		else style->overflow_wrap = OVERFLOW_WRAP_NORMAL;
+	}
+
+	style->line_height = number_from_property(match, PRO_LINE_HEIGHT, 1.2f, N_SCALE);
+	style->leading = number_from_property(match, PRO_LEADING, 0, N_UNDEFINED);
+
+	style->text_indent = number_from_property(match, PRO_TEXT_INDENT, 0, N_LENGTH);
+	style->text_stroke_width = number_from_property(match, PRO_TEXT_STROKE_WIDTH, 0, N_LENGTH);
+
+	style->width = number_from_property(match, PRO_WIDTH, 0, N_AUTO);
+	style->height = number_from_property(match, PRO_HEIGHT, 0, N_AUTO);
+
+	style->margin[0] = number_from_property(match, PRO_MARGIN_TOP, 0, N_LENGTH);
+	style->margin[1] = number_from_property(match, PRO_MARGIN_RIGHT, 0, N_LENGTH);
+	style->margin[2] = number_from_property(match, PRO_MARGIN_BOTTOM, 0, N_LENGTH);
+	style->margin[3] = number_from_property(match, PRO_MARGIN_LEFT, 0, N_LENGTH);
+
+	style->padding[0] = number_from_property(match, PRO_PADDING_TOP, 0, N_LENGTH);
+	style->padding[1] = number_from_property(match, PRO_PADDING_RIGHT, 0, N_LENGTH);
+	style->padding[2] = number_from_property(match, PRO_PADDING_BOTTOM, 0, N_LENGTH);
+	style->padding[3] = number_from_property(match, PRO_PADDING_LEFT, 0, N_LENGTH);
+
+	style->color = color_from_property(match, PRO_COLOR, black);
+	style->text_fill_color = color_from_properties(match, PRO_TEXT_FILL_COLOR, PRO_COLOR, black);
+	style->text_stroke_color = color_from_property(match, PRO_TEXT_STROKE_COLOR, transparent);
+	style->background_color = color_from_property(match, PRO_BACKGROUND_COLOR, transparent);
+
+	style->border_spacing = number_from_property(match, PRO_BORDER_SPACING, 0, N_LENGTH);
+
+	style->border_style_0 = border_style_from_property(match, PRO_BORDER_TOP_STYLE);
+	style->border_style_1 = border_style_from_property(match, PRO_BORDER_RIGHT_STYLE);
+	style->border_style_2 = border_style_from_property(match, PRO_BORDER_BOTTOM_STYLE);
+	style->border_style_3 = border_style_from_property(match, PRO_BORDER_LEFT_STYLE);
+
+	style->border_color[0] = color_from_property(match, PRO_BORDER_TOP_COLOR, style->color);
+	style->border_color[1] = color_from_property(match, PRO_BORDER_RIGHT_COLOR, style->color);
+	style->border_color[2] = color_from_property(match, PRO_BORDER_BOTTOM_COLOR, style->color);
+	style->border_color[3] = color_from_property(match, PRO_BORDER_LEFT_COLOR, style->color);
+
+	style->border_width[0] = border_width_from_property(match, PRO_BORDER_TOP_WIDTH);
+	style->border_width[1] = border_width_from_property(match, PRO_BORDER_RIGHT_WIDTH);
+	style->border_width[2] = border_width_from_property(match, PRO_BORDER_BOTTOM_WIDTH);
+	style->border_width[3] = border_width_from_property(match, PRO_BORDER_LEFT_WIDTH);
+
+	{
+		const char *font_weight = string_from_property(match, PRO_FONT_WEIGHT, "normal");
+		const char *font_style = string_from_property(match, PRO_FONT_STYLE, "normal");
+		const char *font_variant = string_from_property(match, PRO_FONT_VARIANT, "normal");
+		int is_bold = is_bold_from_font_weight(font_weight);
+		int is_italic = is_italic_from_font_style(font_style);
+		style->small_caps = !strcmp(font_variant, "small-caps");
+		value = value_from_property(match, PRO_FONT_FAMILY);
+		for (; value; value = value->next)
+		{
+			/* ignore numbers and keywords used in font short-hand syntax */
+			if (value->type == CSS_STRING || (value->type == CSS_KEYWORD && !keyword_in_list(value->data, font_family_ignore, nelem(font_family_ignore))))
+			{
+				style->font = fz_load_html_font(ctx, set, value->data, is_bold, is_italic, style->small_caps);
+				if (style->font)
+					break;
+			}
+		}
+		if (!style->font)
+			style->font = fz_load_html_font(ctx, set, "serif", is_bold, is_italic, style->small_caps);
+	}
+}
+
+#ifdef DEBUG_CSS_SPLAY
+static void
+do_verify_splay(const fz_css_style_splay *x)
+{
+	printf("%x<", x);
+	if (x->lt)
+	{
+		assert(memcmp(&x->lt->style, &x->style, sizeof(x->style)) < 0);
+		assert(x->lt->up == x);
+		do_verify_splay(x->lt);
+	}
+	printf(",");
+	if (x->gt)
+	{
+		assert(memcmp(&x->gt->style, &x->style, sizeof(x->style)) > 0);
+		assert(x->gt->up == x);
+		do_verify_splay(x->gt);
+	}
+	printf(">\n");
+}
+
+static void
+verify_splay(const fz_css_style_splay *x)
+{
+	if (x == NULL)
+		return;
+	assert(x->up == NULL);
+	do_verify_splay(x);
+	printf("-----\n");
+}
+#endif
+
+const fz_css_style *
+fz_css_enlist(fz_context *ctx, const fz_css_style *style, fz_css_style_splay **tree, fz_pool *pool)
+{
+	fz_css_style_splay **current = tree;
+	fz_css_style_splay *x;
+	fz_css_style_splay *y = NULL;
+
+	/* Search for a match in the tree, if there is one, or for
+	 * the insertion point, if there is not. */
+	while (*current != NULL)
+	{
+		int cmp = memcmp(style, &(*current)->style, sizeof(*style));
+		if (cmp == 0)
+		{
+			/* We have a match - break out and do move to root. */
+			break;
+		}
+		y = (*current);
+		if (cmp < 0)
+			current = &y->lt;
+		else
+			current = &y->gt;
+	}
+	/* Create one if needed */
+	if (*current == NULL)
+	{
+		x = *current = fz_pool_alloc(ctx, pool, sizeof(*y));
+		x->style = *style;
+		x->up = y;
+		x->lt = NULL;
+		x->gt = NULL;
+	}
+	else
+		x = *current;
+	/* Now move to root */
+	/*
+	The splaying steps used:
+
+	Case 1:	|a)       z              x             b)     z                   x
+		|     y       D  =>  A       y            A       y           y       D
+		|   x   C                  B   z                B   x  =>  z     C
+		|  A B                        C D                  C D    A B
+
+	Case 2:	|a)       z              x             b)     z                   x
+		|     y       D  =>   y     z             A       y    =>     z       y
+		|   A   x            A B   C D                  x   D        A B     C D
+		|      B C                                     B C
+
+	Case 3:	|a)       y              x             b)     y                   x
+		|      x     C   =>   A     y              A     x      =>     y     C
+		|     A B                  B C                  B C           A B
+	*/
+#ifdef DEBUG_CSS_SPLAY
+	printf("BEFORE\n");
+	verify_splay(*tree);
+#endif
+	while ((y = x->up) != NULL ) /* While we're not at the root */
+	{
+		fz_css_style_splay *z = y->up;
+		y->up = x;
+		if (z == NULL)
+		{
+			if (y->lt == x)	/* Case 3a */
+			{
+				y->lt = x->gt;
+				if (y->lt)
+					y->lt->up = y;
+				x->gt = y;
+			}
+			else /* Case 3b */
+			{
+				y->gt = x->lt;
+				if (y->gt)
+					y->gt->up = y;
+				x->lt = y;
+			}
+			x->up = NULL;
+			break;
+		}
+		x->up = z->up;
+		if (z->up)
+		{
+			if (z->up->lt == z)
+				z->up->lt = x;
+			else
+				z->up->gt = x;
+		}
+		if (z->lt == y)
+		{
+			if (y->lt == x) /* Case 1a */
+			{
+				z->lt = y->gt;
+				if (z->lt)
+					z->lt->up = z;
+				y->lt = x->gt;
+				if (y->lt)
+					y->lt->up = y;
+				y->gt = z;
+				z->up = y;
+				x->gt = y;
+			}
+			else /* Case 2a */
+			{
+				y->gt = x->lt;
+				if (y->gt)
+					y->gt->up = y;
+				z->lt = x->gt;
+				if (z->lt)
+					z->lt->up = z;
+				x->lt = y;
+				x->gt = z;
+				z->up = x;
+			}
+		}
+		else
+		{
+			if (y->gt == x) /* Case 1b */
+			{
+				z->gt = y->lt;
+				if (z->gt)
+					z->gt->up = z;
+				y->gt = x->lt;
+				if (y->gt)
+					y->gt->up = y;
+				y->lt = z;
+				z->up = y;
+				x->lt = y;
+			}
+			else /* Case 2b */
+			{
+				z->gt = x->lt;
+				if (z->gt)
+					z->gt->up = z;
+				y->lt = x->gt;
+				if (y->lt)
+					y->lt->up = y;
+				x->gt = y;
+				x->lt = z;
+				z->up = x;
+			}
+		}
+	}
+
+	*tree = x;
+#ifdef DEBUG_CSS_SPLAY
+	printf("AFTER\n");
+	verify_splay(x);
+#endif
+
+	return &x->style;
+}
+
+/*
+ * Pretty printing
+ */
+
+static void print_value(fz_css_value *val)
+{
+	printf("%s", val->data);
+	if (val->args)
+	{
+		printf("(");
+		print_value(val->args);
+		printf(")");
+	}
+	if (val->next)
+	{
+		printf(" ");
+		print_value(val->next);
+	}
+}
+
+static void print_property(fz_css_property *prop)
+{
+	printf("\t%s: ", fz_css_property_name(prop->name));
+	print_value(prop->value);
+	if (prop->important)
+		printf(" !important");
+	printf(";\n");
+}
+
+static void print_condition(fz_css_condition *cond)
+{
+	if (cond->type == '=')
+		printf("[%s=%s]", cond->key, cond->val);
+	else if (cond->type == '[')
+		printf("[%s]", cond->key);
+	else
+		printf("%c%s", cond->type, cond->val);
+	if (cond->next)
+		print_condition(cond->next);
+}
+
+static void print_selector(fz_css_selector *sel)
+{
+	if (sel->combine)
+	{
+		print_selector(sel->left);
+		if (sel->combine == ' ')
+			printf(" ");
+		else
+			printf(" %c ", sel->combine);
+		print_selector(sel->right);
+	}
+	else if (sel->name)
+		printf("%s", sel->name);
+	else
+		printf("*");
+	if (sel->cond)
+	{
+		print_condition(sel->cond);
+	}
+}
+
+static void print_rule(fz_css_rule *rule)
+{
+	fz_css_selector *sel;
+	fz_css_property *prop;
+
+	for (sel = rule->selector; sel; sel = sel->next)
+	{
+		print_selector(sel);
+		printf(" /* %d */", selector_specificity(sel, 0));
+		if (sel->next)
+			printf(", ");
+	}
+
+	printf("\n{\n");
+	for (prop = rule->declaration; prop; prop = prop->next)
+	{
+		print_property(prop);
+	}
+	printf("}\n");
+}
+
+void
+fz_debug_css(fz_context *ctx, fz_css *css)
+{
+	fz_css_rule *rule = css->rule;
+	while (rule)
+	{
+		print_rule(rule);
+		rule = rule->next;
+	}
+}