528 lines
23 KiB
Python
528 lines
23 KiB
Python
#!/usr/bin/env python3
|
|||
|
|
"""The identity interface: the suite carries no company, and the company reaches it.
|
||
|
|
|
||
|
|
Two kinds of question, and both are needed. What the suite may NOT contain is a
|
||
|
|
property of its source, so it is measured there. What a company really reaches on
|
||
|
|
the page is a property of the page, so it is measured there.
|
||
|
|
"""
|
||
|
|
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")
|
||
|
|
|
||
|
|
SUITE_FILES = ("business-identity.sty", "business-suite.sty",
|
||
|
|
"business-brief.cls", "business-beamer.sty")
|
||
|
|
|
||
|
|
# A colour value in the suite is a company's property in a file every other company
|
||
|
|
# loads. The palette belongs to the identity of the company, the roles to the
|
||
|
|
# suite, and a role carries a name and never a number.
|
||
|
|
COLOUR_VALUE = re.compile(r"\\definecolor\s*\{[^}]*\}\s*\{(HTML|RGB|rgb|cmyk|gray)\}")
|
||
|
|
# The three templates take the identity from one place and carry none of their
|
||
|
|
# own: not a colour, not a face. A copy passes every check that asks whether
|
||
|
|
# the values are right, and is wrong on the day the one place changes.
|
||
|
|
TEMPLATES = ("business-suite.sty", "business-brief.cls", "business-beamer.sty")
|
||
|
|
OWN_VALUE = re.compile(r"^\s*(\\definecolor|\\colorlet|\\setmainfont"
|
||
|
|
r"|\\setsansfont|\\setmonofont|\\newfontfamily"
|
||
|
|
r"|\\renewcommand\*?\s*\\familydefault)", re.MULTILINE)
|
||
|
|
# Everything after \endinput is dead: LaTeX stops reading there. A block moved
|
||
|
|
# to the end of a file lands behind it without a word in any log, and every
|
||
|
|
# document is then built without what it does. Measured in a sister package on
|
||
|
|
# 2026-09-21, where the head height was measured by code that never ran.
|
||
|
|
ENDINPUT = re.compile(r"^\\endinput\s*$", re.MULTILINE)
|
||
|
|
|
||
|
|
DOCUMENT = r"""\documentclass[a4paper,10pt]{article}
|
||
|
|
\usepackage{nordwind}
|
||
|
|
\title{Lorem Ipsum}
|
||
|
|
\author{Erika Muster}
|
||
|
|
\date{21 September 2026}
|
||
|
|
\begin{document}
|
||
|
|
\maketitle
|
||
|
|
\section{Consetetur}
|
||
|
|
Dolor sit amet \success{} and \fail{} and an arrow →.
|
||
|
|
\end{document}
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
# A company whose logo is a FILE and not a drawing, which is the other of the two
|
||
|
|
# ways the suite carries a logo and the one the README shows. `example-image` is
|
||
|
|
# the test image of TeX Live and lies on every installation.
|
||
|
|
FILE_COMPANY = r"""\ProvidesPackage{filecompany}[2026/09/21 A company with a logo file]
|
||
|
|
\RequirePackage{business-identity}
|
||
|
|
\businesscolours{brand = blue}
|
||
|
|
\setmainfont{Latin Modern Roman}
|
||
|
|
\businessfonts{default = rm, smallcaps = false}
|
||
|
|
\businessidentity{
|
||
|
|
name = Filecompany,
|
||
|
|
url = https://example.org,
|
||
|
|
address = Filecompany — https://example.org,
|
||
|
|
lockup = icon+text,
|
||
|
|
icon = example-image,
|
||
|
|
}
|
||
|
|
\DeclareOption*{\PassOptionsToPackage{\CurrentOption}{business-suite}}
|
||
|
|
\ProcessOptions\relax
|
||
|
|
\RequirePackage{business-suite}
|
||
|
|
\endinput
|
||
|
|
"""
|
||
|
|
|
||
|
|
FILE_DOCUMENT = r"""\documentclass[a4paper,10pt]{article}
|
||
|
|
\usepackage[nologo]{filecompany}
|
||
|
|
\begin{document}
|
||
|
|
\pagestyle{empty}
|
||
|
|
\noindent\logo[height=1cm]
|
||
|
|
|
||
|
|
\noindent\logo[height=2cm]
|
||
|
|
\end{document}
|
||
|
|
"""
|
||
|
|
|
||
|
|
LANGUAGE = r"""\documentclass[a4paper,10pt,ngerman]{article}
|
||
|
|
\usepackage{nordwind}
|
||
|
|
\usepackage[ngerman]{babel}
|
||
|
|
\title{Lorem Ipsum}
|
||
|
|
\author{Erika Muster}
|
||
|
|
\begin{document}
|
||
|
|
\maketitle
|
||
|
|
Dolor sit amet.
|
||
|
|
\end{document}
|
||
|
|
"""
|
||
|
|
|
||
|
|
|
||
|
|
# A company of two lines: the name, and the sentence it sets under the name, in
|
||
|
|
# a face and a colour of its own. Two pages, because the first carries the
|
||
|
|
# address line under the block and every other does not, and a logo in a
|
||
|
|
# sentence, because that one stays the one line it has always been.
|
||
|
|
TAGLINE_DOCUMENT = r"""\documentclass[a4paper,10pt]{article}
|
||
|
|
\usepackage{nordwind}
|
||
|
|
\businessidentity{tagline={Vernunft und Freiheit},
|
||
|
|
name face={\bfseries},
|
||
|
|
%s}
|
||
|
|
\title{Lorem Ipsum}
|
||
|
|
\author{Erika Muster}
|
||
|
|
\begin{document}
|
||
|
|
\maketitle
|
||
|
|
\section{Consetetur}
|
||
|
|
Lorem ipsum dolor sit amet. The logo in a sentence: \logo{} stays one line.
|
||
|
|
\newpage
|
||
|
|
\section{Dolor Sit}
|
||
|
|
At vero eos et accusam.
|
||
|
|
\end{document}
|
||
|
|
"""
|
||
|
|
TAGLINE_QUIET = TAGLINE_DOCUMENT % "tagline color=business-running-quiet"
|
||
|
|
# The same company without a word about the colour of its second line. Nothing
|
||
|
|
# is then set for that line, so it stands in the colour of the name over it.
|
||
|
|
TAGLINE_PLAIN = TAGLINE_DOCUMENT % "url=https://nordwind.example"
|
||
|
|
|
||
|
|
|
||
|
|
# The lockup alone on a page, so nothing but it carries ink. `nologo` keeps the
|
||
|
|
# head empty; what stands there is the one call being measured.
|
||
|
|
SIZES = r"""\documentclass[a4paper,10pt]{article}
|
||
|
|
\usepackage[nologo]{nordwind}
|
||
|
|
\businessidentity{tagline={Vernunft und Freiheit}}
|
||
|
|
\begin{document}
|
||
|
|
\thispagestyle{empty}
|
||
|
|
\noindent\logo[tagline, height=2cm%s]
|
||
|
|
\end{document}
|
||
|
|
"""
|
||
|
|
SIZES_MEASURED = SIZES % ""
|
||
|
|
SIZES_FACTOR = SIZES % ", taglinefactor=0.4"
|
||
|
|
SIZES_HIGH = SIZES % ", iconheight=3cm"
|
||
|
|
SIZES_WIDE = SIZES % ", iconwidth=1cm"
|
||
|
|
|
||
|
|
|
||
|
|
def spread(found, *words):
|
||
|
|
"""The left and the right edge of a group of words of one line."""
|
||
|
|
boxes = [word for word in found if word[0] in words]
|
||
|
|
if not boxes:
|
||
|
|
return None
|
||
|
|
return min(box[1] for box in boxes), max(box[3] for box in boxes)
|
||
|
|
|
||
|
|
|
||
|
|
def icon_box(page):
|
||
|
|
"""The rectangle the icon stands in: the leftmost group of ink columns."""
|
||
|
|
columns = page.ink_columns()
|
||
|
|
if not columns:
|
||
|
|
return None
|
||
|
|
end = columns[0]
|
||
|
|
for column in columns[1:]:
|
||
|
|
if column - end > 2:
|
||
|
|
break
|
||
|
|
end = column
|
||
|
|
rows = [y for y in range(page.height)
|
||
|
|
if any(page.dark(x, y) for x in range(columns[0], end + 1))]
|
||
|
|
factor = 72.0 / page.dpi
|
||
|
|
return ((end - columns[0] + 1) * factor, (rows[-1] - rows[0] + 1) * factor)
|
||
|
|
|
||
|
|
|
||
|
|
def lockup_sizes(check):
|
||
|
|
"""The two sizes of the lockup that measure themselves.
|
||
|
|
|
||
|
|
The tagline is set to the size at which it is exactly as WIDE as the name
|
||
|
|
over it, so the block has one left edge and one right edge. And the icon is
|
||
|
|
as HIGH as that block, because the mark stands beside the text and a height
|
||
|
|
of its own agrees with the text at one size of type and at no other. Both
|
||
|
|
are measured here, and both overrides with them: a factor for the tagline
|
||
|
|
and a height or a width for the icon, each one taking the measurement out of
|
||
|
|
the way for that call alone.
|
||
|
|
"""
|
||
|
|
source = support.write("lockupsizes.tex", SIZES_MEASURED)
|
||
|
|
pdf, _ = build(source, EXAMPLES)
|
||
|
|
found = support.words(pdf, 1)
|
||
|
|
name = spread(found, "Nordwind")
|
||
|
|
tag = spread(found, "Vernunft", "und", "Freiheit")
|
||
|
|
check.that(name and tag, "the lockup did not reach the page")
|
||
|
|
if not name or not tag:
|
||
|
|
return
|
||
|
|
wide, under = name[1] - name[0], tag[1] - tag[0]
|
||
|
|
check.that(abs(wide - under) < 1,
|
||
|
|
f"the name is {wide:.1f} points wide and the tagline "
|
||
|
|
f"{under:.1f}: the second line is set to the width of the first")
|
||
|
|
check.that(abs(name[0] - tag[0]) < 1,
|
||
|
|
"the two lines of the block do not share their left edge")
|
||
|
|
# The icon spans the block: from the top of the first line to the bottom of
|
||
|
|
# the last. Measured at the ink of the drawing, which fills its own box.
|
||
|
|
page = Page(pdf, 1)
|
||
|
|
box = icon_box(page)
|
||
|
|
check.that(box, "the icon did not reach the page")
|
||
|
|
if not box:
|
||
|
|
return
|
||
|
|
block = max(word[4] for word in found) - min(word[2] for word in found)
|
||
|
|
check.that(abs(box[1] - block) < 4,
|
||
|
|
f"the icon is {box[1]:.1f} points high and the text beside it "
|
||
|
|
f"{block:.1f}: the icon follows the text")
|
||
|
|
# And each override takes one of the two measurements out of the way.
|
||
|
|
source = support.write("lockupfactor.tex", SIZES_FACTOR)
|
||
|
|
pdf, _ = build(source, EXAMPLES)
|
||
|
|
found = support.words(pdf, 1)
|
||
|
|
name = spread(found, "Nordwind")
|
||
|
|
tag = spread(found, "Vernunft", "und", "Freiheit")
|
||
|
|
if name and tag:
|
||
|
|
check.that(tag[1] - tag[0] < name[1] - name[0] - 5,
|
||
|
|
"a company that names a factor for its tagline still got "
|
||
|
|
"the line set to the width of the name")
|
||
|
|
for name, fixture, index, asked in (("high", SIZES_HIGH, 1, 85.04),
|
||
|
|
("wide", SIZES_WIDE, 0, 28.35)):
|
||
|
|
source = support.write("lockup%s.tex" % name, fixture)
|
||
|
|
pdf, _ = build(source, EXAMPLES)
|
||
|
|
box = icon_box(Page(pdf, 1))
|
||
|
|
check.that(box, f"the icon of the {name} fixture did not reach the page")
|
||
|
|
if not box:
|
||
|
|
continue
|
||
|
|
# Three centimetres are 85.04 points and one is 28.35. The drawing fills
|
||
|
|
# its box, so a point and a half is the rasterizer.
|
||
|
|
check.that(abs(box[index] - asked) < 2,
|
||
|
|
f"the icon was asked for at {asked:.1f} points and came out "
|
||
|
|
f"at {box[index]:.1f}")
|
||
|
|
# The width follows the height in the proportions of the drawing: the icon
|
||
|
|
# of this company is a disc, so the two are the same number.
|
||
|
|
source = support.write("lockuphigh.tex", SIZES_HIGH)
|
||
|
|
pdf, _ = build(source, EXAMPLES)
|
||
|
|
box = icon_box(Page(pdf, 1))
|
||
|
|
if box:
|
||
|
|
check.that(abs(box[0] - box[1]) < 2,
|
||
|
|
f"the icon came out {box[0]:.1f} wide and {box[1]:.1f} high, "
|
||
|
|
"so the proportions of the drawing were not kept")
|
||
|
|
|
||
|
|
|
||
|
|
def source_checks(check):
|
||
|
|
for name in SUITE_FILES:
|
||
|
|
path = os.path.join(support.SUITE, name)
|
||
|
|
with open(path, encoding="utf-8") as handle:
|
||
|
|
text = handle.read()
|
||
|
|
found = COLOUR_VALUE.search(text)
|
||
|
|
check.that(found is None,
|
||
|
|
f"{name} carries a colour value: {found.group(0) if found else ''}")
|
||
|
|
ends = ENDINPUT.search(text)
|
||
|
|
if ends:
|
||
|
|
rest = text[ends.end():].strip()
|
||
|
|
check.that(rest == "",
|
||
|
|
f"{name} carries {len(rest.splitlines())} lines after "
|
||
|
|
"\\endinput, which LaTeX never reads")
|
||
|
|
if name in TEMPLATES:
|
||
|
|
own = OWN_VALUE.search(text)
|
||
|
|
check.that(own is None,
|
||
|
|
f"{name} sets a colour or a face of its own: "
|
||
|
|
f"{own.group(1) if own else ''} — it has to take both "
|
||
|
|
"from the identity it loads")
|
||
|
|
# Every role a template reads has to be declared, or the build fails with
|
||
|
|
# "undefined color" in a company that does not set it.
|
||
|
|
declared = set()
|
||
|
|
with open(os.path.join(support.SUITE, "business-identity.sty"),
|
||
|
|
encoding="utf-8") as handle:
|
||
|
|
identity = handle.read()
|
||
|
|
for name in re.findall(r"\\business@role\{([a-z-]+)\}", identity):
|
||
|
|
declared.add(name)
|
||
|
|
used = set()
|
||
|
|
for name in SUITE_FILES:
|
||
|
|
with open(os.path.join(support.SUITE, name), encoding="utf-8") as handle:
|
||
|
|
used |= set(re.findall(r"business-([a-z-]+)", handle.read()))
|
||
|
|
# The names of the packages themselves are no roles.
|
||
|
|
used -= {"identity", "suite", "brief", "beamer"}
|
||
|
|
missing = sorted(used - declared)
|
||
|
|
check.that(not missing, f"roles read but never declared: {missing}")
|
||
|
|
|
||
|
|
|
||
|
|
def page_checks(check):
|
||
|
|
source = support.write("identity-probe.tex", DOCUMENT)
|
||
|
|
pdf, _ = build(source, os.path.join(support.ROOT, "examples"))
|
||
|
|
page = Page(pdf, 1)
|
||
|
|
|
||
|
|
# The logo stands at the right edge of the type area, on the same edge as
|
||
|
|
# the text under it. Everything above the title belongs to the head.
|
||
|
|
whole = page.box()
|
||
|
|
check.that(whole is not None, "the page carries no ink at all")
|
||
|
|
left, top, right, bottom = whole
|
||
|
|
head = page.box(0, int(0.12 * page.height))
|
||
|
|
check.that(head is not None, "the head of the page is empty")
|
||
|
|
check.that(abs(head[2] - right) < 6,
|
||
|
|
f"the head does not end on the right edge of the text: "
|
||
|
|
f"{head[2]:.1f} against {right:.1f}")
|
||
|
|
|
||
|
|
# The page number stands at the foot, at the outer edge. Which rows the
|
||
|
|
# foot occupies is READ off the page and not guessed from a share of its
|
||
|
|
# height: the head and the foot follow the face of the company, and a share
|
||
|
|
# that holds for one company misses the foot of the next — measured when the
|
||
|
|
# example company changed its faces and the band that was searched came back
|
||
|
|
# empty.
|
||
|
|
rows = page.ink_rows()
|
||
|
|
last, current = [], []
|
||
|
|
for row in rows:
|
||
|
|
if current and row - current[-1] > 2:
|
||
|
|
last = current
|
||
|
|
current = []
|
||
|
|
current.append(row)
|
||
|
|
foot = page.box(current[0], current[-1] + 1) if current else None
|
||
|
|
check.that(foot is not None, "the foot carries no page number")
|
||
|
|
check.that(foot is not None and abs(foot[2] - right) < 6,
|
||
|
|
f"the page number is not at the right edge: "
|
||
|
|
f"{foot[2] if foot else 0:.1f} against {right:.1f}")
|
||
|
|
|
||
|
|
# What the PDF says about itself. A file whose properties say "untitled" is
|
||
|
|
# a file nobody finds again.
|
||
|
|
#
|
||
|
|
# It is read out of the XMP stream and not out of the info dictionary: that
|
||
|
|
# is where a file search, a reading program and an archive look today, and
|
||
|
|
# `hyperxmp` writes the author THERE and nowhere else — measured here, where
|
||
|
|
# `pdfinfo` prints a title and no author for a document that carries both.
|
||
|
|
xmp = support.run(["pdfinfo", "-meta", pdf]).stdout
|
||
|
|
check.that("<dc:title>" in xmp and "Lorem Ipsum" in xmp,
|
||
|
|
"the PDF carries no title")
|
||
|
|
check.that("<dc:creator>" in xmp and "Erika Muster" in xmp,
|
||
|
|
"the PDF carries no author")
|
||
|
|
|
||
|
|
# And the language, where the document says one. The template sets it
|
||
|
|
# nowhere: hyperref takes it from babel, and a second setting would only
|
||
|
|
# warn. That this really happens is measured and not assumed.
|
||
|
|
spoken = support.write("language-probe.tex", LANGUAGE)
|
||
|
|
speaking, _ = build(spoken, os.path.join(support.ROOT, "examples"))
|
||
|
|
said = support.run(["pdfinfo", "-meta", speaking]).stdout
|
||
|
|
check.that("dc:language" in said and "de" in said,
|
||
|
|
"a document that declares its language carries none in its XMP")
|
||
|
|
|
||
|
|
# The faces of the company, and nothing that LaTeX fell back to. Computer
|
||
|
|
# Modern is what arrives when a font has no shape in the encoding in force.
|
||
|
|
faces = support.fonts(pdf)
|
||
|
|
check.that(faces, "the PDF embeds no font at all")
|
||
|
|
fallbacks = [face for face in faces if face.startswith("CMR")
|
||
|
|
or face.startswith("CMSS") or face.startswith("SFRM")]
|
||
|
|
check.that(not fallbacks, f"LaTeX fell back to {fallbacks}")
|
||
|
|
# The marks come from the symbol family, because no text face carries them.
|
||
|
|
check.that(any("DejaVu" in face for face in faces),
|
||
|
|
f"the marks did not reach the page: {faces}")
|
||
|
|
|
||
|
|
|
||
|
|
def plain(check):
|
||
|
|
"""`plain` gives the identity and never the page of a document.
|
||
|
|
|
||
|
|
The two templates that ask for it are the letter class and the presentation
|
||
|
|
theme, and neither of them is an ordinary document: a page style and a
|
||
|
|
geometry laid over them wrecks their own layout. The case is measured with
|
||
|
|
the smallest document that can show it, because the failure is not a wrong
|
||
|
|
page — it is a run that stops: TeX skips a branch by counting `\\if` and
|
||
|
|
`\\fi` TOKENS, so a flag declared inside the skipped block closes the skip
|
||
|
|
with its own `\\fi` and the whole block runs. Reported on 2026-09-21 from a
|
||
|
|
fourth company, where the letter and the deck stopped building.
|
||
|
|
"""
|
||
|
|
source = os.path.join(support.ROOT, "tests", "documents", "plain.tex")
|
||
|
|
pdf, log = build(source, os.path.join(support.ROOT, "examples"))
|
||
|
|
check.that(os.path.exists(pdf), "a document with `plain` does not build")
|
||
|
|
with open(log, encoding="utf-8", errors="replace") as handle:
|
||
|
|
text = handle.read()
|
||
|
|
# Nothing of the page: no head, no foot, no geometry.
|
||
|
|
check.that("fancyhdr" not in text,
|
||
|
|
"`plain` loads fancyhdr, so it carries the page of a document")
|
||
|
|
check.that("geometry.sty" not in text,
|
||
|
|
"`plain` loads geometry, so it lays out the page again")
|
||
|
|
check.that("longtable" not in text,
|
||
|
|
"`plain` loads the table machinery of the document template")
|
||
|
|
|
||
|
|
|
||
|
|
def logo_from_file(check):
|
||
|
|
"""The other way a company carries its logo: a file instead of a drawing.
|
||
|
|
|
||
|
|
Two heights in one document, and the second asks for twice the first. A
|
||
|
|
height arrives as whatever the document writes — `1cm`, `2em`, `40pt` — and
|
||
|
|
a computation that reads the number and drops the unit passes at one of
|
||
|
|
them and fails at the other: measured in a sister package, where the gap
|
||
|
|
under the logo collapsed at every height given in centimetres while the
|
||
|
|
same formula looked right in em.
|
||
|
|
"""
|
||
|
|
support.write("filecompany.sty", FILE_COMPANY)
|
||
|
|
source = support.write("logofile.tex", FILE_DOCUMENT)
|
||
|
|
pdf, _ = build(source, support.BUILD)
|
||
|
|
page = Page(pdf, 1)
|
||
|
|
rows = page.ink_rows()
|
||
|
|
check.that(rows, "the logo file did not reach the page at all")
|
||
|
|
if not rows:
|
||
|
|
return
|
||
|
|
blocks, current = [], []
|
||
|
|
for row in rows:
|
||
|
|
if current and row - current[-1] > 2:
|
||
|
|
blocks.append(current)
|
||
|
|
current = []
|
||
|
|
current.append(row)
|
||
|
|
blocks.append(current)
|
||
|
|
check.that(len(blocks) == 2,
|
||
|
|
f"the page should carry two logos, it carries {len(blocks)}")
|
||
|
|
if len(blocks) != 2:
|
||
|
|
return
|
||
|
|
small = blocks[0][-1] - blocks[0][0] + 1
|
||
|
|
large = blocks[1][-1] - blocks[1][0] + 1
|
||
|
|
ratio = large / small
|
||
|
|
check.that(1.9 < ratio < 2.1,
|
||
|
|
f"the logo asked for at twice the height came out {ratio:.2f} "
|
||
|
|
"times as high")
|
||
|
|
|
||
|
|
|
||
|
|
def tagline(check):
|
||
|
|
"""The lockup of two lines: the name, and the company's sentence under it.
|
||
|
|
|
||
|
|
Four things are measured, and each of them is a decision the family took.
|
||
|
|
The head of the first page carries three lines — name, tagline, address —
|
||
|
|
and the head of the second two, because the address belongs on the sheet
|
||
|
|
that leaves the company and the tagline belongs to the mark. The logo in a
|
||
|
|
SENTENCE stays one line, because a place asks for the tagline and a company
|
||
|
|
only says what it is; without that division a mark in running text would
|
||
|
|
open the line it stands in. And the page reserves the room for the taller
|
||
|
|
block: the head is measured when the identity arrives, and a template that
|
||
|
|
measured it before the company declared itself asks fancyhdr for the
|
||
|
|
difference on every page — 16.8 points, measured on 2026-09-22.
|
||
|
|
"""
|
||
|
|
source = support.write("taglinedoc.tex", TAGLINE_QUIET)
|
||
|
|
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,
|
||
|
|
"the page kept less room for the head than the two-line lockup "
|
||
|
|
"takes, so fancyhdr asked for the difference")
|
||
|
|
for number, carries in ((1, True), (2, False)):
|
||
|
|
found = support.words(pdf, number)
|
||
|
|
head = [word for word in found if word[4] < 90]
|
||
|
|
spoken = " ".join(word[0] for word in head)
|
||
|
|
check.that("Vernunft" in spoken,
|
||
|
|
f"the tagline is not in the head of page {number}: "
|
||
|
|
f"{spoken}")
|
||
|
|
check.that(("nordwind.example" in spoken) == carries,
|
||
|
|
f"the address line is {'missing from' if carries else 'in'} "
|
||
|
|
f"the head of page {number}, and it belongs on the first "
|
||
|
|
"page alone")
|
||
|
|
name = [word for word in head if word[0] == "Nordwind"]
|
||
|
|
tag = [word for word in head if word[0] == "Vernunft"]
|
||
|
|
if not name or not tag:
|
||
|
|
continue
|
||
|
|
name, tag = name[0], tag[0]
|
||
|
|
# The two lines stand flush left with each other, and the second is the
|
||
|
|
# smaller: the family sets the tagline at a share of the name.
|
||
|
|
check.that(abs(name[1] - tag[1]) < 1,
|
||
|
|
f"the name begins at {name[1]:.1f} points and the tagline "
|
||
|
|
f"at {tag[1]:.1f}: the two lines of the block share their "
|
||
|
|
"left edge")
|
||
|
|
check.that(tag[2] > name[4] - 1,
|
||
|
|
"the tagline does not stand under the name")
|
||
|
|
high, low = name[4] - name[2], tag[4] - tag[2]
|
||
|
|
check.that(low < high,
|
||
|
|
f"the tagline is {low:.1f} points high and the name "
|
||
|
|
f"{high:.1f}: the second line of the lockup is the smaller "
|
||
|
|
"one")
|
||
|
|
# A logo in a SENTENCE carries no tagline: the name stands in the line of
|
||
|
|
# the body text, and the tagline of the company is nowhere near it.
|
||
|
|
body = [word for word in support.words(pdf, 1) if word[2] > 200]
|
||
|
|
inline = [word for word in body if word[0] == "Nordwind"]
|
||
|
|
check.that(inline, "the logo in the sentence did not reach the page")
|
||
|
|
if inline:
|
||
|
|
near = [word for word in body
|
||
|
|
if word[0] == "Vernunft"
|
||
|
|
and abs(word[2] - inline[0][2]) < 30]
|
||
|
|
check.that(not near,
|
||
|
|
"the logo in a sentence carries the tagline, so a mark in "
|
||
|
|
"running text opens the line it stands in")
|
||
|
|
|
||
|
|
|
||
|
|
def darkest(page, word):
|
||
|
|
"""The darkest pixel inside the box a word stands in, 0 to 255.
|
||
|
|
|
||
|
|
The darkest pixel is the core of a stem, and that is the colour the type is
|
||
|
|
set in: the mean over a word measures how much white stands between its
|
||
|
|
letters, which is a property of the letters and of the size, not of the
|
||
|
|
colour.
|
||
|
|
"""
|
||
|
|
scale = page.dpi / 72.0
|
||
|
|
left, top = int(word[1] * scale), int(word[2] * scale)
|
||
|
|
right, low = int(word[3] * scale) + 1, int(word[4] * scale) + 1
|
||
|
|
return min(page.pixels[y * page.width + x]
|
||
|
|
for y in range(max(top, 0), min(low, page.height))
|
||
|
|
for x in range(max(left, 0), min(right, page.width)))
|
||
|
|
|
||
|
|
|
||
|
|
def tagline_colour(check):
|
||
|
|
"""A company that names no colour for its second line sets none.
|
||
|
|
|
||
|
|
The tagline then stands in the colour the name over it stands in, because
|
||
|
|
the lockup sets its colour once and no line of the block sets one of its
|
||
|
|
own. Named, the colour reaches that line alone and the name keeps the one
|
||
|
|
of the lockup. Measured at the darkest pixel of each word: the quiet role
|
||
|
|
of the example company is a 55 percent tint of the brand colour, so the two
|
||
|
|
lines come out far apart where it is named and together where it is not.
|
||
|
|
"""
|
||
|
|
tone = {}
|
||
|
|
for name, fixture in (("quiet", TAGLINE_QUIET), ("plain", TAGLINE_PLAIN)):
|
||
|
|
source = support.write("tagcolour%s.tex" % name, fixture)
|
||
|
|
pdf, _ = build(source, EXAMPLES)
|
||
|
|
page = Page(pdf, 2)
|
||
|
|
found = {word[0]: word for word in support.words(pdf, 2)
|
||
|
|
if word[4] < 90}
|
||
|
|
check.that("Nordwind" in found and "Vernunft" in found,
|
||
|
|
f"the head of the {name} company carries no lockup")
|
||
|
|
if "Nordwind" not in found or "Vernunft" not in found:
|
||
|
|
return
|
||
|
|
tone[name] = (darkest(page, found["Nordwind"]),
|
||
|
|
darkest(page, found["Vernunft"]))
|
||
|
|
check.that(abs(tone["plain"][0] - tone["plain"][1]) < 12,
|
||
|
|
f"without a colour of its own the tagline comes out at "
|
||
|
|
f"{tone['plain'][1]} against the {tone['plain'][0]} of the name: "
|
||
|
|
"a colour was set for that line")
|
||
|
|
check.that(tone["quiet"][1] - tone["quiet"][0] > 30,
|
||
|
|
f"the named colour reached the tagline at {tone['quiet'][1]} "
|
||
|
|
f"against the {tone['quiet'][0]} of the name, and the quiet role "
|
||
|
|
"of the company is far lighter than its brand colour")
|
||
|
|
check.that(abs(tone["quiet"][0] - tone["plain"][0]) < 12,
|
||
|
|
"a colour named for the tagline changed the colour of the name")
|
||
|
|
|
||
|
|
|
||
|
|
def main():
|
||
|
|
check = Check("identity")
|
||
|
|
source_checks(check)
|
||
|
|
plain(check)
|
||
|
|
page_checks(check)
|
||
|
|
logo_from_file(check)
|
||
|
|
tagline(check)
|
||
|
|
tagline_colour(check)
|
||
|
|
lockup_sizes(check)
|
||
|
|
return check.done()
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
sys.exit(main())
|