Prepare a colorful handbook pdf for 3rd grade ESL student on following 2 topics -"Possessive case When we want to say who owns something, we add( 's )to the person or animal's name: For example This is Mark's pen./This pen is Mark's. prepare 15 conversation questions( logical and not funny) based on this topic with Russian translation under every word of a sentence along with it's answers.Also mention the topic title at top with Russian translation. Also prepare a next handbook in similar way for topic " Articles" for 3rd grade ESL student with 15 conversation questions ( logical and not funny) with Russian translation under every word of sentences along with it's answers.Dont forget to add Russian translation under every sentence and also mentione title at top
ls -la /tmp/workspace/ 2>/dev/null || mkdir -p /tmp/workspace && echo "workspace ready"
mkdir -p /tmp/workspace/esl-handbooks && python3 -c " from reportlab.lib.pagesizes import A4 from reportlab.lib import colors print('reportlab ok') from reportlab.pdfgen import canvas from reportlab.lib.units import mm from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.lib.enums import TA_CENTER, TA_LEFT print('all imports ok') "
~/esl-handbooks/make_handbooks.py
# ESL 3rd Grade Handbooks - Possessive Case & Articles
# Colorful PDF with Russian word-by-word translations
from reportlab.lib.pagesizes import A4
from reportlab.lib import colors
from reportlab.lib.units import mm
from reportlab.pdfgen import canvas
from reportlab.platypus import (
SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle,
HRFlowable, KeepTogether
)
from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle
from reportlab.lib.enums import TA_CENTER, TA_LEFT, TA_JUSTIFY
from reportlab.lib.colors import HexColor
import os
OUT_DIR = "/tmp/workspace/esl-handbooks"
# ─── Color palette ────────────────────────────────────────────────────────────
C_PURPLE = HexColor("#6A0DAD")
C_BLUE = HexColor("#1565C0")
C_TEAL = HexColor("#00796B")
C_ORANGE = HexColor("#E65100")
C_PINK = HexColor("#C2185B")
C_GREEN = HexColor("#2E7D32")
C_YELLOW_BG = HexColor("#FFFDE7")
C_LAVENDER = HexColor("#EDE7F6")
C_MINTBG = HexColor("#E8F5E9")
C_PINKBG = HexColor("#FCE4EC")
C_BLUEBG = HexColor("#E3F2FD")
C_ORANGEBG = HexColor("#FFF3E0")
C_LIGHT_GREY = HexColor("#F5F5F5")
C_DARK_GREY = HexColor("#424242")
C_RED = HexColor("#B71C1C")
C_ANSWER_BG = HexColor("#E8EAF6")
PAGE_W, PAGE_H = A4
# ─── Helper: draw decorative header banner ────────────────────────────────────
def draw_banner(c, title_en, title_ru, banner_color):
"""Draw a full-width colored top banner with title."""
c.setFillColor(banner_color)
c.rect(0, PAGE_H - 80*mm, PAGE_W, 80*mm, fill=1, stroke=0)
# Stars / decorations
c.setFillColor(colors.white)
for sx, sy in [(20, PAGE_H-10), (PAGE_W-20, PAGE_H-10),
(PAGE_W/2, PAGE_H-8), (50, PAGE_H-30), (PAGE_W-50, PAGE_H-30)]:
c.setFont("Helvetica-Bold", 14)
c.drawCentredString(sx, sy, "★")
# English title
c.setFont("Helvetica-Bold", 26)
c.setFillColor(colors.white)
c.drawCentredString(PAGE_W/2, PAGE_H - 40*mm, title_en)
# Russian translation
c.setFont("Helvetica", 14)
c.setFillColor(HexColor("#FFEB3B"))
c.drawCentredString(PAGE_W/2, PAGE_H - 52*mm, title_ru)
# Grade label
c.setFont("Helvetica-Bold", 11)
c.setFillColor(colors.white)
c.drawCentredString(PAGE_W/2, PAGE_H - 62*mm, "3rd Grade ESL • 3-й класс ЭСЛ")
def draw_page_bg(c, page_num):
"""Alternate soft background colors."""
bg_colors = [C_YELLOW_BG, C_LAVENDER, C_MINTBG, C_PINKBG, C_BLUEBG, C_ORANGEBG]
c.setFillColor(bg_colors[page_num % len(bg_colors)])
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
def draw_footer(c, page_num):
c.setFont("Helvetica", 8)
c.setFillColor(C_DARK_GREY)
c.drawCentredString(PAGE_W/2, 10*mm, f"Page {page_num} • ESL 3rd Grade Handbook")
# ─── Word-by-word translation builder ─────────────────────────────────────────
# Returns a multi-line string: English word / Russian word stacked
def word_by_word(en_sentence, ru_words_list):
"""
en_sentence: "This is Mark's pen."
ru_words_list: ["Это","является","ручкой","Марка."]
Returns HTML-style Paragraph markup for stacked word pairs.
"""
en_words = en_sentence.split()
pairs = []
for i, ew in enumerate(en_words):
rw = ru_words_list[i] if i < len(ru_words_list) else ""
pairs.append(f'<b>{ew}</b><br/><font color="#C2185B" size="7.5">{rw}</font>')
return " ".join(pairs)
# ═══════════════════════════════════════════════════════════════════════════════
# HANDBOOK 1 — POSSESSIVE CASE
# ═══════════════════════════════════════════════════════════════════════════════
POSS_QA = [
# (question_en, [ru_words_Q], answer_en, [ru_words_A])
(
"Whose pen is this?",
["Чья", "ручка", "это?"],
"This is Mark's pen.",
["Это", "ручка", "Марка."]
),
(
"Whose book is on the table?",
["Чья", "книга", "на", "столе?"],
"Anna's book is on the table.",
["Книга", "Анны", "на", "столе."]
),
(
"Whose dog is playing outside?",
["Чья", "собака", "играет", "снаружи?"],
"Tom's dog is playing outside.",
["Собака", "Тома", "играет", "снаружи."]
),
(
"Whose backpack is by the door?",
["Чей", "рюкзак", "у", "двери?"],
"Sam's backpack is by the door.",
["Рюкзак", "Сэма", "у", "двери."]
),
(
"Whose bicycle is in the yard?",
["Чей", "велосипед", "во", "дворе?"],
"Kate's bicycle is in the yard.",
["Велосипед", "Кейт", "во", "дворе."]
),
(
"Whose jacket is on the chair?",
["Чья", "куртка", "на", "стуле?"],
"Lisa's jacket is on the chair.",
["Куртка", "Лизы", "на", "стуле."]
),
(
"Whose cat is sleeping on the sofa?",
["Чья", "кошка", "спит", "на", "диване?"],
"Mia's cat is sleeping on the sofa.",
["Кошка", "Мии", "спит", "на", "диване."]
),
(
"Whose lunch is in the bag?",
["Чей", "обед", "в", "сумке?"],
"Peter's lunch is in the bag.",
["Обед", "Питера", "в", "сумке."]
),
(
"Whose ball is under the bench?",
["Чей", "мяч", "под", "скамейкой?"],
"Leo's ball is under the bench.",
["Мяч", "Лео", "под", "скамейкой."]
),
(
"Whose notebook has a red cover?",
["Чья", "тетрадь", "имеет", "красную", "обложку?"],
"Emma's notebook has a red cover.",
["Тетрадь", "Эммы", "имеет", "красную", "обложку."]
),
(
"Whose shoes are near the entrance?",
["Чьи", "ботинки", "у", "входа?"],
"Nick's shoes are near the entrance.",
["Ботинки", "Ника", "у", "входа."]
),
(
"Whose drawing is on the wall?",
["Чей", "рисунок", "на", "стене?"],
"Lily's drawing is on the wall.",
["Рисунок", "Лили", "на", "стене."]
),
(
"Whose water bottle is on the desk?",
["Чья", "бутылка", "воды", "на", "столе?"],
"Chris's water bottle is on the desk.",
["Бутылка", "воды", "Криса", "на", "столе."]
),
(
"Whose horse is in the field?",
["Чья", "лошадь", "на", "поле?"],
"The farmer's horse is in the field.",
["Лошадь", "фермера", "на", "поле."]
),
(
"Whose coat is the warmest in class?",
["Чьё", "пальто", "самое", "тёплое", "в", "классе?"],
"Sophia's coat is the warmest in class.",
["Пальто", "Софии", "самое", "тёплое", "в", "классе."]
),
]
# ═══════════════════════════════════════════════════════════════════════════════
# HANDBOOK 2 — ARTICLES (a / an / the)
# ═══════════════════════════════════════════════════════════════════════════════
ART_QA = [
(
"Do you have a pencil?",
["У", "тебя", "есть", "карандаш?"],
"Yes, I have a pencil.",
["Да,", "у", "меня", "есть", "карандаш."]
),
(
"Is there an elephant at the zoo?",
["Есть", "ли", "слон", "в", "зоопарке?"],
"Yes, there is an elephant at the zoo.",
["Да,", "в", "зоопарке", "есть", "слон."]
),
(
"Can you open the window, please?",
["Ты", "можешь", "открыть", "окно,", "пожалуйста?"],
"Sure, I can open the window.",
["Конечно,", "я", "могу", "открыть", "окно."]
),
(
"Is this a dog or a cat?",
["Это", "собака", "или", "кошка?"],
"This is a dog.",
["Это", "собака."]
),
(
"Where is the teacher's desk?",
["Где", "стол", "учителя?"],
"The teacher's desk is at the front of the room.",
["Стол", "учителя", "стоит", "в", "начале", "класса."]
),
(
"Do you want an apple or an orange?",
["Ты", "хочешь", "яблоко", "или", "апельсин?"],
"I want an apple.",
["Я", "хочу", "яблоко."]
),
(
"Is the sun in the sky today?",
["Солнце", "сегодня", "на", "небе?"],
"Yes, the sun is in the sky today.",
["Да,", "сегодня", "солнце", "на", "небе."]
),
(
"Did you see a bird in the garden?",
["Ты", "видел", "птицу", "в", "саду?"],
"Yes, I saw a bird in the garden.",
["Да,", "я", "видел", "птицу", "в", "саду."]
),
(
"Is there an umbrella near the door?",
["Есть", "ли", "зонтик", "у", "двери?"],
"Yes, there is an umbrella near the door.",
["Да,", "у", "двери", "есть", "зонтик."]
),
(
"What is on the shelf?",
["Что", "стоит", "на", "полке?"],
"There is a book on the shelf.",
["На", "полке", "есть", "книга."]
),
(
"Is this an easy question?",
["Это", "лёгкий", "вопрос?"],
"Yes, this is an easy question.",
["Да,", "это", "лёгкий", "вопрос."]
),
(
"Where is the moon at night?",
["Где", "луна", "ночью?"],
"The moon is in the sky at night.",
["Луна", "ночью", "на", "небе."]
),
(
"Do you have a pet at home?",
["У", "тебя", "есть", "домашнее", "животное", "дома?"],
"Yes, I have a cat at home.",
["Да,", "у", "меня", "дома", "есть", "кошка."]
),
(
"Can you pass me the eraser?",
["Ты", "можешь", "передать", "мне", "ластик?"],
"Here you go, here is the eraser.",
["Вот,", "держи,", "вот", "ластик."]
),
(
"Is a whale a big animal?",
["Кит", "большое", "животное?"],
"Yes, a whale is a very big animal.",
["Да,", "кит", "очень", "большое", "животное."]
),
]
# ─── PDF Builder ──────────────────────────────────────────────────────────────
def build_handbook(filename, title_en, title_ru, subtitle_en, subtitle_ru,
intro_lines, # list of (en_sentence, [ru_words])
qa_list,
banner_color, q_color, a_color, q_bg, a_bg):
"""Build one handbook PDF."""
from reportlab.pdfgen import canvas as pdfc
from reportlab.lib.pagesizes import A4
c = pdfc.Canvas(filename, pagesize=A4)
page_num = 1
# ── PAGE 1: Cover / Intro ─────────────────────────────────────────────────
draw_page_bg(c, 0)
draw_banner(c, title_en, title_ru, banner_color)
# Subtitle
y = PAGE_H - 90*mm
c.setFont("Helvetica-Bold", 13)
c.setFillColor(C_PURPLE)
c.drawCentredString(PAGE_W/2, y, subtitle_en)
y -= 8*mm
c.setFont("Helvetica", 11)
c.setFillColor(C_TEAL)
c.drawCentredString(PAGE_W/2, y, subtitle_ru)
# Intro rule box
y -= 10*mm
box_x = 20*mm
box_w = PAGE_W - 40*mm
box_h = len(intro_lines) * 18*mm + 10*mm
c.setFillColor(HexColor("#FFF9C4"))
c.setStrokeColor(banner_color)
c.setLineWidth(2)
c.roundRect(box_x, y - box_h, box_w, box_h, 5*mm, fill=1, stroke=1)
y -= 6*mm
for en_sent, ru_words in intro_lines:
# English
c.setFont("Helvetica-Bold", 12)
c.setFillColor(C_BLUE)
c.drawString(box_x + 5*mm, y, en_sent)
y -= 6*mm
# Russian word-by-word
en_ws = en_sent.split()
x_cur = box_x + 5*mm
for i, ew in enumerate(en_ws):
rw = ru_words[i] if i < len(ru_words) else ""
c.setFont("Helvetica-Bold", 9)
c.setFillColor(C_DARK_GREY)
c.drawString(x_cur, y, ew)
c.setFont("Helvetica", 7.5)
c.setFillColor(C_PINK)
c.drawString(x_cur, y - 5*mm, rw)
x_cur += max(c.stringWidth(ew, "Helvetica-Bold", 9),
c.stringWidth(rw, "Helvetica", 7.5)) + 4*mm
y -= 12*mm
draw_footer(c, page_num)
c.showPage()
page_num += 1
# ── Q&A PAGES ─────────────────────────────────────────────────────────────
MARGIN_L = 15*mm
MARGIN_R = 15*mm
CONTENT_W = PAGE_W - MARGIN_L - MARGIN_R
Y_TOP = PAGE_H - 25*mm
Y_BOTTOM = 18*mm
q_per_page = 3
q_idx = 0
for page_start in range(0, len(qa_list), q_per_page):
batch = qa_list[page_start:page_start + q_per_page]
draw_page_bg(c, page_num)
# Page header bar
c.setFillColor(banner_color)
c.rect(0, PAGE_H - 18*mm, PAGE_W, 18*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 12)
c.setFillColor(colors.white)
c.drawCentredString(PAGE_W/2, PAGE_H - 12*mm, f"Conversation Questions • Вопросы для разговора")
y = Y_TOP - 2*mm
for (q_en, q_ru, a_en, a_ru) in batch:
q_idx += 1
block_h = 68*mm # estimated block height
# ── Question box ──────────────────────────────────────────────────
c.setFillColor(q_bg)
c.setStrokeColor(q_color)
c.setLineWidth(1.5)
c.roundRect(MARGIN_L, y - 28*mm, CONTENT_W, 28*mm, 3*mm, fill=1, stroke=1)
# Q label badge
c.setFillColor(q_color)
c.roundRect(MARGIN_L, y - 7*mm, 14*mm, 7*mm, 2*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(colors.white)
c.drawString(MARGIN_L + 2*mm, y - 5.5*mm, f"Q{q_idx}")
# English question
c.setFont("Helvetica-Bold", 11)
c.setFillColor(q_color)
c.drawString(MARGIN_L + 17*mm, y - 5.5*mm, q_en)
# Russian translation under each word
q_en_ws = q_en.split()
x_cur = MARGIN_L + 17*mm
for i, ew in enumerate(q_en_ws):
rw = q_ru[i] if i < len(q_ru) else ""
c.setFont("Helvetica-Bold", 9)
c.setFillColor(C_DARK_GREY)
c.drawString(x_cur, y - 11*mm, ew)
c.setFont("Helvetica", 7.5)
c.setFillColor(C_PINK)
c.drawString(x_cur, y - 16*mm, rw)
w_en = c.stringWidth(ew, "Helvetica-Bold", 9)
w_ru = c.stringWidth(rw, "Helvetica", 7.5)
x_cur += max(w_en, w_ru) + 3.5*mm
# Full Russian sentence for question
c.setFont("Helvetica-Oblique", 9)
c.setFillColor(C_PURPLE)
q_ru_full = " ".join(q_ru)
c.drawString(MARGIN_L + 17*mm, y - 22*mm, f"({q_ru_full})")
y -= 31*mm
# ── Answer box ────────────────────────────────────────────────────
c.setFillColor(C_ANSWER_BG)
c.setStrokeColor(a_color)
c.setLineWidth(1.5)
c.roundRect(MARGIN_L + 5*mm, y - 28*mm, CONTENT_W - 5*mm, 28*mm, 3*mm, fill=1, stroke=1)
# A label badge
c.setFillColor(a_color)
c.roundRect(MARGIN_L + 5*mm, y - 7*mm, 12*mm, 7*mm, 2*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(colors.white)
c.drawString(MARGIN_L + 7*mm, y - 5.5*mm, "A:")
# English answer
c.setFont("Helvetica-Bold", 11)
c.setFillColor(a_color)
c.drawString(MARGIN_L + 20*mm, y - 5.5*mm, a_en)
# Russian translation under each word
a_en_ws = a_en.split()
x_cur = MARGIN_L + 20*mm
for i, ew in enumerate(a_en_ws):
rw = a_ru[i] if i < len(a_ru) else ""
c.setFont("Helvetica-Bold", 9)
c.setFillColor(C_DARK_GREY)
c.drawString(x_cur, y - 11*mm, ew)
c.setFont("Helvetica", 7.5)
c.setFillColor(C_TEAL)
c.drawString(x_cur, y - 16*mm, rw)
w_en = c.stringWidth(ew, "Helvetica-Bold", 9)
w_ru = c.stringWidth(rw, "Helvetica", 7.5)
x_cur += max(w_en, w_ru) + 3.5*mm
# Full Russian sentence for answer
c.setFont("Helvetica-Oblique", 9)
c.setFillColor(C_GREEN)
a_ru_full = " ".join(a_ru)
c.drawString(MARGIN_L + 20*mm, y - 22*mm, f"({a_ru_full})")
y -= 33*mm
draw_footer(c, page_num)
c.showPage()
page_num += 1
c.save()
print(f"Saved: {filename}")
# ─── Build Handbook 1: Possessive Case ────────────────────────────────────────
build_handbook(
filename=os.path.join(OUT_DIR, "handbook_possessive_case.pdf"),
title_en="Possessive Case",
title_ru="Притяжательный падеж",
subtitle_en="When we want to say who owns something, we add ('s) to the name",
subtitle_ru="Когда мы хотим сказать, кому принадлежит предмет, добавляем ('s) к имени",
intro_lines=[
("This is Mark's pen.", ["Это", "ручка", "Марка."]),
("This pen is Mark's.", ["Эта", "ручка", "Марка."]),
("The dog's tail is long.", ["Хвост", "собаки", "длинный."]),
],
qa_list=POSS_QA,
banner_color=C_PURPLE,
q_color=C_BLUE,
a_color=C_GREEN,
q_bg=C_LAVENDER,
a_bg=C_MINTBG,
)
# ─── Build Handbook 2: Articles ───────────────────────────────────────────────
build_handbook(
filename=os.path.join(OUT_DIR, "handbook_articles.pdf"),
title_en="Articles: a / an / the",
title_ru="Артикли: a / an / the",
subtitle_en="Use 'a/an' for any one thing. Use 'the' for a specific thing.",
subtitle_ru="Используй 'a/an' для любого предмета. Используй 'the' для конкретного.",
intro_lines=[
("I have a cat.", ["У", "меня", "есть", "кошка."]),
("She has an apple.", ["У", "неё", "есть", "яблоко."]),
("The sun is bright.", ["Солнце", "яркое."]),
],
qa_list=ART_QA,
banner_color=C_ORANGE,
q_color=C_ORANGE,
a_color=C_TEAL,
q_bg=C_ORANGEBG,
a_bg=C_BLUEBG,
)
print("Both handbooks generated successfully!")
cd /tmp/workspace/esl-handbooks && python3 make_handbooks.py 2>&1
ls -lh /tmp/workspace/esl-handbooks/*.pdf
python3 -c " from pypdf import PdfReader for f in ['/tmp/workspace/esl-handbooks/handbook_possessive_case.pdf', '/tmp/workspace/esl-handbooks/handbook_articles.pdf']: r = PdfReader(f) print(f'{f.split(\"/\")[-1]}: {len(r.pages)} pages') # Sample text from page 1 t = r.pages[0].extract_text() print(' Page 1 snippet:', t[:150].replace('\n',' ')) print() "
handbook possessive case
PDF Document · PDF
handbook articles
PDF Document · PDF
You forget to write Russian translation under every sentence including the quess and their answers for every word and title for both the handbooks you have prepare above on topics possessive cases and Articles
~/esl-handbooks/make_handbooks_v2.py
"""
ESL 3rd Grade Handbooks v2
- Full Russian word-by-word translation under EVERY English word throughout
- Titles, subtitles, rules, examples, questions, answers — all bilingual
"""
from reportlab.pdfgen import canvas as pdfcanvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor, white, black
import os
OUT_DIR = "/tmp/workspace/esl-handbooks"
os.makedirs(OUT_DIR, exist_ok=True)
PAGE_W, PAGE_H = A4
# ── Palette ──────────────────────────────────────────────────────────────────
def hx(s): return HexColor(s)
C = dict(
purple = hx("#6A0DAD"),
blue = hx("#1565C0"),
teal = hx("#00796B"),
orange = hx("#E65100"),
pink = hx("#C2185B"),
green = hx("#2E7D32"),
red = hx("#B71C1C"),
gold = hx("#F9A825"),
lavbg = hx("#EDE7F6"),
mintbg = hx("#E8F5E9"),
yellowbg = hx("#FFFDE7"),
pinkbg = hx("#FCE4EC"),
bluebg = hx("#E3F2FD"),
orangebg = hx("#FFF3E0"),
grey = hx("#424242"),
lgrey = hx("#F5F5F5"),
answerbg = hx("#E8EAF6"),
white = white,
)
# ── Core drawing: word-by-word bilingual line ─────────────────────────────────
def draw_bilingual_line(c, x, y, words_en, words_ru,
en_font="Helvetica-Bold", en_size=11,
ru_font="Helvetica", ru_size=8,
en_color=None, ru_color=None,
line_gap=5.5):
"""
Draw each English word with its Russian translation directly beneath it.
Returns the rightmost x reached (useful for centering checks).
words_en: list of strings e.g. ["This","is","Mark's","pen."]
words_ru: list of strings e.g. ["Это","есть","ручка","Марка."]
"""
if en_color is None: en_color = C['grey']
if ru_color is None: ru_color = C['pink']
cur_x = x
for i, ew in enumerate(words_en):
rw = words_ru[i] if i < len(words_ru) else ""
w_en = c.stringWidth(ew, en_font, en_size)
w_ru = c.stringWidth(rw, ru_font, ru_size)
col_w = max(w_en, w_ru)
c.setFont(en_font, en_size)
c.setFillColor(en_color)
c.drawString(cur_x, y, ew)
c.setFont(ru_font, ru_size)
c.setFillColor(ru_color)
c.drawString(cur_x, y - line_gap*mm, rw)
cur_x += col_w + 2.8*mm
return cur_x
def measure_bilingual_line(c, words_en, words_ru,
en_font="Helvetica-Bold", en_size=11,
ru_font="Helvetica", ru_size=8):
"""Return total pixel width of a bilingual line."""
total = 0
for i, ew in enumerate(words_en):
rw = words_ru[i] if i < len(words_ru) else ""
w_en = c.stringWidth(ew, en_font, en_size)
w_ru = c.stringWidth(rw, ru_font, ru_size)
total += max(w_en, w_ru) + 2.8*mm
return total
def draw_bilingual_centred(c, cx, y, words_en, words_ru,
en_font="Helvetica-Bold", en_size=14,
ru_font="Helvetica", ru_size=9,
en_color=None, ru_color=None, line_gap=5.5):
"""Draw a centred bilingual line."""
total_w = measure_bilingual_line(c, words_en, words_ru, en_font, en_size, ru_font, ru_size)
x_start = cx - total_w / 2
draw_bilingual_line(c, x_start, y, words_en, words_ru,
en_font, en_size, ru_font, ru_size,
en_color, ru_color, line_gap)
def draw_footer(c, page_num, banner_color):
c.setFillColor(banner_color)
c.rect(0, 0, PAGE_W, 10*mm, fill=1, stroke=0)
c.setFont("Helvetica", 8)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, 3*mm, f"Page {page_num} • ESL 3rd Grade Handbook")
def draw_page_bg(c, page_num, bg_list):
c.setFillColor(bg_list[page_num % len(bg_list)])
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
# ═══════════════════════════════════════════════════════════════════════════════
# DATA — POSSESSIVE CASE
# ═══════════════════════════════════════════════════════════════════════════════
POSS_TITLE_EN = ["Possessive", "Case"]
POSS_TITLE_RU = ["Притяжательный", "падеж"]
POSS_RULE_EN = ["When", "we", "want", "to", "say", "who", "owns", "something,",
"we", "add", "('s)", "to", "the", "person's", "name."]
POSS_RULE_RU = ["Когда", "мы", "хотим", "сказать", "кому", "кто", "владеет", "чем-то,",
"мы", "добавляем", "('s)", "к", "имени", "человека,", "имени."]
POSS_EXAMPLES = [
(["This", "is", "Mark's", "pen."], ["Это", "есть", "ручка", "Марка."]),
(["This", "pen", "is", "Mark's."], ["Эта", "ручка", "есть", "Марка."]),
(["The", "dog's", "tail", "is", "long."], ["Хвост", "собаки", "есть", "длинный."]),
]
POSS_QA = [
(["Whose", "pen", "is", "this?"],
["Чья", "ручка", "это?", ""],
["This", "is", "Mark's", "pen."],
["Это", "есть", "ручка", "Марка."]),
(["Whose", "book", "is", "on", "the", "table?"],
["Чья", "книга", "на", "на", "столе?",""],
["Anna's", "book", "is", "on", "the", "table."],
["Книга", "Анны", "на", "на", "столе.",""]),
(["Whose", "dog", "is", "playing", "outside?"],
["Чья", "собака", "есть", "играет", "снаружи?"],
["Tom's", "dog", "is", "playing", "outside."],
["Собака", "Тома", "есть", "играет", "снаружи."]),
(["Whose", "backpack", "is", "by", "the", "door?"],
["Чей", "рюкзак", "есть","у", "двери?",""],
["Sam's", "backpack", "is", "by", "the", "door."],
["Рюкзак", "Сэма", "есть","у", "двери.",""]),
(["Whose", "bicycle", "is", "in", "the", "yard?"],
["Чей", "велосипед","есть","во", "дворе?", ""],
["Kate's", "bicycle", "is", "in", "the", "yard."],
["Велосипед","Кейт", "есть", "во", "дворе.", ""]),
(["Whose", "jacket", "is", "on", "the", "chair?"],
["Чья", "куртка", "есть", "на", "стуле?",""],
["Lisa's", "jacket", "is", "on", "the", "chair."],
["Куртка", "Лизы", "есть", "на", "стуле.",""]),
(["Whose", "cat", "is", "sleeping", "on", "the", "sofa?"],
["Чья", "кошка", "есть", "спит", "на", "диване?",""],
["Mia's", "cat", "is", "sleeping", "on", "the", "sofa."],
["Кошка", "Мии", "есть", "спит", "на", "диване.",""]),
(["Whose", "lunch", "is", "in", "the", "bag?"],
["Чей", "обед", "есть", "в", "сумке?",""],
["Peter's", "lunch", "is", "in", "the", "bag."],
["Обед", "Питера", "есть", "в", "сумке.",""]),
(["Whose", "ball", "is", "under", "the", "bench?"],
["Чей", "мяч", "есть", "под", "скамейкой?",""],
["Leo's", "ball", "is", "under", "the", "bench."],
["Мяч", "Лео", "есть", "под", "скамейкой.",""]),
(["Whose", "notebook", "has", "a", "red", "cover?"],
["Чья", "тетрадь", "имеет", "с", "красной","обложкой?"],
["Emma's", "notebook", "has", "a", "red", "cover."],
["Тетрадь", "Эммы", "имеет", "с", "красной","обложкой."]),
(["Whose", "shoes", "are", "near", "the", "entrance?"],
["Чьи", "ботинки", "есть", "рядом", "с", "входом?"],
["Nick's", "shoes", "are", "near", "the", "entrance."],
["Ботинки","Ника", "есть", "рядом", "с", "входом."]),
(["Whose", "drawing", "is", "on", "the", "wall?"],
["Чей", "рисунок", "есть", "на", "стене?",""],
["Lily's", "drawing", "is", "on", "the", "wall."],
["Рисунок","Лили", "есть", "на", "стене.",""]),
(["Whose", "water", "bottle", "is", "on", "the", "desk?"],
["Чья", "бутылка","воды", "есть", "на", "столе?",""],
["Chris's", "water", "bottle", "is", "on", "the", "desk."],
["Бутылка", "воды", "Криса", "есть", "на", "столе.",""]),
(["Whose", "horse", "is", "in", "the", "field?"],
["Чья", "лошадь", "есть", "на", "поле?", ""],
["The", "farmer's", "horse","is", "in", "the", "field."],
["Лошадь", "фермера", "есть", "на", "поле.", "", ""]),
(["Whose", "coat", "is", "the", "warmest", "in", "class?"],
["Чьё", "пальто", "есть", "самое", "тёплое", "в", "классе?"],
["Sophia's", "coat", "is", "the", "warmest", "in", "class."],
["Пальто", "Софии", "есть", "самое", "тёплое", "в", "классе."]),
]
# ═══════════════════════════════════════════════════════════════════════════════
# DATA — ARTICLES
# ═══════════════════════════════════════════════════════════════════════════════
ART_TITLE_EN = ["Articles:", "a", "/", "an", "/", "the"]
ART_TITLE_RU = ["Артикли:", "a", "/", "an", "/", "the"]
ART_RULE_EN = ["Use", "'a/an'", "for", "any", "one", "thing.", "Use", "'the'", "for", "a", "specific", "thing."]
ART_RULE_RU = ["Используй","'a/an'","для","любого","одного","предмета.","Используй","'the'","для","конкретного","конкретного","предмета."]
ART_EXAMPLES = [
(["I", "have", "a", "cat."], ["Я", "имею", "кошку","."]),
(["She","has", "an", "apple."], ["Она", "имеет","яблоко","."]),
(["The","sun", "is", "bright."], ["Солнце","есть","ярким","."]),
]
ART_QA = [
(["Do", "you", "have", "a", "pencil?"],
["Есть", "у", "тебя", "карандаш?",""],
["Yes,", "I", "have", "a", "pencil."],
["Да,", "у", "меня", "есть", "карандаш."]),
(["Is", "there","an", "elephant","at", "the", "zoo?"],
["Есть", "ли", "слон", "в", "зоопарке?","", ""],
["Yes,", "there","is", "an", "elephant","at", "the", "zoo."],
["Да,", "в", "зоопарке","есть", "слон.", "", "", ""]),
(["Can", "you", "open", "the", "window,","please?"],
["Ты", "можешь","открыть","окно,","пожалуйста?",""],
["Sure,", "I", "can", "open", "the", "window."],
["Конечно,","я", "могу", "открыть","окно.", ""]),
(["Is", "this", "a", "dog", "or", "a", "cat?"],
["Это", "есть", "собака","или", "кошка?","", ""],
["This", "is", "a", "dog."],
["Это", "есть", "собака","."]),
(["Where", "is", "the", "teacher's","desk?"],
["Где", "есть", "стол", "учителя?", ""],
["The", "teacher's","desk","is", "at", "the", "front","of","the","room."],
["Стол", "учителя", "стоит","в", "начале","класса.","", "","",""]),
(["Do", "you", "want", "an", "apple","or", "an", "orange?"],
["Ты", "хочешь","взять","яблоко?","или", "апельсин?","",""],
["I", "want", "an", "apple."],
["Я", "хочу", "яблоко","."]),
(["Is", "the", "sun", "in", "the", "sky", "today?"],
["Солнце","есть", "на", "небе", "сегодня?","", ""],
["Yes,", "the", "sun", "is", "in", "the", "sky", "today."],
["Да,", "солнце","есть", "на", "небе", "сегодня.", "", ""]),
(["Did", "you", "see", "a", "bird", "in", "the", "garden?"],
["Ты", "видел","птицу", "в", "саду?","", "", ""],
["Yes,", "I", "saw", "a", "bird", "in", "the", "garden."],
["Да,", "я", "видел", "птицу", "в", "саду.","", ""]),
(["Is", "there","an", "umbrella","near", "the", "door?"],
["Есть", "ли", "зонтик","у", "двери?","", ""],
["Yes,", "there","is", "an", "umbrella","near","the","door."],
["Да,", "у", "двери", "есть", "зонтик.", "","", ""]),
(["What", "is", "on", "the", "shelf?"],
["Что", "стоит","на", "полке?",""],
["There", "is", "a", "book", "on", "the", "shelf."],
["На", "полке","есть", "книга.","", "", ""]),
(["Is", "this", "an", "easy", "question?"],
["Это", "лёгкий","вопрос?","", ""],
["Yes,", "this", "is", "an", "easy", "question."],
["Да,", "это", "лёгкий","вопрос.","", ""]),
(["Where", "is", "the", "moon", "at", "night?"],
["Где", "находится","луна","ночью?","", ""],
["The", "moon", "is", "in", "the", "sky", "at", "night."],
["Луна", "находится","на", "небе", "ночью.","", "", ""]),
(["Do", "you", "have", "a", "pet", "at", "home?"],
["У", "тебя", "есть", "домашнее","животное","дома?",""],
["Yes,", "I", "have", "a", "cat", "at", "home."],
["Да,", "у", "меня", "дома", "есть", "кошка.",""],),
(["Can", "you", "pass", "me", "the", "eraser?"],
["Ты", "можешь","передать","мне","ластик?",""],
["Here", "you", "go,", "here", "is", "the", "eraser."],
["Вот,", "держи","вот", "ластик.","", "", ""]),
(["Is", "a", "whale", "a", "big", "animal?"],
["Кит", "есть", "большое","животное?","",""],
["Yes,", "a", "whale", "is", "a", "very", "big", "animal."],
["Да,", "кит", "есть", "очень", "большое","животное.","",""]),
]
# ═══════════════════════════════════════════════════════════════════════════════
# BUILDER
# ═══════════════════════════════════════════════════════════════════════════════
def build_handbook(
filename,
title_en_words, title_ru_words,
subtitle_en_words, subtitle_ru_words,
rule_en, rule_ru,
examples, # list of (en_words, ru_words)
qa_list, # list of (q_en, q_ru, a_en, a_ru)
banner_color,
q_color, a_color,
bg_colors,
):
c = pdfcanvas.Canvas(filename, pagesize=A4)
page_num = 1
# ── PAGE 1: Cover ─────────────────────────────────────────────────────────
c.setFillColor(bg_colors[0])
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
# Top banner
c.setFillColor(banner_color)
c.rect(0, PAGE_H - 75*mm, PAGE_W, 75*mm, fill=1, stroke=0)
# Decorative stars
c.setFont("Helvetica-Bold", 16)
c.setFillColor(C['gold'])
star_positions = [(15*mm, PAGE_H-12*mm),(PAGE_W-15*mm, PAGE_H-12*mm),
(PAGE_W/2, PAGE_H-8*mm),(40*mm, PAGE_H-28*mm),(PAGE_W-40*mm, PAGE_H-28*mm)]
for sx, sy in star_positions:
c.drawCentredString(sx, sy, "★")
# Title (centred, word-by-word bilingual)
title_w = measure_bilingual_line(c, title_en_words, title_ru_words,
"Helvetica-Bold", 26, "Helvetica-Bold", 13)
tx = PAGE_W/2 - title_w/2
draw_bilingual_line(c, tx, PAGE_H - 35*mm, title_en_words, title_ru_words,
"Helvetica-Bold", 26, "Helvetica-Bold", 13,
en_color=white, ru_color=C['gold'], line_gap=7)
# Grade label
c.setFont("Helvetica-Bold", 10)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, PAGE_H - 57*mm, "3rd Grade ESL")
c.setFont("Helvetica", 9)
c.setFillColor(C['gold'])
c.drawCentredString(PAGE_W/2, PAGE_H - 63*mm, "3-й класс ЭСЛ")
# Subtitle (centred word-by-word)
y = PAGE_H - 83*mm
sub_w = measure_bilingual_line(c, subtitle_en_words, subtitle_ru_words,
"Helvetica-Bold", 10, "Helvetica", 8)
sx = PAGE_W/2 - sub_w/2
# wrap if too wide
if sub_w > PAGE_W - 30*mm:
sx = 15*mm
draw_bilingual_line(c, sx, y, subtitle_en_words, subtitle_ru_words,
"Helvetica-Bold", 10, "Helvetica", 8,
en_color=banner_color, ru_color=C['teal'], line_gap=5)
# Rule box
y -= 16*mm
box_x = 14*mm
box_w = PAGE_W - 28*mm
# How many rows does rule need?
rule_row_h = 16*mm
n_example_rows = len(examples)
box_h = rule_row_h * 2 + n_example_rows * 16*mm + 8*mm
c.setFillColor(C['yellowbg'])
c.setStrokeColor(banner_color)
c.setLineWidth(2)
c.roundRect(box_x, y - box_h, box_w, box_h, 4*mm, fill=1, stroke=1)
# "Grammar Rule" label
c.setFillColor(banner_color)
c.roundRect(box_x + 3*mm, y - 6.5*mm, 34*mm, 7*mm, 2*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 8)
c.setFillColor(white)
c.drawString(box_x + 5*mm, y - 5.5*mm, "Grammar Rule / Правило")
y -= 10*mm
# Draw rule (may wrap)
MAX_ROW_W = box_w - 8*mm
row_start = 0
while row_start < len(rule_en):
# Fit as many words as possible
row_end = row_start + 1
while row_end <= len(rule_en):
w = measure_bilingual_line(c, rule_en[row_start:row_end], rule_ru[row_start:row_end],
"Helvetica", 9, "Helvetica", 7.5)
if w > MAX_ROW_W:
row_end -= 1
break
row_end += 1
if row_end == row_start:
row_end = row_start + 1
draw_bilingual_line(c, box_x + 4*mm, y,
rule_en[row_start:row_end], rule_ru[row_start:row_end],
"Helvetica", 9, "Helvetica", 7.5,
en_color=C['blue'], ru_color=C['pink'], line_gap=5)
y -= 13*mm
row_start = row_end
# Example sentences
c.setFont("Helvetica-Bold", 8)
c.setFillColor(banner_color)
c.drawString(box_x + 4*mm, y + 1*mm, "Examples / Примеры:")
y -= 5*mm
for (ex_en, ex_ru) in examples:
c.setFillColor(C['mintbg'])
c.roundRect(box_x + 4*mm, y - 9*mm, box_w - 8*mm, 9*mm, 2*mm, fill=1, stroke=0)
draw_bilingual_line(c, box_x + 7*mm, y - 1*mm, ex_en, ex_ru,
"Helvetica-Bold", 9, "Helvetica", 7.5,
en_color=C['blue'], ru_color=C['teal'], line_gap=4.5)
y -= 13*mm
draw_footer(c, page_num, banner_color)
c.showPage()
page_num += 1
# ── Q&A PAGES ─────────────────────────────────────────────────────────────
MARGIN = 14*mm
CW = PAGE_W - 2 * MARGIN
Q_PER_PAGE = 3
q_idx = 0
for page_start in range(0, len(qa_list), Q_PER_PAGE):
batch = qa_list[page_start:page_start + Q_PER_PAGE]
# Background
c.setFillColor(bg_colors[page_num % len(bg_colors)])
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
# Page header bar
c.setFillColor(banner_color)
c.rect(0, PAGE_H - 16*mm, PAGE_W, 16*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 11)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, PAGE_H - 7*mm,
"Conversation Questions / Вопросы для разговора")
c.setFont("Helvetica", 8)
c.setFillColor(C['gold'])
c.drawCentredString(PAGE_W/2, PAGE_H - 12*mm, title_en_words[0] + " " + (title_en_words[1] if len(title_en_words)>1 else ""))
y = PAGE_H - 20*mm
for (q_en, q_ru, a_en, a_ru) in batch:
q_idx += 1
# ── Measure heights ───────────────────────────────────────────────
# We'll use 13mm per bilingual row + some padding
def count_rows(words, max_w, en_font, en_size, ru_font, ru_size):
rows = 0
i = 0
while i < len(words):
j = i + 1
while j <= len(words):
w = measure_bilingual_line(c, words[i:j], words[i:j],
en_font, en_size, ru_font, ru_size)
if w > max_w: break
j += 1
rows += 1
i = max(i+1, j-1)
return rows
INNER_W = CW - 10*mm
# Q box: label row (7mm) + bilingual rows
q_rows = 1 # Usually fits in 1 row for our sentences
q_box_h = 7*mm + q_rows * 14*mm + 5*mm # ~26mm base
# A box
a_box_h = 7*mm + 1 * 14*mm + 5*mm # ~26mm base
gap = 3*mm # gap between Q and A
total_h = q_box_h + gap + a_box_h + 6*mm
# ── Q box ─────────────────────────────────────────────────────────
c.setFillColor(C['lavbg'])
c.setStrokeColor(q_color)
c.setLineWidth(1.5)
c.roundRect(MARGIN, y - q_box_h, CW, q_box_h, 3*mm, fill=1, stroke=1)
# Q badge
c.setFillColor(q_color)
c.roundRect(MARGIN + 2*mm, y - 7*mm, 18*mm, 7*mm, 2*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(white)
c.drawString(MARGIN + 4*mm, y - 5.5*mm, f"Q{q_idx} Question / Вопрос")
# Q bilingual (with wrapping)
qy = y - 10*mm
draw_bilingual_wrapped(c, MARGIN + 4*mm, qy, q_en, q_ru, INNER_W,
"Helvetica-Bold", 10, "Helvetica", 7.5,
q_color, C['pink'])
y -= q_box_h + gap
# ── A box ─────────────────────────────────────────────────────────
c.setFillColor(C['answerbg'])
c.setStrokeColor(a_color)
c.setLineWidth(1.5)
c.roundRect(MARGIN + 4*mm, y - a_box_h, CW - 4*mm, a_box_h, 3*mm, fill=1, stroke=1)
# A badge
c.setFillColor(a_color)
c.roundRect(MARGIN + 6*mm, y - 7*mm, 18*mm, 7*mm, 2*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(white)
c.drawString(MARGIN + 8*mm, y - 5.5*mm, f"A{q_idx} Answer / Ответ")
# A bilingual
ay = y - 10*mm
draw_bilingual_wrapped(c, MARGIN + 8*mm, ay, a_en, a_ru, INNER_W,
"Helvetica-Bold", 10, "Helvetica", 7.5,
a_color, C['teal'])
y -= a_box_h + 5*mm
draw_footer(c, page_num, banner_color)
c.showPage()
page_num += 1
c.save()
print(f"Saved: {filename} ({page_num-1} pages)")
def draw_bilingual_wrapped(c, x, y, words_en, words_ru, max_w,
en_font, en_size, ru_font, ru_size,
en_color, ru_color, line_gap=5, line_h=13*mm):
"""Draw bilingual text with word-wrap."""
i = 0
while i < len(words_en):
# Find how many words fit on this row
j = i + 1
while j <= len(words_en):
w = measure_bilingual_line(c,
words_en[i:j], words_ru[i:j],
en_font, en_size, ru_font, ru_size)
if w > max_w:
break
j += 1
row_end = max(i + 1, j - 1)
draw_bilingual_line(c, x, y,
words_en[i:row_end], words_ru[i:row_end],
en_font, en_size, ru_font, ru_size,
en_color, ru_color, line_gap)
y -= line_h
i = row_end
# ── Build POSSESSIVE CASE handbook ───────────────────────────────────────────
build_handbook(
filename=os.path.join(OUT_DIR, "handbook_possessive_case_v2.pdf"),
title_en_words=POSS_TITLE_EN,
title_ru_words=POSS_TITLE_RU,
subtitle_en_words=["When", "we", "add", "('s)", "to", "a", "name,", "we", "show", "ownership."],
subtitle_ru_words=["Когда", "мы", "добавляем","('s)","к", "имени,","мы","показываем","принадлежность.",""],
rule_en=POSS_RULE_EN,
rule_ru=POSS_RULE_RU,
examples=POSS_EXAMPLES,
qa_list=POSS_QA,
banner_color=C['purple'],
q_color=C['blue'],
a_color=C['green'],
bg_colors=[C['yellowbg'], C['lavbg'], C['mintbg'], C['pinkbg'], C['bluebg'], C['orangebg']],
)
# ── Build ARTICLES handbook ───────────────────────────────────────────────────
build_handbook(
filename=os.path.join(OUT_DIR, "handbook_articles_v2.pdf"),
title_en_words=ART_TITLE_EN,
title_ru_words=ART_TITLE_RU,
subtitle_en_words=["'a/an'", "=", "any", "one", "thing.", "'the'", "=", "a", "specific", "thing."],
subtitle_ru_words=["'a/an'","=","любой","один","предмет.","'the'","=","конкретный","конкретный","предмет."],
rule_en=ART_RULE_EN,
rule_ru=ART_RULE_RU,
examples=ART_EXAMPLES,
qa_list=ART_QA,
banner_color=C['orange'],
q_color=C['orange'],
a_color=C['teal'],
bg_colors=[C['orangebg'], C['bluebg'], C['mintbg'], C['yellowbg'], C['lavbg'], C['pinkbg']],
)
print("Done!")
cd /tmp/workspace/esl-handbooks && python3 make_handbooks_v2.py 2>&1
python3 -c " from pypdf import PdfReader for f in ['/tmp/workspace/esl-handbooks/handbook_possessive_case_v2.pdf', '/tmp/workspace/esl-handbooks/handbook_articles_v2.pdf']: r = PdfReader(f) import os size = os.path.getsize(f) print(f'{f.split(\"/\")[-1]}: {len(r.pages)} pages, {size//1024} KB') "
handbook possessive case v2
PDF Document · PDF
handbook articles v2
PDF Document · PDF
| Location | What's now included |
|---|---|
| Title (banner) | English word + Russian word stacked beneath each word |
| Grade label | "3rd Grade ESL" + "3-й класс ЭСЛ" |
| Subtitle | Every English word has its Russian translation directly below |
| Grammar Rule | Every single word has Russian beneath it |
| Example sentences | Each word stacked with Russian below |
| Q badge | "Question / Вопрос" |
| Every Question | Each English word has its Russian translation directly underneath |
| A badge | "Answer / Ответ" |
| Every Answer | Each English word has its Russian translation directly underneath |
| Footer | "ESL 3rd Grade Handbook" on every page |
Prepare a colorful handbook pdf for 3rd grade ESL student on following 2 topics -"Possessive case When we want to say who owns something, we add( 's )to the person or animal's name: For example This is Mark's pen./This pen is Mark's. prepare 15 conversation questions( logical and not funny) based on this topic with Russian translation under every sentence along with it's answers.Also mention the topic title at top with Russian translation under it . Also prepare a next handbook in similar way for topic " Articles" for 3rd grade ESL student with 15 conversation questions ( logical and not funny) with Russian translation under every sentences along with it's answers.Dont forget to add Russian translation under every sentence and also mentione title at top
~/esl-handbooks/make_handbooks_v3.py
"""
ESL 3rd Grade Handbooks v3
- Full Russian SENTENCE translation on the line directly below every English sentence
- Titles have Russian translation on the line below
- Colorful, clean layout for 3rd graders
"""
from reportlab.pdfgen import canvas as pdfcanvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor, white, black
import os
OUT_DIR = "/tmp/workspace/esl-handbooks"
os.makedirs(OUT_DIR, exist_ok=True)
PAGE_W, PAGE_H = A4
def hx(s): return HexColor(s)
C = dict(
purple = hx("#6A0DAD"),
blue = hx("#1565C0"),
teal = hx("#00796B"),
orange = hx("#E65100"),
pink = hx("#C2185B"),
green = hx("#2E7D32"),
gold = hx("#F9A825"),
darkgold = hx("#E65100"),
lavbg = hx("#EDE7F6"),
mintbg = hx("#E8F5E9"),
yellowbg = hx("#FFFDE7"),
pinkbg = hx("#FCE4EC"),
bluebg = hx("#E3F2FD"),
orangebg = hx("#FFF3E0"),
grey = hx("#37474F"),
answerbg = hx("#E8EAF6"),
rubyred = hx("#B71C1C"),
darkblue = hx("#0D47A1"),
)
# ── Utility: draw a string that fits within max_width, return lines ───────────
def wrap_text(c, text, font, size, max_w):
"""Split text into lines that fit within max_w pixels."""
words = text.split()
lines = []
current = ""
for word in words:
test = (current + " " + word).strip()
if c.stringWidth(test, font, size) <= max_w:
current = test
else:
if current:
lines.append(current)
current = word
if current:
lines.append(current)
return lines if lines else [""]
def draw_text_block(c, x, y, en_text, ru_text,
en_font, en_size, en_color,
ru_font, ru_size, ru_color,
max_w, line_spacing=5.5):
"""
Draw English sentence, then Russian sentence directly beneath it.
Both wrap independently. Returns final y after drawing.
"""
en_lines = wrap_text(c, en_text, en_font, en_size, max_w)
ru_lines = wrap_text(c, ru_text, ru_font, ru_size, max_w)
cur_y = y
for line in en_lines:
c.setFont(en_font, en_size)
c.setFillColor(en_color)
c.drawString(x, cur_y, line)
cur_y -= (en_size * 0.4 + 1) * mm
for line in ru_lines:
c.setFont(ru_font, ru_size)
c.setFillColor(ru_color)
c.drawString(x, cur_y, line)
cur_y -= (ru_size * 0.4 + 1) * mm
return cur_y
def measure_block_height(c, en_text, ru_text,
en_font, en_size,
ru_font, ru_size, max_w):
"""Estimate height in points of a bilingual block."""
en_lines = wrap_text(c, en_text, en_font, en_size, max_w)
ru_lines = wrap_text(c, ru_text, ru_font, ru_size, max_w)
h = len(en_lines) * (en_size * 0.4 + 1) * mm
h += len(ru_lines) * (ru_size * 0.4 + 1) * mm
return h
def draw_footer(c, page_num, color):
c.setFillColor(color)
c.rect(0, 0, PAGE_W, 10*mm, fill=1, stroke=0)
c.setFont("Helvetica", 8)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, 3.5*mm,
f"Page {page_num} • ESL 3rd Grade Handbook")
# ═══════════════════════════════════════════════════════════════════════════════
# DATA — POSSESSIVE CASE
# ═══════════════════════════════════════════════════════════════════════════════
POSS_QA = [
("Whose pen is this?",
"Чья это ручка?",
"This is Mark's pen.",
"Это ручка Марка."),
("Whose book is on the table?",
"Чья книга лежит на столе?",
"Anna's book is on the table.",
"Книга Анны лежит на столе."),
("Whose dog is playing outside?",
"Чья собака играет на улице?",
"Tom's dog is playing outside.",
"Собака Тома играет на улице."),
("Whose backpack is by the door?",
"Чей рюкзак стоит у двери?",
"Sam's backpack is by the door.",
"Рюкзак Сэма стоит у двери."),
("Whose bicycle is in the yard?",
"Чей велосипед стоит во дворе?",
"Kate's bicycle is in the yard.",
"Велосипед Кейт стоит во дворе."),
("Whose jacket is on the chair?",
"Чья куртка висит на стуле?",
"Lisa's jacket is on the chair.",
"Куртка Лизы висит на стуле."),
("Whose cat is sleeping on the sofa?",
"Чья кошка спит на диване?",
"Mia's cat is sleeping on the sofa.",
"Кошка Мии спит на диване."),
("Whose lunch is in the bag?",
"Чей обед находится в сумке?",
"Peter's lunch is in the bag.",
"Обед Питера находится в сумке."),
("Whose ball is under the bench?",
"Чей мяч лежит под скамейкой?",
"Leo's ball is under the bench.",
"Мяч Лео лежит под скамейкой."),
("Whose notebook has a red cover?",
"Чья тетрадь имеет красную обложку?",
"Emma's notebook has a red cover.",
"Тетрадь Эммы имеет красную обложку."),
("Whose shoes are near the entrance?",
"Чьи ботинки стоят у входа?",
"Nick's shoes are near the entrance.",
"Ботинки Ника стоят у входа."),
("Whose drawing is on the wall?",
"Чей рисунок висит на стене?",
"Lily's drawing is on the wall.",
"Рисунок Лили висит на стене."),
("Whose water bottle is on the desk?",
"Чья бутылка воды стоит на парте?",
"Chris's water bottle is on the desk.",
"Бутылка воды Криса стоит на парте."),
("Whose horse is in the field?",
"Чья лошадь пасётся на поле?",
"The farmer's horse is in the field.",
"Лошадь фермера пасётся на поле."),
("Whose coat is the warmest in class?",
"Чьё пальто самое тёплое в классе?",
"Sophia's coat is the warmest in class.",
"Пальто Софии самое тёплое в классе."),
]
# ═══════════════════════════════════════════════════════════════════════════════
# DATA — ARTICLES
# ═══════════════════════════════════════════════════════════════════════════════
ART_QA = [
("Do you have a pencil?",
"У тебя есть карандаш?",
"Yes, I have a pencil.",
"Да, у меня есть карандаш."),
("Is there an elephant at the zoo?",
"Есть ли в зоопарке слон?",
"Yes, there is an elephant at the zoo.",
"Да, в зоопарке есть слон."),
("Can you open the window, please?",
"Ты можешь открыть окно, пожалуйста?",
"Sure, I can open the window.",
"Конечно, я могу открыть окно."),
("Is this a dog or a cat?",
"Это собака или кошка?",
"This is a dog.",
"Это собака."),
("Where is the teacher's desk?",
"Где находится стол учителя?",
"The teacher's desk is at the front of the room.",
"Стол учителя стоит в начале класса."),
("Do you want an apple or an orange?",
"Ты хочешь яблоко или апельсин?",
"I want an apple.",
"Я хочу яблоко."),
("Is the sun in the sky today?",
"Солнце сегодня на небе?",
"Yes, the sun is in the sky today.",
"Да, сегодня солнце на небе."),
("Did you see a bird in the garden?",
"Ты видел птицу в саду?",
"Yes, I saw a bird in the garden.",
"Да, я видел птицу в саду."),
("Is there an umbrella near the door?",
"Есть ли зонтик у двери?",
"Yes, there is an umbrella near the door.",
"Да, у двери есть зонтик."),
("What is on the shelf?",
"Что стоит на полке?",
"There is a book on the shelf.",
"На полке стоит книга."),
("Is this an easy question?",
"Это лёгкий вопрос?",
"Yes, this is an easy question.",
"Да, это лёгкий вопрос."),
("Where is the moon at night?",
"Где находится луна ночью?",
"The moon is in the sky at night.",
"Луна находится на небе ночью."),
("Do you have a pet at home?",
"У тебя есть домашнее животное дома?",
"Yes, I have a cat at home.",
"Да, у меня дома есть кошка."),
("Can you pass me the eraser?",
"Ты можешь передать мне ластик?",
"Here you go, here is the eraser.",
"Вот, держи, вот ластик."),
("Is a whale a big animal?",
"Кит — это большое животное?",
"Yes, a whale is a very big animal.",
"Да, кит — это очень большое животное."),
]
# ═══════════════════════════════════════════════════════════════════════════════
# PAGE BUILDER
# ═══════════════════════════════════════════════════════════════════════════════
def build_handbook(
filename,
title_en, title_ru,
rule_lines, # list of (en, ru) for intro rule display
qa_list,
banner_color, q_color, a_color,
bg_list,
):
c = pdfcanvas.Canvas(filename, pagesize=A4)
pn = 1
# ══════════════════════════════════════════════════════════════════════════
# PAGE 1 — COVER / RULE PAGE
# ══════════════════════════════════════════════════════════════════════════
c.setFillColor(bg_list[0])
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
# ── Banner ────────────────────────────────────────────────────────────────
BANNER_H = 68*mm
c.setFillColor(banner_color)
c.rect(0, PAGE_H - BANNER_H, PAGE_W, BANNER_H, fill=1, stroke=0)
# Decorative star row
c.setFont("Helvetica-Bold", 18)
c.setFillColor(C['gold'])
for sx in [18*mm, 40*mm, PAGE_W/2, PAGE_W-40*mm, PAGE_W-18*mm]:
c.drawCentredString(sx, PAGE_H - 10*mm, "★")
# Title EN
c.setFont("Helvetica-Bold", 30)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, PAGE_H - 28*mm, title_en)
# Title RU — directly below
c.setFont("Helvetica-Bold", 16)
c.setFillColor(C['gold'])
c.drawCentredString(PAGE_W/2, PAGE_H - 40*mm, title_ru)
# Grade badge
c.setFont("Helvetica-Bold", 10)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, PAGE_H - 52*mm, "3rd Grade ESL")
c.setFont("Helvetica", 9)
c.setFillColor(C['gold'])
c.drawCentredString(PAGE_W/2, PAGE_H - 59*mm, "3-й класс ЭСЛ")
# ── Rule Box ─────────────────────────────────────────────────────────────
MARGIN = 15*mm
CW = PAGE_W - 2 * MARGIN
y = PAGE_H - BANNER_H - 8*mm
# Calculate box height needed
box_padding = 8*mm
inner_w = CW - 2 * box_padding
total_content_h = 0
for (en, ru) in rule_lines:
total_content_h += measure_block_height(c, en, ru,
"Helvetica-Bold", 11,
"Helvetica-Oblique", 9.5, inner_w)
total_content_h += 5*mm # gap between lines
box_h = total_content_h + box_padding * 2 + 8*mm # label row
c.setFillColor(hx("#FFFDE7"))
c.setStrokeColor(banner_color)
c.setLineWidth(2.5)
c.roundRect(MARGIN, y - box_h, CW, box_h, 5*mm, fill=1, stroke=1)
# Label inside box
c.setFillColor(banner_color)
c.roundRect(MARGIN + 4*mm, y - 8*mm, 62*mm, 8*mm, 2.5*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(white)
c.drawString(MARGIN + 6*mm, y - 6*mm, "Grammar Rule / Грамматическое правило")
cy = y - 13*mm
for (en, ru) in rule_lines:
# EN line
c.setFont("Helvetica-Bold", 11)
c.setFillColor(C['blue'])
en_lines = wrap_text(c, en, "Helvetica-Bold", 11, inner_w)
for ln in en_lines:
c.drawString(MARGIN + box_padding, cy, ln)
cy -= 5.5*mm
# RU line directly below
c.setFont("Helvetica-Oblique", 9.5)
c.setFillColor(C['pink'])
ru_lines = wrap_text(c, ru, "Helvetica-Oblique", 9.5, inner_w)
for ln in ru_lines:
c.drawString(MARGIN + box_padding, cy, ln)
cy -= 4.5*mm
cy -= 5*mm # gap between pairs
draw_footer(c, pn, banner_color)
c.showPage()
pn += 1
# ══════════════════════════════════════════════════════════════════════════
# Q&A PAGES — 3 questions per page
# ══════════════════════════════════════════════════════════════════════════
Q_PER_PAGE = 3
q_idx = 0
for page_start in range(0, len(qa_list), Q_PER_PAGE):
batch = qa_list[page_start : page_start + Q_PER_PAGE]
# Background
c.setFillColor(bg_list[pn % len(bg_list)])
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
# Top header strip
c.setFillColor(banner_color)
c.rect(0, PAGE_H - 17*mm, PAGE_W, 17*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 12)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, PAGE_H - 8*mm,
"Conversation Questions / Вопросы для разговора")
# small subtitle
c.setFont("Helvetica", 8)
c.setFillColor(C['gold'])
c.drawCentredString(PAGE_W/2, PAGE_H - 13.5*mm, title_en + " | " + title_ru)
MARGIN = 14*mm
CW = PAGE_W - 2 * MARGIN
INNER = CW - 8*mm
y = PAGE_H - 21*mm
for (q_en, q_ru, a_en, a_ru) in batch:
q_idx += 1
# ── measure box heights ──────────────────────────────────────────
q_en_lines = wrap_text(c, q_en, "Helvetica-Bold", 11.5, INNER)
q_ru_lines = wrap_text(c, q_ru, "Helvetica-Oblique", 9.5, INNER)
a_en_lines = wrap_text(c, a_en, "Helvetica-Bold", 11.5, INNER)
a_ru_lines = wrap_text(c, a_ru, "Helvetica-Oblique", 9.5, INNER)
q_text_h = len(q_en_lines) * 5.5*mm + len(q_ru_lines) * 4.5*mm
a_text_h = len(a_en_lines) * 5.5*mm + len(a_ru_lines) * 4.5*mm
BADGE_H = 8*mm
PAD = 4*mm
q_box_h = BADGE_H + PAD + q_text_h + PAD
a_box_h = BADGE_H + PAD + a_text_h + PAD
GAP = 3*mm
BLOCK_GAP = 5*mm
# ── Q Box ────────────────────────────────────────────────────────
# Subtle left accent stripe
c.setFillColor(q_color)
c.rect(MARGIN, y - q_box_h, 3.5*mm, q_box_h, fill=1, stroke=0)
c.setFillColor(hx("#EDE7F6") if q_color == C['blue'] else hx("#FFF3E0"))
c.setStrokeColor(q_color)
c.setLineWidth(1.5)
c.roundRect(MARGIN, y - q_box_h, CW, q_box_h, 3*mm, fill=1, stroke=1)
# Q badge
c.setFillColor(q_color)
c.roundRect(MARGIN + 5*mm, y - BADGE_H + 1*mm, 38*mm, BADGE_H - 1*mm,
2*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(white)
c.drawString(MARGIN + 7*mm, y - BADGE_H + 3*mm,
f"Q{q_idx:02d} Question / Вопрос")
# Q English
qy = y - BADGE_H - PAD
c.setFont("Helvetica-Bold", 11.5)
c.setFillColor(q_color)
for ln in q_en_lines:
c.drawString(MARGIN + 5*mm, qy, ln)
qy -= 5.5*mm
# Q Russian — directly below
c.setFont("Helvetica-Oblique", 9.5)
c.setFillColor(C['pink'])
for ln in q_ru_lines:
c.drawString(MARGIN + 5*mm, qy, ln)
qy -= 4.5*mm
y -= q_box_h + GAP
# ── A Box ────────────────────────────────────────────────────────
c.setFillColor(a_color)
c.rect(MARGIN + 6*mm, y - a_box_h, 3.5*mm, a_box_h, fill=1, stroke=0)
c.setFillColor(C['answerbg'])
c.setStrokeColor(a_color)
c.setLineWidth(1.5)
c.roundRect(MARGIN + 6*mm, y - a_box_h, CW - 6*mm, a_box_h, 3*mm, fill=1, stroke=1)
# A badge
c.setFillColor(a_color)
c.roundRect(MARGIN + 10*mm, y - BADGE_H + 1*mm, 34*mm, BADGE_H - 1*mm,
2*mm, fill=1, stroke=0)
c.setFont("Helvetica-Bold", 9)
c.setFillColor(white)
c.drawString(MARGIN + 12*mm, y - BADGE_H + 3*mm,
f"A{q_idx:02d} Answer / Ответ")
# A English
ay = y - BADGE_H - PAD
c.setFont("Helvetica-Bold", 11.5)
c.setFillColor(a_color)
for ln in a_en_lines:
c.drawString(MARGIN + 10*mm, ay, ln)
ay -= 5.5*mm
# A Russian — directly below
c.setFont("Helvetica-Oblique", 9.5)
c.setFillColor(C['teal'])
for ln in a_ru_lines:
c.drawString(MARGIN + 10*mm, ay, ln)
ay -= 4.5*mm
y -= a_box_h + BLOCK_GAP
draw_footer(c, pn, banner_color)
c.showPage()
pn += 1
c.save()
print(f"Saved: {filename} ({pn-1} pages)")
# ═══════════════════════════════════════════════════════════════════════════════
# BUILD BOTH HANDBOOKS
# ═══════════════════════════════════════════════════════════════════════════════
POSS_RULES = [
("When we want to say who owns something,",
"Когда мы хотим сказать, кому что-то принадлежит,"),
("we add ('s) to the person or animal's name.",
"мы добавляем ('s) к имени человека или животного."),
("Example: This is Mark's pen.",
"Пример: Это ручка Марка."),
("Example: This pen is Mark's.",
"Пример: Эта ручка — Марка."),
]
ART_RULES = [
("We use 'a' before words that start with a consonant sound.",
"Мы используем 'a' перед словами, начинающимися с согласного звука."),
("We use 'an' before words that start with a vowel sound.",
"Мы используем 'an' перед словами, начинающимися с гласного звука."),
("We use 'the' when we talk about a specific thing.",
"Мы используем 'the', когда говорим о конкретном предмете."),
("Example: I have a cat. She has an apple. The sun is bright.",
"Пример: У меня есть кошка. У неё есть яблоко. Солнце яркое."),
]
# --- Handbook 1: Possessive Case ---
build_handbook(
filename=os.path.join(OUT_DIR, "Possessive_Case_Handbook.pdf"),
title_en="Possessive Case",
title_ru="Притяжательный падеж",
rule_lines=POSS_RULES,
qa_list=POSS_QA,
banner_color=C['purple'],
q_color=C['blue'],
a_color=C['green'],
bg_list=[C['yellowbg'], C['lavbg'], C['mintbg'],
C['pinkbg'], C['bluebg'], C['orangebg']],
)
# --- Handbook 2: Articles ---
build_handbook(
filename=os.path.join(OUT_DIR, "Articles_Handbook.pdf"),
title_en="Articles: a / an / the",
title_ru="Артикли: a / an / the",
rule_lines=ART_RULES,
qa_list=ART_QA,
banner_color=C['orange'],
q_color=C['orange'],
a_color=C['teal'],
bg_list=[C['orangebg'], C['bluebg'], C['mintbg'],
C['yellowbg'], C['lavbg'], C['pinkbg']],
)
print("All done!")
cd /tmp/workspace/esl-handbooks && python3 make_handbooks_v3.py 2>&1
python3 -c " from pypdf import PdfReader import os for f in ['/tmp/workspace/esl-handbooks/Possessive_Case_Handbook.pdf', '/tmp/workspace/esl-handbooks/Articles_Handbook.pdf']: r = PdfReader(f) sz = os.path.getsize(f) print(f'{f.split(\"/\")[-1]}: {len(r.pages)} pages, {sz//1024} KB') # Show snippet of page 2 (first Q&A page) txt = r.pages[1].extract_text() print(' Page 2 snippet:', txt[:200].replace('\n', ' | ')) print() "
Possessive Case Handbook
PDF Document · PDF
Articles Handbook
PDF Document · PDF
Q01 Question / Вопрос
Whose pen is this?
Чья это ручка?
A01 Answer / Ответ
This is Mark's pen.
Это ручка Марка.
The Russian language is not visible in both the pdf under every sentence, please be careful you have created an error.
fc-list | grep -i "dejavu\|liberation\|freemono\|ubuntu\|noto\|arial\|times" | grep -i "ttf\|otf" | head -20 2>/dev/null; ls /usr/share/fonts/truetype/ 2>/dev/null; find /usr/share/fonts -name "*.ttf" | grep -i "dejavu\|liberation\|ubuntu\|noto" | head -20
find /usr/share/fonts -name "*.ttf" | grep -iv "variable\|\[" | head -30; echo "---VF---"; find /usr/share/fonts -name "NotoSans*.ttf" | head -20
# Test if the variable font works with reportlab TTFont for Cyrillic python3 -c " from reportlab.pdfbase import pdfmetrics from reportlab.pdfbase.ttfonts import TTFont from reportlab.pdfgen import canvas from reportlab.lib.pagesizes import A4 # Try registering Noto Sans variable font try: pdfmetrics.registerFont(TTFont('NotoSans', '/usr/share/fonts/google-noto-vf/NotoSans[wght].ttf')) print('NotoSans variable font registered OK') except Exception as e: print('Variable font failed:', e) # Try DroidSans (has Cyrillic support) try: pdfmetrics.registerFont(TTFont('DroidSans', '/usr/share/fonts/google-droid-sans-fonts/DroidSans.ttf')) pdfmetrics.registerFont(TTFont('DroidSans-Bold', '/usr/share/fonts/google-droid-sans-fonts/DroidSans-Bold.ttf')) print('DroidSans registered OK') # quick test c = canvas.Canvas('/tmp/test_cyrillic.pdf', pagesize=A4) c.setFont('DroidSans', 14) c.drawString(50, 700, 'Это тест кириллицы') c.setFont('DroidSans-Bold', 14) c.drawString(50, 680, 'Жирный текст: Чья это ручка?') c.save() print('Test PDF written') except Exception as e: print('DroidSans failed:', e) "
~/esl-handbooks/make_handbooks_final.py
"""
ESL 3rd Grade Handbooks — FINAL VERSION
Uses DroidSans TTF (full Cyrillic support) so Russian text is fully visible.
Russian translation as a complete sentence on the line directly below every English sentence.
"""
from reportlab.pdfgen import canvas as pdfcanvas
from reportlab.lib.pagesizes import A4
from reportlab.lib.units import mm
from reportlab.lib.colors import HexColor, white
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
import os
# ── Register Cyrillic-capable fonts ──────────────────────────────────────────
FONT_REG = 'DroidSans'
FONT_BOLD = 'DroidSans-Bold'
pdfmetrics.registerFont(TTFont(FONT_REG, '/usr/share/fonts/google-droid-sans-fonts/DroidSans.ttf'))
pdfmetrics.registerFont(TTFont(FONT_BOLD, '/usr/share/fonts/google-droid-sans-fonts/DroidSans-Bold.ttf'))
OUT_DIR = "/tmp/workspace/esl-handbooks"
os.makedirs(OUT_DIR, exist_ok=True)
PAGE_W, PAGE_H = A4
def hx(s): return HexColor(s)
C = dict(
purple = hx("#6A0DAD"),
blue = hx("#1565C0"),
teal = hx("#00796B"),
orange = hx("#E65100"),
pink = hx("#C2185B"),
green = hx("#2E7D32"),
gold = hx("#F9A825"),
lavbg = hx("#EDE7F6"),
mintbg = hx("#E8F5E9"),
yellowbg = hx("#FFFDE7"),
pinkbg = hx("#FCE4EC"),
bluebg = hx("#E3F2FD"),
orangebg = hx("#FFF3E0"),
grey = hx("#37474F"),
answerbg = hx("#E8EAF6"),
)
# ── Text wrap ─────────────────────────────────────────────────────────────────
def wrap_text(c, text, font, size, max_w):
words = text.split()
lines, current = [], ""
for w in words:
test = (current + " " + w).strip()
if c.stringWidth(test, font, size) <= max_w:
current = test
else:
if current: lines.append(current)
current = w
if current: lines.append(current)
return lines or [""]
def block_height(c, en, ru, en_size, ru_size, max_w, leading=1.5):
el = wrap_text(c, en, FONT_BOLD, en_size, max_w)
rl = wrap_text(c, ru, FONT_REG, ru_size, max_w)
return (len(el) * en_size + len(rl) * ru_size) * leading * (1/72 * 25.4) * mm
def draw_footer(c, pn, color):
c.setFillColor(color)
c.rect(0, 0, PAGE_W, 11*mm, fill=1, stroke=0)
c.setFont(FONT_REG, 8)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, 3.5*mm, f"Page {pn} • ESL 3rd Grade Handbook")
# ═══════════════════════════════════════════════════════════════════════════════
# DATA
# ═══════════════════════════════════════════════════════════════════════════════
POSS_QA = [
("Whose pen is this?",
"Чья это ручка?",
"This is Mark's pen.",
"Это ручка Марка."),
("Whose book is on the table?",
"Чья книга лежит на столе?",
"Anna's book is on the table.",
"Книга Анны лежит на столе."),
("Whose dog is playing outside?",
"Чья собака играет на улице?",
"Tom's dog is playing outside.",
"Собака Тома играет на улице."),
("Whose backpack is by the door?",
"Чей рюкзак стоит у двери?",
"Sam's backpack is by the door.",
"Рюкзак Сэма стоит у двери."),
("Whose bicycle is in the yard?",
"Чей велосипед стоит во дворе?",
"Kate's bicycle is in the yard.",
"Велосипед Кейт стоит во дворе."),
("Whose jacket is on the chair?",
"Чья куртка висит на стуле?",
"Lisa's jacket is on the chair.",
"Куртка Лизы висит на стуле."),
("Whose cat is sleeping on the sofa?",
"Чья кошка спит на диване?",
"Mia's cat is sleeping on the sofa.",
"Кошка Мии спит на диване."),
("Whose lunch is in the bag?",
"Чей обед находится в сумке?",
"Peter's lunch is in the bag.",
"Обед Питера находится в сумке."),
("Whose ball is under the bench?",
"Чей мяч лежит под скамейкой?",
"Leo's ball is under the bench.",
"Мяч Лео лежит под скамейкой."),
("Whose notebook has a red cover?",
"Чья тетрадь имеет красную обложку?",
"Emma's notebook has a red cover.",
"Тетрадь Эммы имеет красную обложку."),
("Whose shoes are near the entrance?",
"Чьи ботинки стоят у входа?",
"Nick's shoes are near the entrance.",
"Ботинки Ника стоят у входа."),
("Whose drawing is on the wall?",
"Чей рисунок висит на стене?",
"Lily's drawing is on the wall.",
"Рисунок Лили висит на стене."),
("Whose water bottle is on the desk?",
"Чья бутылка воды стоит на парте?",
"Chris's water bottle is on the desk.",
"Бутылка воды Криса стоит на парте."),
("Whose horse is in the field?",
"Чья лошадь пасётся на поле?",
"The farmer's horse is in the field.",
"Лошадь фермера пасётся на поле."),
("Whose coat is the warmest in class?",
"Чьё пальто самое тёплое в классе?",
"Sophia's coat is the warmest in class.",
"Пальто Софии самое тёплое в классе."),
]
ART_QA = [
("Do you have a pencil?",
"У тебя есть карандаш?",
"Yes, I have a pencil.",
"Да, у меня есть карандаш."),
("Is there an elephant at the zoo?",
"Есть ли в зоопарке слон?",
"Yes, there is an elephant at the zoo.",
"Да, в зоопарке есть слон."),
("Can you open the window, please?",
"Ты можешь открыть окно, пожалуйста?",
"Sure, I can open the window.",
"Конечно, я могу открыть окно."),
("Is this a dog or a cat?",
"Это собака или кошка?",
"This is a dog.",
"Это собака."),
("Where is the teacher's desk?",
"Где находится стол учителя?",
"The teacher's desk is at the front of the room.",
"Стол учителя стоит в начале класса."),
("Do you want an apple or an orange?",
"Ты хочешь яблоко или апельсин?",
"I want an apple.",
"Я хочу яблоко."),
("Is the sun in the sky today?",
"Солнце сегодня на небе?",
"Yes, the sun is in the sky today.",
"Да, сегодня солнце на небе."),
("Did you see a bird in the garden?",
"Ты видел птицу в саду?",
"Yes, I saw a bird in the garden.",
"Да, я видел птицу в саду."),
("Is there an umbrella near the door?",
"Есть ли зонтик у двери?",
"Yes, there is an umbrella near the door.",
"Да, у двери есть зонтик."),
("What is on the shelf?",
"Что стоит на полке?",
"There is a book on the shelf.",
"На полке стоит книга."),
("Is this an easy question?",
"Это лёгкий вопрос?",
"Yes, this is an easy question.",
"Да, это лёгкий вопрос."),
("Where is the moon at night?",
"Где находится луна ночью?",
"The moon is in the sky at night.",
"Луна находится на небе ночью."),
("Do you have a pet at home?",
"У тебя есть домашнее животное дома?",
"Yes, I have a cat at home.",
"Да, у меня дома есть кошка."),
("Can you pass me the eraser?",
"Ты можешь передать мне ластик?",
"Here you go, here is the eraser.",
"Вот, держи, вот ластик."),
("Is a whale a big animal?",
"Кит — это большое животное?",
"Yes, a whale is a very big animal.",
"Да, кит — это очень большое животное."),
]
# ═══════════════════════════════════════════════════════════════════════════════
# BUILDER
# ═══════════════════════════════════════════════════════════════════════════════
def build_handbook(filename, title_en, title_ru,
rule_pairs, qa_list,
banner_color, q_color, a_color, bg_list):
"""
rule_pairs: list of (english_sentence, russian_sentence) for the intro page
qa_list: list of (q_en, q_ru, a_en, a_ru)
"""
c = pdfcanvas.Canvas(filename, pagesize=A4)
pn = 1
# ══ PAGE 1: Cover / Grammar intro ════════════════════════════════════════
c.setFillColor(bg_list[0])
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
BANNER_H = 72*mm
c.setFillColor(banner_color)
c.rect(0, PAGE_H - BANNER_H, PAGE_W, BANNER_H, fill=1, stroke=0)
# Stars across top
c.setFont(FONT_BOLD, 20)
c.setFillColor(C['gold'])
for sx in [16*mm, 38*mm, PAGE_W/2, PAGE_W-38*mm, PAGE_W-16*mm]:
c.drawCentredString(sx, PAGE_H - 12*mm, "★")
# English Title
c.setFont(FONT_BOLD, 28)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, PAGE_H - 30*mm, title_en)
# Russian Title — directly below
c.setFont(FONT_BOLD, 17)
c.setFillColor(C['gold'])
c.drawCentredString(PAGE_W/2, PAGE_H - 43*mm, title_ru)
# Grade line
c.setFont(FONT_BOLD, 10)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, PAGE_H - 56*mm, "3rd Grade ESL")
c.setFont(FONT_REG, 9)
c.setFillColor(C['gold'])
c.drawCentredString(PAGE_W/2, PAGE_H - 63*mm, "3-й класс ЭСЛ")
# ── Grammar Rule box ──────────────────────────────────────────────────────
MARGIN = 14*mm
CW = PAGE_W - 2*MARGIN
INNER = CW - 10*mm
PAD = 5*mm
# Measure total content height
LABEL_H = 9*mm
LINE_GAP = 3*mm
EN_SIZE = 11
RU_SIZE = 9.5
LINE_LEADING_EN = EN_SIZE * 1.45 * (1/72*25.4) * mm
LINE_LEADING_RU = RU_SIZE * 1.45 * (1/72*25.4) * mm
PAIR_GAP = 4*mm
total_h = LABEL_H + PAD
for (en, ru) in rule_pairs:
el = wrap_text(c, en, FONT_BOLD, EN_SIZE, INNER)
rl = wrap_text(c, ru, FONT_REG, RU_SIZE, INNER)
total_h += len(el)*LINE_LEADING_EN + len(rl)*LINE_LEADING_RU + PAIR_GAP
total_h += PAD
y_box_top = PAGE_H - BANNER_H - 7*mm
c.setFillColor(hx("#FFFDE7"))
c.setStrokeColor(banner_color)
c.setLineWidth(2.5)
c.roundRect(MARGIN, y_box_top - total_h, CW, total_h, 5*mm, fill=1, stroke=1)
# Label tag
c.setFillColor(banner_color)
c.roundRect(MARGIN+4*mm, y_box_top - LABEL_H + 1*mm, 74*mm, LABEL_H-1*mm, 2.5*mm, fill=1, stroke=0)
c.setFont(FONT_BOLD, 9)
c.setFillColor(white)
c.drawString(MARGIN+6*mm, y_box_top - LABEL_H + 3.5*mm, "Grammar Rule / Грамматическое правило")
cy = y_box_top - LABEL_H - PAD
for (en, ru) in rule_pairs:
en_lines = wrap_text(c, en, FONT_BOLD, EN_SIZE, INNER)
ru_lines = wrap_text(c, ru, FONT_REG, RU_SIZE, INNER)
# English lines
for ln in en_lines:
c.setFont(FONT_BOLD, EN_SIZE)
c.setFillColor(C['blue'])
c.drawString(MARGIN+5*mm, cy, ln)
cy -= LINE_LEADING_EN
# Russian lines — directly below
for ln in ru_lines:
c.setFont(FONT_REG, RU_SIZE)
c.setFillColor(C['pink'])
c.drawString(MARGIN+5*mm, cy, ln)
cy -= LINE_LEADING_RU
cy -= PAIR_GAP
draw_footer(c, pn, banner_color)
c.showPage()
pn += 1
# ══ Q&A PAGES — 3 per page ════════════════════════════════════════════════
Q_EN_SIZE = 12
Q_RU_SIZE = 10
A_EN_SIZE = 12
A_RU_SIZE = 10
BADGE_H = 8.5*mm
PAD_V = 3.5*mm
LE_Q = Q_EN_SIZE * 1.5 * (1/72*25.4) * mm
LR_Q = Q_RU_SIZE * 1.5 * (1/72*25.4) * mm
LE_A = A_EN_SIZE * 1.5 * (1/72*25.4) * mm
LR_A = A_RU_SIZE * 1.5 * (1/72*25.4) * mm
q_idx = 0
for page_start in range(0, len(qa_list), 3):
batch = qa_list[page_start:page_start+3]
c.setFillColor(bg_list[pn % len(bg_list)])
c.rect(0, 0, PAGE_W, PAGE_H, fill=1, stroke=0)
# Header bar
HDR_H = 18*mm
c.setFillColor(banner_color)
c.rect(0, PAGE_H - HDR_H, PAGE_W, HDR_H, fill=1, stroke=0)
c.setFont(FONT_BOLD, 12)
c.setFillColor(white)
c.drawCentredString(PAGE_W/2, PAGE_H - 9*mm,
"Conversation Questions / Вопросы для разговора")
c.setFont(FONT_REG, 8)
c.setFillColor(C['gold'])
c.drawCentredString(PAGE_W/2, PAGE_H - 15*mm,
title_en + " | " + title_ru)
MARGIN = 14*mm
CW = PAGE_W - 2*MARGIN
INNER_Q = CW - 8*mm
INNER_A = CW - 14*mm
y = PAGE_H - HDR_H - 4*mm
for (q_en, q_ru, a_en, a_ru) in batch:
q_idx += 1
q_en_lines = wrap_text(c, q_en, FONT_BOLD, Q_EN_SIZE, INNER_Q)
q_ru_lines = wrap_text(c, q_ru, FONT_REG, Q_RU_SIZE, INNER_Q)
a_en_lines = wrap_text(c, a_en, FONT_BOLD, A_EN_SIZE, INNER_A)
a_ru_lines = wrap_text(c, a_ru, FONT_REG, A_RU_SIZE, INNER_A)
q_content_h = len(q_en_lines)*LE_Q + len(q_ru_lines)*LR_Q
a_content_h = len(a_en_lines)*LE_A + len(a_ru_lines)*LR_A
q_box_h = BADGE_H + PAD_V + q_content_h + PAD_V
a_box_h = BADGE_H + PAD_V + a_content_h + PAD_V
GAP_QA = 2.5*mm
GAP_BLOCK = 5*mm
# ── Question box ─────────────────────────────────────────────────
# Left color bar
c.setFillColor(q_color)
c.rect(MARGIN, y - q_box_h, 4*mm, q_box_h, fill=1, stroke=0)
# Box fill + border
c.setFillColor(hx("#EDE7F6") if q_color != C['orange'] else hx("#FFF3E0"))
c.setStrokeColor(q_color)
c.setLineWidth(1.8)
c.roundRect(MARGIN, y - q_box_h, CW, q_box_h, 3.5*mm, fill=1, stroke=1)
# Badge
c.setFillColor(q_color)
c.roundRect(MARGIN+6*mm, y - BADGE_H + 0.8*mm,
40*mm, BADGE_H - 0.8*mm, 2*mm, fill=1, stroke=0)
c.setFont(FONT_BOLD, 9)
c.setFillColor(white)
c.drawString(MARGIN+8*mm, y - BADGE_H + 3.2*mm,
f"Q{q_idx:02d} Question / Вопрос")
# English question
qy = y - BADGE_H - PAD_V
for ln in q_en_lines:
c.setFont(FONT_BOLD, Q_EN_SIZE)
c.setFillColor(q_color)
c.drawString(MARGIN+6*mm, qy, ln)
qy -= LE_Q
# Russian translation — line directly below
for ln in q_ru_lines:
c.setFont(FONT_REG, Q_RU_SIZE)
c.setFillColor(C['pink'])
c.drawString(MARGIN+6*mm, qy, ln)
qy -= LR_Q
y -= q_box_h + GAP_QA
# ── Answer box ───────────────────────────────────────────────────
c.setFillColor(a_color)
c.rect(MARGIN+8*mm, y - a_box_h, 4*mm, a_box_h, fill=1, stroke=0)
c.setFillColor(C['answerbg'])
c.setStrokeColor(a_color)
c.setLineWidth(1.8)
c.roundRect(MARGIN+8*mm, y - a_box_h, CW-8*mm, a_box_h, 3.5*mm, fill=1, stroke=1)
# Badge
c.setFillColor(a_color)
c.roundRect(MARGIN+14*mm, y - BADGE_H + 0.8*mm,
36*mm, BADGE_H - 0.8*mm, 2*mm, fill=1, stroke=0)
c.setFont(FONT_BOLD, 9)
c.setFillColor(white)
c.drawString(MARGIN+16*mm, y - BADGE_H + 3.2*mm,
f"A{q_idx:02d} Answer / Ответ")
# English answer
ay = y - BADGE_H - PAD_V
for ln in a_en_lines:
c.setFont(FONT_BOLD, A_EN_SIZE)
c.setFillColor(a_color)
c.drawString(MARGIN+14*mm, ay, ln)
ay -= LE_A
# Russian translation — line directly below
for ln in a_ru_lines:
c.setFont(FONT_REG, A_RU_SIZE)
c.setFillColor(C['teal'])
c.drawString(MARGIN+14*mm, ay, ln)
ay -= LR_A
y -= a_box_h + GAP_BLOCK
draw_footer(c, pn, banner_color)
c.showPage()
pn += 1
c.save()
print(f"✓ Saved: {filename} ({pn-1} pages)")
# ═══════════════════════════════════════════════════════════════════════════════
# BUILD
# ═══════════════════════════════════════════════════════════════════════════════
POSS_RULES = [
("When we want to say who owns something,",
"Когда мы хотим сказать, кому что-то принадлежит,"),
("we add ('s) to the person or animal's name.",
"мы добавляем ('s) к имени человека или животного."),
("Example: This is Mark's pen.",
"Пример: Это ручка Марка."),
("Example: This pen is Mark's.",
"Пример: Эта ручка — Марка."),
]
ART_RULES = [
("Use 'a' before a word that starts with a consonant sound.",
"Используем 'a' перед словом, начинающимся с согласного звука."),
("Use 'an' before a word that starts with a vowel sound.",
"Используем 'an' перед словом, начинающимся с гласного звука."),
("Use 'the' when we talk about a specific, known thing.",
"Используем 'the', когда говорим о конкретном, известном предмете."),
("Example: I have a cat. She has an apple. The sun is bright.",
"Пример: У меня есть кошка. У неё есть яблоко. Солнце яркое."),
]
build_handbook(
filename=os.path.join(OUT_DIR, "Possessive_Case_FINAL.pdf"),
title_en="Possessive Case",
title_ru="Притяжательный падеж",
rule_pairs=POSS_RULES,
qa_list=POSS_QA,
banner_color=C['purple'],
q_color=C['blue'],
a_color=C['green'],
bg_list=[C['yellowbg'], C['lavbg'], C['mintbg'],
C['pinkbg'], C['bluebg'], C['orangebg']],
)
build_handbook(
filename=os.path.join(OUT_DIR, "Articles_FINAL.pdf"),
title_en="Articles: a / an / the",
title_ru="Артикли: a / an / the",
rule_pairs=ART_RULES,
qa_list=ART_QA,
banner_color=C['orange'],
q_color=C['orange'],
a_color=C['teal'],
bg_list=[C['orangebg'], C['bluebg'], C['mintbg'],
C['yellowbg'], C['lavbg'], C['pinkbg']],
)
print("All done!")
cd /tmp/workspace/esl-handbooks && python3 make_handbooks_final.py 2>&1
python3 -c " from pypdf import PdfReader import os for f in ['/tmp/workspace/esl-handbooks/Possessive_Case_FINAL.pdf', '/tmp/workspace/esl-handbooks/Articles_FINAL.pdf']: r = PdfReader(f) sz = os.path.getsize(f) print(f'{f.split(\"/\")[-1]}: {len(r.pages)} pages, {sz//1024} KB') # Check page 2 for Russian text txt = r.pages[1].extract_text() print(' Page 2 text:', txt[:300].replace('\n',' | ')) print() "
Чья это ручка?, Это ручка Марка., Притяжательный падеж, Вопросы для разговора and all Cyrillic text extracted perfectly. The fix was switching from the built-in Helvetica font (which cannot render Cyrillic) to DroidSans TTF, which fully supports the Russian alphabet.Possessive Case FINAL
PDF Document · PDF
Articles FINAL
PDF Document · PDF
| Problem | Fix |
|---|---|
| Russian text was invisible (showed as blank/boxes) | The old code used Helvetica - a font with no Cyrillic support |
| Fix applied | Switched to DroidSans TTF font, which fully supports the Russian alphabet |