Skip to main content
Recipe number 172

Cookbook · Chapter 12 · Textbooks

A Japanese drill with the answers upside down

Two pages of a Japanese drill series, arithmetic across and kanji in columns, each exercise followed by its answers printed upside down.

On this page
Output
Canvas · PDF
Postext
Tested with Postext 1.59.0
Needs ≥ 1.59.0 · postext-pdf ≥ 1.59.0
Licence
Updated 24 Oct 2026
Code MIT · Text CC BY 4.0
  • Foreign-language sample: no English edition yet
  • Trim 182 × 257 mm
  • 1 column
  • Klee One 12/22
  • Zen Maru Gothic
  • 4 pages
  • Level
  • Postext 1.59.0
  • Laid out in 105 ms
  • 137 lines of code

In short

A Japanese school drill book: sums, number boxes, choices and true-or-false on one page, kanji and readings on another, set top to bottom. Under every exercise its answers are printed upside down, so a child turns the book to check.

What you'll build

Two pages from a third-year drill series on B5. The arithmetic page runs across: a pale blue band with the subject badge, the title and a tanuki mascot, a name line and a score box, then ひっ算 with a box for each digit of the answer, number boxes after six sums, ア イ ウ choices in a row, ○× statements with a bracket at the end of the line and two word problems with lines to write the working on. The kanji page is set the way a 国語 drill is printed, top to bottom and right to left: readings with a box for the kanji, lined words whose reading goes in brackets, choices to ring and a meaning to pick. Under every exercise, its answers are printed upside down in small grey type. A child sees where they are, cannot read them at a glance, and turns the book round to check.

This recipe answers

  • How do I print the answers upside down at the foot of an exercise?
  • How do I set a Japanese drill page in vertical columns with the same exercise design as the horizontal one?
  • How do I set long division the way each country's schools write it?

The short answer

script.js · lines 35–55in full code
// `placement: 'inverted'` sets each exercise's key right after it in a box turned 180°: the
// pupil sees where the answers are without reading them, and turns the book to check. The
// key's words come from the exercise (the options' katakana, ○ and ×, the operations), so
// the Markdown holds nothing twice.
const drill = {
  id: 'drill',
  number: { shape: 'square', fontFamily: TEXT, fontWeight: 600, background: col('blue') },
  instruction: { fontFamily: ROUND, bold: false },
  options: { layout: 'row' }, // ア イ ウ side by side (the default labels in Japanese)
  trueFalse: { look: 'bracket' }, // ( ) for ○ or ×
  blank: { look: 'box', width: 'answer', color: col('rule') }, // □ to write in
  area: { kind: 'lines', color: col('rule') },
  grid: { rule: col('rule') },
  solutions: { placement: 'inverted', box: 'kotae' },
  marginTop: em(1.1), marginBottom: em(0.4),
};
const kotae = { id: 'kotae', title: '', backgroundEnabled: false, border: { enabled: false },
  padding: { top: pt(0), right: pt(0), bottom: pt(0), left: pt(0) },
  body: { fontFamily: ROUND, fontSize: pt(8), lineHeight: pt(12), color: col('muted'),
    firstLineIndent: pt(0) },
  marginTop: mm(1), marginBottom: mm(2), keepTogether: true };

The answers printed upside down under each exercise

Ingredients

Type
Klee One, Zen Maru Gothic (SIL OFL 1.1)
Assets
  • kitsune-600.jpg
  • tanuki-600.jpg
  • A young tanuki in a yellow school hat holding a big pencil. (Generated With Diffusion Models, original)
  • A fox cub in a red scarf reading a book. (Generated With Diffusion Models, original)

Method

#1 · Turn each exercise's key upside down

The code is the short answer above. placement: 'inverted' sets the key after each exercise in a box turned 180°, and the box goes inside the exercise's own box, so a page break never parts the two. The key is made from the exercise: the operations with their results, the katakana of the right option, ○ and ×, the words of the gaps. The answers box style, kotae, only sets the type small and grey.

#2 · One band for a page across and a page down

