Print Layout Templates
Status: Implemented
Author: Toluwaleke Ogundipe
Reviewers: Federico Mena Quintero, Jonathan Blandford
Goals
Specify a format for defining reusable print layout templates.
Rationale
Different puzzle kinds require slightly different layouts when printed on paper. All of them require the puzzle grid and the clues, but for example, in arrowword puzzles, the clues are embedded in the grid and they do not require a list of clues.
Overall Approach
Print layout templates are reusable, declarative specifications that define how different elements of a puzzle are arranged and rendered on a printed page.
At its core, a template describes a layout applicable to a set of puzzle kind(s), breaking down the printable area into a hierarchy of elements, each with a distinct role, such as displaying the grid, clues, title, metadata, or other visual components. Thanks to the flexibility of the layout engine, the same template may be used for multiple page sizes.
These templates provide a consistent and flexible way to support various puzzle kinds and page sizes.
The Top-Level
At the top-level, a TEMPLATE contains some metadata, the main page, and an optional overflow page with space for additional clues. Each page is typically a nested set of ELEMENTS.
Templates are defined in the JSON format as follows.
Important
The code blocks herein are not valid JSON but descriptions of the expected format.
Field/Member names are case-sensitive.
Unknown/Unexpected fields are ignored.
{
// Applicable puzzle kinds (case-insensitive).
"puzzle_kinds": [ String, ... ],
// The main page.
"main_page": Box,
// Optional: An overflow page for additional clues.
"overflow": Box
}
Where Box is a container of elements and has the following format:
{
// The orientation of the box (case-insensitive); one of:
//
// - horizontal
// - vertical
"orientation": String
// Sub-elements of the box.
"elements": [ Element, ... ]
}
Elementdefines a template element.must contain at least one non-DIVIDER element.
Note
When dealing with a BOX:
the axis of orientation is called the MAJOR axis.
the other axis is called the MINOR axis.
Template Elements
There are two basic categories of elements:
DISPLAY elements: These might contain text, a grid, or something else, or even nothing at all (to insert white space between other elements).
CONTAINER elements: These are used to arrange other elements.
Primarily, each element has a KIND and may include kind-specific data to customize its look/behavior, e.g font size/styles for text, or clue flow information for multi-region clue layouts. Additionally, it may also include data for sizing within a container.
Element Dimension Kinds
Each dimension (width or height) of an element can be one of the following kinds (or be computed in one of the following ways):
INTRINSIC: This dimension can be computed from just the element data.
DERIVED: Given the other dimension (width), this dimension (height) can be computed from the element data.
Important
No element may have a DERIVED width. Otherwise, it could result in a situation where a CONTAINER element has both DERIVED width and height, in which case it would be imposible to compute either.
Thankfully, no element of puzzle printing naturally has a DERIVED width.
WEIGHTED: This dimension cannot be computed from the element data; it is computed by the element’s parent box using the element’s ratio.
Every DISPLAY element kind has a pre-defined dimension kind along each axis, but the dimension kind of a container element on either axis depends on the dimension kinds of the elements it contains. The dimension kinds for each element kind are defined later on.
Element Format
An element has the following format:
{
// The element kind (case-insensitive).
"kind": String,
// Optional: Relative ratio of the element within its parent box;
// Positive.
"ratio": Float,
// Optional: The minimum dimension of the element along its parent box'
// major axis, in millimeters;
// Positive.
"minimum": Float,
// Optional: The maximum dimension of the element along its parent box'
// major axis, in millimeters;
// Positive.
"maximum": Float,
// Optional: Whether to omit the element if its minimum dimension is not
// satisfied.
"cut_off": Boolean,
// Optional: Whether the element's dimension along its parent box' major
// axis may be added to, if there's excess space left in the box.
"takes_excess": Boolean,
// Optional: Used for element kinds that require extra info.
"data": ...
}
for
ratio:the default is 1.0 i.e when not specified.
it applies only if the element has a WEIGHTED dimension along its parent box’ major axis e.g a NOTES element in a horizontal box, and ignored otherwise e.g for a NOTES element in a vertical box.
within each box, sub-element ratios are relative i.e the normalized/absolute ratio of each sub-element (WEIGHTED) is
(sub-element ratio) / (sum of all sub-element ratios); this is more flexible than absolute ratios, without significant additional cost.
for
minimum:the default is 0.0 i.e when not specified.
Note
The minimum dimension is not guranteed; the parent box can only allocate as much as is available.
The minimum dimension applies if and only if the computed dimension is greater than zero.
for
maximum:if not specified, the element’s dimension is only limited by:
its
ratio, if it has a WEIGHTED dimension along its parent box’ major axis, orthe space available in its parent box.
if specified, it must be greater than or equal to
minimum.
for
cut_off:the default is
falsei.e when not specified.if
trueand the dimension allocated to the element is less thanminimum, the element is omitted and other elements may take up its space.
for
takes_excess:the default is
falsei.e when not specified.it only applies to elements with non-WEIGHTED dimensions along the parent box’ major axis (reason explained below); ignored otherwise.
if true and the element’s parent box has space left (after computing the dimensions of all sub-elements), the excess space is distributed amongst all sub-elements of the box which have this field set to
trueand haven’t been cut off, such that they don’t exceed theirmaximums.
Note
Excess space in a box occurs when it contains sub-elements with WEIGHTED dimensions along its major axis and all those sub-elements either get cut off or limited by their
maximums.Since all sub-elements with WEIGHTED dimensions are already either cut off or allocated their
maximums, only sub-elements with non-WEIGHTED dimensions (if any) may take up the excess space.Even if there are sub-elements within a box that have this field set to
true, all the excess space may not be taken up if those sub-elements have been cut off or are limited by theirmaximums.
Element Kinds
The element kinds are as follows. Each doesn’t use the data field except
stated otherwise.
TITLE: The puzzle’s title.
The width is WEIGHTED.
The height is DERIVED.
The
datafield has the following format:{ // The font description with which to render text "font": String }
fontis in the format accepted bypango_font_description_from_string(), with the following specifics:only the font size is required, and must be in points.
if the font family is not specified, a default is used.
METADATA: The publisher, author, date, etc (as many as are defined).
The dimension kinds are as for the TITLE element kind.
The
datafield is as for the TITLE element kind.
INTRO: The puzzle’s intro.
The dimension kinds are as for the TITLE element kind.
The
datafield is as for the TITLE element kind.
NOTES: The puzzle’s notes.
The dimension kinds are as for the TITLE element kind.
The
datafield is as for the TITLE element kind.
GRID: The puzzle’s grid.
The width is INTRINSIC.
The height is DERIVED.
Note
This is so that when the allocated width is less than the intrinsic width, just the right amount of height needed can be computed. Unfortunately, the converse can’t be easily/neatly achieved.
CLUES: The puzzle’s clues.
The width is WEIGHTED.
The height is WEIGHTED.
The
datafield has the following format:{ // A unique (amongst CLUES elements) identifier; // Positive. "id": Integer, // Clue direction(s) to contain (case-insensitive). One of: // // - all: Contain all clue directions, one after another // - any: Any available clue direction // - <any of the standard IPUZ directions> "direction": String, // The ID of another CLUES element into which the content of this // element may flow. "flows_into": Integer, // The font description with which to render text "font": String }
idis required.a source is a CLUES element into which no other flows.
an extension is a CLUES element into which another flows.
all data fields other than
idare optional, with the following exception(s):directionandfontare required for a source.
for
direction:if specified,
directionis ignored for an extension.if direction
allis used, there must be only one source.if direction
anyis used, thedirectionof all sources must beany.no two sources may have the same IPUZ direction.
for
flows_into:if specified, there must exist another CLUES element with that value as its
id.a CLUES element must not flow into itself.
multiple CLUES elements must not flow into the same other CLUES element.
clues may flow across pages, but only forward i.e a CLUES element must not flow into another on a page before it.
a cyclic flow (i.e a loop of CLUES elements that have no apparent source nor end) is invalid.
fontis as for the TITLE element kind.if not specified for an extension, it inherits that of its source.
SOLUTION: The puzzle’s solution in some form.
The width is INTRINSIC.
The height is DERIVED (for the same reason as the GRID element kind).
DIVIDER: A vertical/horizontal divider.
The dimension kinds depend on the orientation of its parent box:
horizontal box
vertical box
width
INTRINSIC
WEIGHTED
height
WEIGHTED
INTRINSIC
Has a pre-defined thickness.
NOTE: This is a DISPLAY element, NOT a CONTAINER.
SPACER: A blank space.
The width is WEIGHTED.
The height is WEIGHTED.
BOX: A container to organize elements.
Along each axis, the dimension kind is determined by a set of rules which take priority in the order in which they’re stated below; the first one whose condition is true determines the dimension kind.
Along the major axis:
If the box contains at least one element with a WEIGHTED dimension along that axis, the dimension is WEIGHTED.
If the box contains at least one element with a DERIVED dimension along that axis, the dimension is DERIVED.
Otherwise, the dimension is INTRINSIC.
Along the minor axis:
If the box contains at least one element with a DERIVED dimension along that axis, the dimension is DERIVED.
If the box contains at least one element with an INTRINSIC dimension along that axis, the dimension is INTRINSIC.
Otherwise, the dimension is WEIGHTED.
The
datafield has the format of aBoxas earlier defined for the top-level page fields.
Template Config
The template loader accepts a configuration parameter consisting of the following fields:
clues_min_width(double): If greater than zero, then for every CLUES element within a horizontal BOX:if
minimumis specified, it is overriden with this value.if
maximumis also specified (i.e only ifminimumis overriden), but less than this value, it is also overriden (to preserve the constraintmaximum >= minimum).
This is mostly a hack to help reduce the number of templates needed to cover various combinations of cases, but is actually effective enough.
Template Selection/Matching
A set of templates are pre-defined and used for printing puzzles. A template is selected based on the following criteria:
Puzzle kind: If a template’s
puzzle_kindsfield contains the target puzzle kind, it’s a match.
The pre-defined templates are loaded and tested in succession until one matches. The first to match (for all criteria) is selected. If none matches, selection fails. Ideally, there should be one and only one potential match per puzzle.
Areas For Improvement
[ ] Allow user-defined templates. This will require the following (amongst other things):
[ ] a version field in the format to prevent breakage when there are breaking changes to the format.
[ ] report errors during template loading, instead of treating them as programming errors.
[ ] A GUI to allow users to interactively create, modify and save layouts. This will require the following (amongst other things):
[ ] recursively serializing element structures to JSON (could use
JsonBuilder+JsonGenerator).