One LaTeX design for a company, in three templates
A company that writes with LaTeX ends up with a document class, a letter class and a presentation theme that share a design and not a line of code. Every fix is then made three times, two of them late and the third never. Here the page is written once. A company declares its colours, faces and logo in one file, and the document template, the letter class and the presentation theme read that one file. Nothing in the suite carries a colour value, a font name or a file name of any company, which is what lets it be published while the companies stay private. Every measure of the page follows from a measurement or from a named definition: the room the head and the foot need is taken from the boxes they really build, one line of the body text stands between the head and the text and between the text and the foot, a heading never stands alone at the foot of a page, a picture takes the size of the family, and a Markdown file reaches the same page as the same document written in LaTeX. The suite ships with a company that does not exist, Nordwind AG, so that it builds and is measured anywhere: three colours, the TeX Gyre families every installation carries, and an icon drawn in TikZ. 574 checks over five measurements, all of them on the rendered page or on the build log rather than on the source. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
adc58e19d7
39 files changed
+6627
No files matched your search
+747
@@ -0,0 +1,747 @@
|
||||
#!/usr/bin/env python3
|
||||
"""The page, measured where the reader looks at it.
|
||||
|
||||
Two things are measured, and the second is the one the family has paid for
|
||||
three times: an announcement and what it announces stand on the same page. The
|
||||
fixture produces that situation itself, over every distance to the page foot,
|
||||
instead of hoping that a document happens to carry it.
|
||||
"""
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
import support
|
||||
from support import Check, Page, build
|
||||
|
||||
EXAMPLES = os.path.join(support.ROOT, "examples")
|
||||
|
||||
HEAD = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage{nordwind}
|
||||
\begin{document}
|
||||
"""
|
||||
|
||||
# Every distance from a full page to none: the announcement is pushed down the
|
||||
# page line by line, and at some of those distances it used to stay behind alone
|
||||
# at the foot while what it announced began overleaf.
|
||||
CASES = 16
|
||||
|
||||
|
||||
def table(marker):
|
||||
return ("\\begin{longtable}{ll}\n\\toprule\n"
|
||||
"\\businesstablehead{Name} & \\businesstablehead{Value}\\\\\n"
|
||||
"\\midrule\n\\endhead\n"
|
||||
"%s & 1\\\\ \\businessrowrule\n"
|
||||
"Second row & 2\\\\\n\\bottomrule\n\\end{longtable}\n" % marker)
|
||||
|
||||
|
||||
def long_table(marker):
|
||||
"""A table that asks for much more room than a heading does by itself."""
|
||||
rows = "".join("Row %d & %d\\\\ \\businessrowrule\n" % (number, number)
|
||||
for number in range(12))
|
||||
return ("\\begin{longtable}{ll}\n\\toprule\n"
|
||||
"\\businesstablehead{Name} & \\businesstablehead{Value}\\\\\n"
|
||||
"\\midrule\n\\endhead\n"
|
||||
"%s & 1\\\\ \\businessrowrule\n%s"
|
||||
"Last row & 2\\\\\n\\bottomrule\n\\end{longtable}\n"
|
||||
% (marker, rows))
|
||||
|
||||
|
||||
def pair_document():
|
||||
"""A heading directly under a heading, over every distance to the foot.
|
||||
|
||||
Two headings that follow one another are one announcement: the second names
|
||||
a part of what the first names, and a page that breaks between them carries
|
||||
a heading and nothing else.
|
||||
"""
|
||||
parts = [HEAD]
|
||||
for case in range(CASES):
|
||||
parts.append("\\newpage\n")
|
||||
parts.append("Filler line.\\par\n" * case)
|
||||
parts.append("\\section{Pair case %d}\n" % case)
|
||||
parts.append("\\subsection{Under case %d}\n" % case)
|
||||
parts.append("Lorem ipsum dolor sit amet.\\par\n" * 3)
|
||||
# And the same with something under the second heading that asks for much
|
||||
# more room than a heading does by itself: there the reservation of the
|
||||
# second one breaks the page, and the first is left behind alone.
|
||||
for case in range(CASES):
|
||||
parts.append("\\newpage\n")
|
||||
parts.append("Filler line.\\par\n" * case)
|
||||
parts.append("\\section{Deep case %d}\n" % case)
|
||||
parts.append("\\subsection{Deeper case %d}\n" % case)
|
||||
parts.append(long_table("Deep cell case %d" % case))
|
||||
# And with a picture under the second heading: a picture measures nothing
|
||||
# into the aux file, so the reservation of the heading is its own eight
|
||||
# lines, and what breaks the page is the picture that does not fit.
|
||||
for case in range(CASES):
|
||||
parts.append("\\newpage\n")
|
||||
parts.append("Filler line.\\par\n" * case)
|
||||
parts.append("\\section{Shown case %d}\n" % case)
|
||||
parts.append("\\subsection{Picture case %d}\n" % case)
|
||||
parts.append("\\begin{figure}[H]\\centering\n"
|
||||
"\\includegraphics[height=0.55\\textheight]"
|
||||
"{example-image}\n"
|
||||
"\\caption{Figure case %d}\n\\end{figure}\n" % case)
|
||||
parts.append("\\end{document}\n")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
def sweep_document():
|
||||
parts = [HEAD]
|
||||
for case in range(CASES):
|
||||
parts.append("\\newpage\n")
|
||||
parts.append("Filler line.\\par\n" * case)
|
||||
parts.append("\\section{Heading case %d}\n" % case)
|
||||
parts.append("\\businessleadin[1]\nThe numbers:\n\\businesstogether\n")
|
||||
parts.append(table("Heading cell case %d" % case))
|
||||
for case in range(CASES):
|
||||
parts.append("\\newpage\n")
|
||||
parts.append("Filler line.\\par\n" * case)
|
||||
parts.append("\\businessleadin[1]\nSentence case %d:\n\\businesstogether\n"
|
||||
% case)
|
||||
parts.append(table("Sentence cell case %d" % case))
|
||||
parts.append("\\end{document}\n")
|
||||
return "".join(parts)
|
||||
|
||||
|
||||
# The title rule, measured between three bands of ink: a paragraph, the heading,
|
||||
# a paragraph. The page carries nothing else, so which band is which is known
|
||||
# and not guessed.
|
||||
RULE = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage[nologo]{nordwind}
|
||||
\begin{document}
|
||||
\pagestyle{empty}
|
||||
Lorem ipsum before the heading.
|
||||
|
||||
\section{Consetetur}
|
||||
|
||||
Dolor sit amet after the heading.
|
||||
\end{document}
|
||||
"""
|
||||
|
||||
|
||||
# The head of a page: a heading on the left, the logo with the name on the
|
||||
# right. The two texts are of very different size, so any alignment but the one
|
||||
# on the text line is visible at once.
|
||||
HEADLINE = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage{nordwind}
|
||||
%s\begin{document}
|
||||
\section{Lorem}
|
||||
Dolor sit amet.
|
||||
\end{document}
|
||||
"""
|
||||
HEADLINE_ONE = HEADLINE % ""
|
||||
# The same head with the lockup of two lines. The heading on the left has to
|
||||
# stand on the line of the NAME, the first of the two, and not on the middle of
|
||||
# a block that has grown a second line.
|
||||
HEADLINE_TWO = HEADLINE % (
|
||||
"\\businessidentity{tagline={Vernunft und Freiheit}}\n")
|
||||
|
||||
|
||||
# The three pictures of the rule, each alone on a page of its own: one taller
|
||||
# than wide, one wider than tall, and a raster of pixels that is smaller than
|
||||
# the page and may not be blown up past twice its size.
|
||||
PICTURES = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage{nordwind}
|
||||
\begin{document}
|
||||
\pagestyle{empty}
|
||||
\businessgraphic{example-image-9x16}
|
||||
\newpage
|
||||
\businessgraphic{example-image-16x9}
|
||||
\newpage
|
||||
\businessgraphic{example-image-a.png}
|
||||
\end{document}
|
||||
"""
|
||||
|
||||
|
||||
# The same raster picture in a document that allows it a tenth of enlargement
|
||||
# and no more: 401.5 points wide by itself, so 441.6 with the option below,
|
||||
# where the line would otherwise give it 540.6.
|
||||
ZOOM = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage[picturezoom=1.1]{nordwind}
|
||||
\begin{document}
|
||||
\businessgraphic{example-image-a.png}
|
||||
\end{document}
|
||||
"""
|
||||
|
||||
|
||||
# A picture that does not fit into what is left of the page: half a page of
|
||||
# text, then a picture that would take a whole one.
|
||||
PICTUREROOM = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage{nordwind}
|
||||
\begin{document}
|
||||
\pagestyle{empty}
|
||||
""" + "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.\\par\n" * 28 + r"""
|
||||
\businessgraphic{example-image-16x9}
|
||||
\end{document}
|
||||
"""
|
||||
|
||||
|
||||
# A document that opens with a heading, and the same one that opens with a
|
||||
# paragraph: the distance under the title block belongs to the block and is the
|
||||
# same in both.
|
||||
OPENING = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage{nordwind}
|
||||
\title{Lorem Ipsum}
|
||||
\author{Erika Muster}
|
||||
\date{21 September 2026}
|
||||
\begin{document}
|
||||
\maketitle
|
||||
%s
|
||||
\end{document}
|
||||
"""
|
||||
OPENING_HEADING = OPENING % "\n\\section{Consetetur}\n\nDolor sit amet.\n"
|
||||
OPENING_TEXT = OPENING % "\nDolor sit amet.\n"
|
||||
|
||||
|
||||
# The inner corner of the foot, with something in it that is taller than the
|
||||
# line the foot reserves. Without the measurement that gives the difference to
|
||||
# the foot, a logo of two centimetres is drawn into the text above it.
|
||||
FOOT = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage{nordwind}
|
||||
\businessfootleft{\includegraphics[height=2cm]{example-image}}
|
||||
\begin{document}
|
||||
\section{Consetetur}
|
||||
""" + "Lorem ipsum dolor sit amet, consetetur sadipscing.\\par\n" * 40 + r"""
|
||||
\end{document}
|
||||
"""
|
||||
|
||||
|
||||
# The two distances of the page, asked of the page itself. What a document may
|
||||
# put in the foot changes how much room the foot needs, so the fixture is built
|
||||
# twice: once with the bare line of the foot, once with a picture of two
|
||||
# centimetres in it. The numbers come out of the run because they ARE the
|
||||
# definition — the page is laid out from them, and a distance read off the
|
||||
# raster carries the slack of whichever letters happen to stand at the edge.
|
||||
EDGES = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage{nordwind}
|
||||
%s\makeatletter
|
||||
\AtBeginDocument{%%
|
||||
\typeout{EDGES line=\the\baselineskip}%%
|
||||
\typeout{EDGES depth=\the\dp\strutbox}%%
|
||||
\typeout{EDGES topskip=\the\topskip}%%
|
||||
\typeout{EDGES headsep=\the\headsep}%%
|
||||
\typeout{EDGES footskip=\the\footskip}%%
|
||||
\typeout{EDGES foot=\the\business@foothigh}}
|
||||
\makeatother
|
||||
\begin{document}
|
||||
\section{Consetetur}
|
||||
""" + "Lorem ipsum dolor sit amet, consetetur sadipscing.\\par\n" * 40 + r"""
|
||||
\end{document}
|
||||
"""
|
||||
# A company whose mark is taller than the family's. The head has to keep the
|
||||
# room for it, and everything under the head has to move down by what it took:
|
||||
# the head is MEASURED at the head, and a document that asks for another height
|
||||
# is the only thing that proves the measurement is one.
|
||||
HEADROOM = r"""\documentclass[a4paper,10pt]{article}
|
||||
\usepackage[logoheight=%s]{nordwind}
|
||||
\begin{document}
|
||||
\section{Lorem}
|
||||
Dolor sit amet.
|
||||
\end{document}
|
||||
"""
|
||||
|
||||
EDGES_LINE = EDGES % ""
|
||||
EDGES_PICTURE = EDGES % \
|
||||
"\\businessfootleft{\\includegraphics[height=2cm]{example-image}}\n"
|
||||
|
||||
|
||||
def bands(page):
|
||||
"""The rows of ink, grouped into the blocks they form."""
|
||||
rows = page.ink_rows()
|
||||
grouped, current = [], []
|
||||
for row in rows:
|
||||
if current and row - current[-1] > 1:
|
||||
grouped.append(current)
|
||||
current = []
|
||||
current.append(row)
|
||||
if current:
|
||||
grouped.append(current)
|
||||
return grouped
|
||||
|
||||
|
||||
def page_of(pdf, needle, count):
|
||||
"""The page a text stands on, counted from one; zero where it is missing."""
|
||||
for number in range(1, count + 1):
|
||||
found = support.run(["pdftotext", "-f", str(number), "-l", str(number),
|
||||
pdf, "-"]).stdout
|
||||
if needle in found:
|
||||
return number
|
||||
return 0
|
||||
|
||||
|
||||
def title_rule(check):
|
||||
source = support.write("titlerule.tex", RULE)
|
||||
pdf, _ = build(source, EXAMPLES)
|
||||
page = Page(pdf, 1)
|
||||
blocks = bands(page)
|
||||
check.that(len(blocks) == 3,
|
||||
f"the fixture should carry three blocks of ink, it carries "
|
||||
f"{len(blocks)}")
|
||||
if len(blocks) != 3:
|
||||
return
|
||||
factor = 72.0 / page.dpi
|
||||
above = (blocks[1][0] - blocks[0][-1]) * factor
|
||||
below = (blocks[2][0] - blocks[1][-1]) * factor
|
||||
# The two distances of the family are one line and a half over a heading and
|
||||
# half a line under it, which is the three to one the rule asks for. What
|
||||
# the page shows is that glue plus the slack of the two faces that meet at
|
||||
# it — the descender of the paragraph, the capitals of the heading — and
|
||||
# that slack is under half a line for every face this suite has been set
|
||||
# in. Measured in the report of the origin, the document this family works
|
||||
# from: 19.7 points over a heading of the second level and 9.1 under it, at
|
||||
# a line of twelve points.
|
||||
line = 12.0
|
||||
check.that(1.25 * line < above < 2.0 * line,
|
||||
f"the room over the heading is {above:.1f} points, and one line "
|
||||
f"and a half of {line:.0f} points with the slack of the faces is "
|
||||
"between 15 and 24")
|
||||
check.that(0.4 * line < below < 1.1 * line,
|
||||
f"the room under the heading is {below:.1f} points, and half a "
|
||||
f"line of {line:.0f} points with the slack of the faces is "
|
||||
"between 5 and 13")
|
||||
check.that(above > below,
|
||||
f"the room over the heading is {above:.1f} points and under it "
|
||||
f"{below:.1f}: a heading belongs to what follows it")
|
||||
|
||||
|
||||
def left_edge(check):
|
||||
pdf, _ = build(os.path.join(EXAMPLES, "sample.tex"), EXAMPLES)
|
||||
page = Page(pdf, 1)
|
||||
edges = []
|
||||
for band in bands(page):
|
||||
columns = page.ink_columns(band[0], band[-1] + 1)
|
||||
if columns:
|
||||
edges.append(columns[0])
|
||||
left = min(edges)
|
||||
# A paragraph of this design begins at the margin, and so does a heading.
|
||||
# The bands that legitimately begin further right are the ones the layout
|
||||
# puts there: the folio at the foot, the logo and the address in the head,
|
||||
# and the cells of a table.
|
||||
off = [edge for edge in edges if edge - left > 4]
|
||||
check.that(len(off) <= len(edges) / 2,
|
||||
f"{len(off)} of {len(edges)} blocks do not begin on the left "
|
||||
"edge of the text")
|
||||
|
||||
|
||||
def together(check):
|
||||
source = support.write("sweep.tex", sweep_document())
|
||||
pdf, _ = build(source, EXAMPLES)
|
||||
count = support.pages(pdf)
|
||||
for case in range(CASES):
|
||||
heading = page_of(pdf, "Heading case %d" % case, count)
|
||||
cell = page_of(pdf, "Heading cell case %d" % case, count)
|
||||
check.that(heading and cell and heading == cell,
|
||||
f"heading case {case}: the heading stands on page {heading} "
|
||||
f"and its table on page {cell}")
|
||||
for case in range(CASES):
|
||||
sentence = page_of(pdf, "Sentence case %d:" % case, count)
|
||||
cell = page_of(pdf, "Sentence cell case %d" % case, count)
|
||||
check.that(sentence and cell and sentence == cell,
|
||||
f"sentence case {case}: the sentence stands on page "
|
||||
f"{sentence} and its table on page {cell}")
|
||||
|
||||
|
||||
def foot(check):
|
||||
"""What a document puts in the inner corner of the foot stays in the foot."""
|
||||
source = support.write("foot.tex", FOOT)
|
||||
pdf, _ = build(source, EXAMPLES)
|
||||
page = Page(pdf, 1)
|
||||
blocks = bands(page)
|
||||
check.that(len(blocks) >= 2, "the page carries no foot")
|
||||
if len(blocks) < 2:
|
||||
return
|
||||
text, standing = blocks[-2], blocks[-1]
|
||||
factor = 72.0 / page.dpi
|
||||
gap = (standing[0] - text[-1]) * factor
|
||||
check.that(gap > 0,
|
||||
f"the foot overprints the text above it by {-gap:.1f} points")
|
||||
below = (page.height - standing[-1]) * factor
|
||||
check.that(14 < below < 57,
|
||||
f"the foot stands {below:.1f} points from the lower edge of the "
|
||||
"paper, outside the margin the page keeps")
|
||||
# And it is really the picture that stands there, not a line of text: a
|
||||
# block of two centimetres is 56.9 points high.
|
||||
high = (standing[-1] - standing[0] + 1) * factor
|
||||
check.that(high > 50,
|
||||
f"the foot block is {high:.1f} points high, so what the document "
|
||||
"put there did not arrive")
|
||||
|
||||
|
||||
def edges(check):
|
||||
"""One line of the text between the head and the text, and at the foot.
|
||||
|
||||
The head, the text and the foot are three blocks on one sheet, and the two
|
||||
distances between them are the same: exactly one line of the body text,
|
||||
whatever stands in the head and in the foot. Three lengths say it.
|
||||
|
||||
`\\headsep` is that distance over the text, which is how LaTeX defines it:
|
||||
from the lower edge of the head box to the upper edge of the text block.
|
||||
|
||||
`\\topskip` says where the FIRST line of a page stands in that block, and
|
||||
LaTeX sets it to a round ten points while a line stands `\\ht\\strutbox`
|
||||
over its own baseline. The block would then begin over its first line and
|
||||
the distance the page defines would not be the one a reader measures.
|
||||
|
||||
Under the text there is no such length: `\\footskip` runs from the baseline
|
||||
of the last line to the baseline of the foot, so the foot itself and what a
|
||||
line hangs below its baseline lie inside it. Both come off, and what is left
|
||||
is the line.
|
||||
"""
|
||||
for name, fixture, what in (("edgeline", EDGES_LINE, "a bare line"),
|
||||
("edgefoot", EDGES_PICTURE, "a picture")):
|
||||
source = support.write(name + ".tex", fixture)
|
||||
pdf, log = build(source, EXAMPLES)
|
||||
with open(log, encoding="utf-8") as handle:
|
||||
found = dict(re.findall(r"EDGES (\w+)=([0-9.]+)pt", handle.read()))
|
||||
check.that(len(found) == 6,
|
||||
f"the fixture with {what} in the foot reported "
|
||||
f"{len(found)} of the six lengths of the page")
|
||||
if len(found) != 6:
|
||||
continue
|
||||
line, depth = float(found["line"]), float(found["depth"])
|
||||
over = float(found["headsep"])
|
||||
under = float(found["footskip"]) - float(found["foot"]) - depth
|
||||
first = float(found["topskip"])
|
||||
check.that(abs(over - line) < 0.01,
|
||||
f"with {what} in the foot the head stands {over:.2f} points "
|
||||
f"over the text, and a line is {line:.2f}")
|
||||
check.that(abs(under - line) < 0.01,
|
||||
f"with {what} in the foot the text stands {under:.2f} points "
|
||||
f"over the foot, and a line is {line:.2f}")
|
||||
check.that(abs(first - (line - depth)) < 0.01,
|
||||
f"the first line of a page begins {first:.2f} points under "
|
||||
f"the upper edge of the text block, and a line stands "
|
||||
f"{line - depth:.2f} over its own baseline")
|
||||
# And the page shows it: between the last line of the text and what
|
||||
# stands in the foot there is a line, plus whatever that last line does
|
||||
# not hang below its baseline. Never less, and never a second line.
|
||||
page = Page(pdf, 1)
|
||||
blocks = bands(page)
|
||||
check.that(len(blocks) >= 2,
|
||||
f"the page with {what} in the foot carries no foot")
|
||||
if len(blocks) < 2:
|
||||
continue
|
||||
factor = 72.0 / page.dpi
|
||||
gap = (blocks[-1][0] - blocks[-2][-1]) * factor
|
||||
check.that(line - 0.5 < gap < line + depth + 0.5,
|
||||
f"with {what} in the foot the ink of the text stands "
|
||||
f"{gap:.1f} points over the ink of the foot, and a line of "
|
||||
f"{line:.0f} points with the depth of a line is between "
|
||||
f"{line:.0f} and {line + depth:.1f}")
|
||||
|
||||
|
||||
def headroom(check):
|
||||
"""A mark of another height, and the page that keeps the room for it.
|
||||
|
||||
The head reserves what it measures at itself, and that is only proven by a
|
||||
document which asks for a height the family did not set: at the one height
|
||||
every example carries, a reservation computed from a factor and one taken
|
||||
from the box look exactly alike. Two heights are built, the family's and
|
||||
twice it, and three things are measured — the log stays silent, the head
|
||||
grows with the mark, and the text under it moves down by what the head took.
|
||||
|
||||
Reported on 2026-09-22 from a sibling template of this family, which found
|
||||
the same gap in its own suite: the case had been reasoned about in the
|
||||
source and never built.
|
||||
"""
|
||||
seen = {}
|
||||
for name, height in (("small", "2em"), ("large", "4em")):
|
||||
source = support.write("headroom%s.tex" % name, HEADROOM % height)
|
||||
pdf, log = build(source, EXAMPLES)
|
||||
with open(log, encoding="utf-8") as handle:
|
||||
printed = handle.read()
|
||||
check.that("headheight is too small" not in printed,
|
||||
f"with a mark of {height} the page kept less room for the "
|
||||
"head than the head takes")
|
||||
reserved = {key: float(value) for key, value in
|
||||
re.findall(r"^\* \\(headheight|headsep|topmargin)"
|
||||
r"=(-?[0-9.]+)pt$", printed, re.MULTILINE)}
|
||||
check.that(len(reserved) == 3,
|
||||
f"with a mark of {height} the page did not report its "
|
||||
"layout")
|
||||
if len(reserved) != 3:
|
||||
return
|
||||
# Where the text block begins, out of the numbers the page was laid out
|
||||
# from: an inch of driver offset, the margin, the room for the head and
|
||||
# the distance under it. TeX points against the PostScript points of the
|
||||
# raster, which are the shorter by the ratio of the two inches.
|
||||
top = (72.27 + reserved["topmargin"] + reserved["headheight"]
|
||||
+ reserved["headsep"]) * 72.0 / 72.27
|
||||
page = Page(pdf, 1)
|
||||
factor = 72.0 / page.dpi
|
||||
blocks = bands(page)
|
||||
above = [band for band in blocks if band[-1] * factor < top]
|
||||
below = [band for band in blocks if band[0] * factor >= top - 1]
|
||||
check.that(len(above) + len(below) == len(blocks),
|
||||
f"with a mark of {height} a block of ink crosses the lower "
|
||||
"edge of the head")
|
||||
check.that(above and below,
|
||||
f"with a mark of {height} the page carries no head or no "
|
||||
"text")
|
||||
if not above or not below:
|
||||
return
|
||||
seen[name] = (reserved["headheight"], below[0][0] * factor, top)
|
||||
if len(seen) != 2:
|
||||
return
|
||||
# The mark doubles, so the room the head keeps grows with it — by about the
|
||||
# same twenty points, and never by nothing.
|
||||
grown = seen["large"][0] - seen["small"][0]
|
||||
check.that(grown > 15,
|
||||
f"the mark doubled and the room for the head grew by "
|
||||
f"{grown:.1f} points, so the room does not follow the mark")
|
||||
moved = seen["large"][1] - seen["small"][1]
|
||||
check.that(moved > 15,
|
||||
f"the text under the taller head begins {moved:.1f} points "
|
||||
"lower, so the page did not give the head what it took")
|
||||
|
||||
|
||||
def first_glyph(page, top, bottom, left, right):
|
||||
"""The column range of the leftmost letter in a rectangle of the page."""
|
||||
columns = [x for x in range(left, right)
|
||||
if any(page.dark(x, y) for y in range(top, bottom))]
|
||||
if not columns:
|
||||
return None
|
||||
start = end = columns[0]
|
||||
for column in columns[1:]:
|
||||
if column - end > 2:
|
||||
break
|
||||
end = column
|
||||
return start, end + 1
|
||||
|
||||
|
||||
def bottom_row(page, top, bottom, left, right):
|
||||
rows = [y for y in range(top, bottom)
|
||||
if any(page.dark(x, y) for x in range(left, right))]
|
||||
return rows[-1] if rows else None
|
||||
|
||||
|
||||
def head_line(check):
|
||||
"""The two texts of the head stand on ONE line.
|
||||
|
||||
The heading on the left is nine points, the name beside the logo sixteen,
|
||||
so the two carry every alignment differently: on a common top edge their
|
||||
baselines fall apart by the difference of the two cap heights, and the head
|
||||
reads as two blocks that missed each other. What is measured is the lower
|
||||
edge of the first capital on each side — the L of the heading and the N of
|
||||
the name, both flat on the baseline.
|
||||
|
||||
The tolerance is the rasterizer's own: a capital of sixteen points and one
|
||||
of nine end within a third of a point of each other at 300 dpi, while the
|
||||
defect this catches was sixteen points, the whole height of the name.
|
||||
|
||||
It is measured over both shapes of the lockup, the one of one line and the
|
||||
one of two: a block that has grown a second line still hands the head the
|
||||
baseline of its FIRST, or the heading on the left drops to the middle of a
|
||||
block it knows nothing about.
|
||||
"""
|
||||
for what, fixture, lines in (("one line", HEADLINE_ONE, 1),
|
||||
("two lines", HEADLINE_TWO, 2)):
|
||||
source = support.write("headline%d.tex" % lines, fixture)
|
||||
pdf, _ = build(source, EXAMPLES)
|
||||
page = Page(pdf, 1, dpi=300)
|
||||
# The topmost block of ink on the page IS the line of the head: the icon
|
||||
# reaches above and below the texts beside it, so the heading, the name
|
||||
# and the logo form one band. The address line under the logo and the
|
||||
# text of the page are the blocks after it.
|
||||
blocks = bands(page)
|
||||
check.that(len(blocks) >= 2, f"the page with {what} carries no head")
|
||||
if len(blocks) < 2:
|
||||
continue
|
||||
top, end = blocks[0][0], blocks[0][-1]
|
||||
middle = page.width // 2
|
||||
# On the right the first group of columns is the icon and the second the
|
||||
# name; on the left the first is the first letter of the heading.
|
||||
icon = first_glyph(page, top, end + 1, middle, page.width)
|
||||
check.that(icon, f"the head with {what} carries no logo")
|
||||
if not icon:
|
||||
continue
|
||||
# Right of the icon stand the lines of the block, and there they are
|
||||
# separate bands: the icon itself bridges them, because it is one shape
|
||||
# as tall as both. The name is the first of them.
|
||||
text = [y for y in range(top, end + 1)
|
||||
if any(page.dark(x, y) for x in range(icon[1] + 2, page.width))]
|
||||
rows, current = [], []
|
||||
for row in text:
|
||||
if current and row - current[-1] > 1:
|
||||
rows.append(current)
|
||||
current = []
|
||||
current.append(row)
|
||||
if current:
|
||||
rows.append(current)
|
||||
check.that(len(rows) == lines,
|
||||
f"the block beside the logo carries {len(rows)} lines and "
|
||||
f"should carry {lines}")
|
||||
if not rows:
|
||||
continue
|
||||
first, last = rows[0][0], rows[0][-1] + 1
|
||||
name = first_glyph(page, first, last, icon[1] + 2, page.width)
|
||||
heading = first_glyph(page, first, last, 0, middle)
|
||||
check.that(name and heading,
|
||||
f"the head with {what} carries no name or no heading")
|
||||
if not name or not heading:
|
||||
continue
|
||||
left_base = bottom_row(page, first, last, heading[0], heading[1])
|
||||
right_base = bottom_row(page, first, last, name[0], name[1])
|
||||
apart = abs(left_base - right_base) * 72.0 / page.dpi
|
||||
check.that(apart < 1,
|
||||
f"with {what} in the lockup the heading in the head and the "
|
||||
f"name beside the logo stand {apart:.1f} points apart and "
|
||||
"not on one line")
|
||||
|
||||
|
||||
def opening(check):
|
||||
"""The title block brings the distance under it, and nothing adds to it.
|
||||
|
||||
A heading brings three lines of space with it and the title block does too.
|
||||
Where a document opens with a heading, the two used to stand one under the
|
||||
other: 76.8 points between the date and the heading, against the 44.2 the
|
||||
same block gives a paragraph. Six lines of white where the page gives three
|
||||
everywhere else.
|
||||
"""
|
||||
gaps = []
|
||||
for name, source in (("openheading", OPENING_HEADING),
|
||||
("opentext", OPENING_TEXT)):
|
||||
pdf, _ = build(support.write(name + ".tex", source), EXAMPLES)
|
||||
page = Page(pdf, 1)
|
||||
blocks = bands(page)
|
||||
check.that(len(blocks) >= 6, f"{name}: the page carries no title block")
|
||||
if len(blocks) < 6:
|
||||
return
|
||||
factor = 72.0 / page.dpi
|
||||
# The bands of this page: the head, the address line under the logo,
|
||||
# the title, the author, the date — and then what the document opens
|
||||
# with, which is what this measures.
|
||||
gaps.append((blocks[5][0] - blocks[4][-1]) * factor)
|
||||
apart = abs(gaps[0] - gaps[1])
|
||||
check.that(apart < 6,
|
||||
f"a document that opens with a heading gives {gaps[0]:.1f} "
|
||||
f"points under the title block and one that opens with a "
|
||||
f"paragraph {gaps[1]:.1f}, which is {apart:.1f} points apart")
|
||||
|
||||
|
||||
def pairs(check):
|
||||
"""No page break between two headings that follow one another."""
|
||||
source = support.write("pairs.tex", pair_document())
|
||||
pdf, _ = build(source, EXAMPLES)
|
||||
count = support.pages(pdf)
|
||||
for case in range(CASES):
|
||||
upper = page_of(pdf, "Pair case %d" % case, count)
|
||||
lower = page_of(pdf, "Under case %d" % case, count)
|
||||
check.that(upper and lower and upper == lower,
|
||||
f"pair case {case}: the heading stands on page {upper} and "
|
||||
f"the heading under it on page {lower}")
|
||||
for case in range(CASES):
|
||||
upper = page_of(pdf, "Deep case %d" % case, count)
|
||||
lower = page_of(pdf, "Deeper case %d" % case, count)
|
||||
cell = page_of(pdf, "Deep cell case %d" % case, count)
|
||||
check.that(lower and cell and lower == cell,
|
||||
f"deep case {case}: the heading stands on page {lower} and "
|
||||
f"its table on page {cell}")
|
||||
for case in range(CASES):
|
||||
upper = page_of(pdf, "Shown case %d" % case, count)
|
||||
lower = page_of(pdf, "Picture case %d" % case, count)
|
||||
shown = page_of(pdf, "Figure case %d" % case, count)
|
||||
check.that(upper and lower and upper == lower,
|
||||
f"picture case {case}: the heading stands on page {upper} "
|
||||
f"and the heading under it on page {lower}")
|
||||
check.that(lower and shown and lower == shown,
|
||||
f"picture case {case}: the heading stands on page {lower} "
|
||||
f"and its picture on page {shown}")
|
||||
check.that(upper and lower and upper == lower,
|
||||
f"deep case {case}: the heading stands on page {upper} and "
|
||||
f"the heading under it on page {lower}")
|
||||
|
||||
|
||||
def pictures(check):
|
||||
"""What a picture takes of the page, measured at the ink on the page.
|
||||
|
||||
Upright: the full width of the line, at most half the height of the text.
|
||||
Lying: the full height that is free, at most the full width. A raster of
|
||||
pixels is enlarged twice over and no further. Every one of them centred.
|
||||
"""
|
||||
pdf, _ = build(support.write("pictures.tex", PICTURES), EXAMPLES)
|
||||
for number, (name, rule) in enumerate((
|
||||
("upright", "height"), ("lying", "width"),
|
||||
("raster", "zoom")), start=1):
|
||||
page = Page(pdf, number)
|
||||
factor = 72.0 / page.dpi
|
||||
# The page carries the head, the picture and the folio; the picture is
|
||||
# the tallest block of ink on it.
|
||||
blocks = bands(page)
|
||||
check.that(blocks, f"the {name} picture is not on its page")
|
||||
if not blocks:
|
||||
continue
|
||||
tallest = max(blocks, key=lambda band: band[-1] - band[0])
|
||||
top, bottom = tallest[0], tallest[-1]
|
||||
columns = page.ink_columns(top, bottom + 1)
|
||||
width = (columns[-1] - columns[0] + 1) * factor
|
||||
height = (bottom - top + 1) * factor
|
||||
left = columns[0] * factor
|
||||
right = page.width * factor - columns[-1] * factor
|
||||
# A4 is 597.5 points wide, the text block 540.6 with 28.35 on each
|
||||
# side, and the text is 713.9 points high.
|
||||
if rule == "height":
|
||||
check.that(abs(height - 0.5 * 713.9) < 12,
|
||||
f"the upright picture is {height:.1f} points high and "
|
||||
"not the half of the text height it may take")
|
||||
if rule == "width":
|
||||
check.that(abs(width - 540.6) < 4,
|
||||
f"the lying picture is {width:.1f} points wide against "
|
||||
"the 540.6 of the line")
|
||||
if rule == "zoom":
|
||||
# 401.5 points wide by itself, and the line gives 540.6, which is
|
||||
# 1.35 of it — under the two a raster may be enlarged by.
|
||||
check.that(abs(width - 540.6) < 4,
|
||||
f"the raster picture is {width:.1f} points wide against "
|
||||
"the 540.6 of the line")
|
||||
check.that(abs(left - right) < 4,
|
||||
f"the {name} picture stands {left:.1f} points from the left "
|
||||
f"edge and {right:.1f} from the right, so it is not centred")
|
||||
# A raster picture is enlarged as far as the document allows and no
|
||||
# further, which is what the option says.
|
||||
zoom, _ = build(support.write("zoom.tex", ZOOM), EXAMPLES)
|
||||
page = Page(zoom, 1)
|
||||
factor = 72.0 / page.dpi
|
||||
blocks = bands(page)
|
||||
tallest = max(blocks, key=lambda band: band[-1] - band[0])
|
||||
columns = page.ink_columns(tallest[0], tallest[-1] + 1)
|
||||
width = (columns[-1] - columns[0] + 1) * factor
|
||||
check.that(abs(width - 441.6) < 4,
|
||||
f"the raster picture is {width:.1f} points wide, and a tenth "
|
||||
"over its own 401.5 is 441.6")
|
||||
# And the picture that does not fit into what is left of the page stays on
|
||||
# it, small enough to fill what is there.
|
||||
room, _ = build(support.write("pictureroom.tex", PICTUREROOM), EXAMPLES)
|
||||
check.that(support.pages(room) == 1,
|
||||
f"the page with the picture broke into {support.pages(room)} "
|
||||
"pages instead of fitting the picture into what was left")
|
||||
if support.pages(room) == 1:
|
||||
page = Page(room, 1)
|
||||
factor = 72.0 / page.dpi
|
||||
tallest = max(bands(page), key=lambda band: band[-1] - band[0])
|
||||
high = (tallest[-1] - tallest[0] + 1) * factor
|
||||
# At the full width of the line the picture is 304 points high, and
|
||||
# what was left of the page is less than that.
|
||||
check.that(high < 300,
|
||||
f"the picture is {high:.1f} points high, so it was not made "
|
||||
"small enough for the room that was left")
|
||||
|
||||
|
||||
def main():
|
||||
check = Check("layout")
|
||||
title_rule(check)
|
||||
pairs(check)
|
||||
pictures(check)
|
||||
head_line(check)
|
||||
opening(check)
|
||||
edges(check)
|
||||
headroom(check)
|
||||
foot(check)
|
||||
left_edge(check)
|
||||
together(check)
|
||||
return check.done()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in new issue
Block a user