script.js · lines 59–76in full code
// A heading design on a vertical page is laid along its column: x runs down the page and y
// to the left, so the same band stands at the right edge, from head to foot.
const BAND = 30; // mm: the band's depth (its height across, its width down)
const header = { enabled: true, minHeight: mm(BAND + 3), slot: { elements: [
  { kind: 'box', id: 'band', reserve: false, style: { backgroundColor: col('tint'),
    borderRadius: mm(4) }, placement: { anchor: { to: 'container', edge: 'top-left' },
    size: { width: 'fill', height: mm(BAND) } } },
  { kind: 'image', id: 'mascot', resourceId: '{attr.pic}', decorative: true, reserve: false,
    placement: { anchor: { to: 'container', edge: 'top-right' }, offset: { x: mm(-4), y: mm(1) },
      size: { width: mm(28), height: mm(28) } } },
  { kind: 'text', id: 'subject', content: '{attr.kicker}', fontFamily: ROUND, fontSize: pt(10),
    fontWeight: 700, color: col('ink'), box: { backgroundColor: col('sun'), borderRadius: mm(3),
      padding: { top: mm(0.6), bottom: mm(0.6), left: mm(2.4), right: mm(2.4) } },
    placement: { anchor: { to: 'container', edge: 'top-left' }, offset: { x: mm(5), y: mm(5) } } },
  { kind: 'text', id: 'title', content: '{titleText}', fontFamily: ROUND, fontSize: pt(20),
    fontWeight: 700, color: col('ink'),
    placement: { anchor: { to: '#subject', edge: 'below' }, offset: { x: mm(0), y: mm(2.5) } } },
] } };

A heading design on a vertical page is laid along its column: x runs down the page and y runs to the left. The band anchored top-left with the full width stands at the top of the arithmetic page and down the right edge of the kanji page; the mascot anchored top-right sits at the band's far end on both. Nothing in the design names a direction.

#3 · Two drills from one text

script.js · lines 271–279in full code
// Two drill books of one series: arithmetic across, bound on the left; kanji in columns,
// bound on the right (vertical text binds there by itself).
const [sansu, kokugo] = markdown.split(/(?=^# 春のことば)/m);
const build = (text) => withLoadedFonts(() => buildDocument({ markdown: text, resources },
  config()), { ...kitFonts(FONTS), text: markdown });
const across = await build(sansu);
// 国語 is set in columns, top to bottom, right to left: the same design, turned.
writingMode = 'vertical-rl';
const docs = [across, await build(kokugo)];

The configuration is a function, so each build gets its own object, and only writingMode changes between the two. Set vertically, the 国語 drill binds on the right by itself; showBook lays each drill out on its own side of the spine. The text is set in Klee One, a textbook face, and the labels in Zen Maru Gothic; both load their vertical punctuation for the brackets of the kanji page.

The whole recipe

Sandbox
// ═══ Postext Cookbook · Nº 172 · A Japanese drill with the answers upside down ═══
// https://postext.dev/en/cookbook/japanese-drill-upside-down-answers
// Code: MIT · Text: original (CC BY 4.0) · Pictures: generated with diffusion models
// Fonts: Klee One, Zen Maru Gothic (SIL OFL 1.1) · Needs postext ≥ 1.59.0
import {
  buildDocument, withLoadedFonts, renderPageToCanvas, registerResourceImage,
  loadVerticalAlternates,
} from 'https://esm.sh/postext';
import { renderToPdf, decompressWoff2 } from 'https://esm.sh/postext-pdf';

const LANG = 'ja'; // @lang: the language of the frame; the drill is Japanese in all
const RECIPE = 'japanese-drill-upside-down-answers';

// ─── 1 · Design ─────────────────────────────────────────────────────────────
// #region palette: a drill book's blue, a warm yellow and a pale sky for the band
const palette = {
  ink: '#23272c', // text
  blue: '#2a6cb0', // the accent: exercise numbers, the title band's rule, the score box
  sun: '#f2c14e', // the subject badge
  tint: '#eaf2fb', // the title band
  rule: '#a8bfd8', // writing lines and boxes
  muted: '#69717b', // folios, the name line, the upside-down answers
  paper: '#ffffff',
};
const col = (id) => ({ hex: palette[id], model: 'hex', paletteId: id });
const colorPalette = [
  ...Object.entries(palette).map(([id, hex]) => ({ id, name: id, value: { hex, model: 'hex' } })),
  { id: 'main-color', name: 'accent (defaults)', value: { hex: palette.blue, model: 'hex' } },
];
// #endregion
// 教科書体 for the text a child copies from, a round gothic for the labels.
const [TEXT, ROUND] = ['Klee One', 'Zen Maru Gothic'];

// #region answer: the answers printed upside down under each exercise
// `placement: 'inverted'` sets each exercise's key right after it in a box turned 180°: the
// pupil sees where the answers are without reading them, and turns the book to check. The
// key's words come from the exercise (the options' katakana, ○ and ×, the operations), so
// the Markdown holds nothing twice.
const drill = {
  id: 'drill',
  number: { shape: 'square', fontFamily: TEXT, fontWeight: 600, background: col('blue') },
  instruction: { fontFamily: ROUND, bold: false },
  options: { layout: 'row' }, // ア イ ウ side by side (the default labels in Japanese)
  trueFalse: { look: 'bracket' }, // ( ) for ○ or ×
  blank: { look: 'box', width: 'answer', color: col('rule') }, // □ to write in
  area: { kind: 'lines', color: col('rule') },
  grid: { rule: col('rule') },
  solutions: { placement: 'inverted', box: 'kotae' },

The composed script.js runs as it is: paste it into any page’s module script, or open the recipe on CodePen. Recipe folder on GitHub ↗ (opens in a new tab)

Variations

#Collect the answers at the end

A drill printed with a separate answer booklet keeps the key out of the pages: chapterEnd lists it after the last exercise.

-  solutions: { placement: 'inverted', box: 'kotae' },
+  solutions: { placement: 'chapterEnd', box: 'kotae' },

The puzzle page turns its answers the same way, on cards in two columns.

Pitfalls

Pitfall

Chinese faces load by slices, through the cjk block

Fontsource serves a Chinese, Japanese or Korean family as about a hundred files per weight, each covering a range of characters. loadFonts fetches only the latin file, so on screen the Han characters come from a system face and measure wrong, and fontsourceProvider hands the PDF that latin file, which prints them as empty boxes. List the cjk kit block, call loadCjkFonts(FONTS, markdown) after loadFonts (once per voice, with the text it sets, when the book uses several CJK faces) and give renderToPdf fontProvider: cjkPdfProvider: both take the files that hold the text's characters. Chinese, Japanese and Korean fonts →

Pitfall

Tag a Japanese text 'ja', never zh-Hans or LANG

A recipe's editions are en and es, but a Japanese sample is Japanese in both: `locale: LANG` would tag it English or Spanish, and a Chinese tag would set it by the Chinese rules (Kaiming punctuation, small kana free to start a line, 图 for 図, Chinese glyph forms in the PDF). From postext 1.30 the tag is also the language the text is shaped in (measured, painted and embedded), so any tag but 'ja' gives other glyph forms than the Japanese ones (the Chinese ones under a Chinese tag) and other line breaks. Write 'ja': it picks the Japan region (JLReq line breaking and punctuation, sesame emphasis marks, furigana spacing, 図 and 表 labels) and turns hyphenation off. The lint fails a text with kana under a zh or ko tag. Japanese line breaking (kinsoku) →

  • A reading cannot stand over a blank yet: a ruby inside [[…]] is part of the hidden answer. The 漢字の書き取り exercise sets each reading on its own line and the box after it, three to a row in a :::columns group; on the vertical page the reading stands beside the box, as furigana do.
  • Klee One ships 400 and 600 only: boldFontWeight: 600 keeps every bold run in a cut that exists, the item numbers and the exercise numbers included.

Credits

Text
Original prose, CC BY 4.0
Images
  • A young tanuki in a yellow school hat holding a big pencil. · Generated With Diffusion Models · original
  • A fox cub in a red scarf reading a book. · Generated With Diffusion Models · original
Fonts
Klee One (SIL OFL 1.1) · Zen Maru Gothic (SIL OFL 1.1)
Sandbox