Please create a colorful handbook pdf from from list of vocabulary words mentioned in attached file ,write list of vocabulary words the list must be with respelling of individual vocabulary words in bracelet and it's meaning in English and later in Russian language for all individual vocabulary words and also add 3 daily life use of vocabulary as examples of sentences with it's Russian translation under English sentences. The vocabulary words should be bit more bigger and in colorful backgrounds so easy to differentiate. Please note you have to only use the vocabulary words given under title " Language notes" on different pages of attached file, don't choose other words from this attached file and please skip that last " language notes" on page 92 with model verbs.

Reading File
Running Command

cd /tmp/workspace/attachments/79eccc15-8a5f-48c4-9b95-0653e65d105a/ && python3 -c " import pdfplumber with pdfplumber.open('DOC-20261105-WA0001.pdf') as p: print(len(p.pages)) for i,pg in enumerate(p.pages): t=pg.extract_text() or '' print('=== PAGE',i+1, len(t)) if 'anguage notes' in t or 'ANGUAGE NOTES' in t: print(t) "

Running Command

cd /tmp/workspace/attachments/79eccc15-8a5f-48c4-9b95-0653e65d105a/ && python3 -c " import pdfplumber with pdfplumber.open('DOC-20261105-WA0001.pdf') as p: for i in (1,4): print('=== PAGE',i+1) print(p.pages[i].extract_text()) "

Running Command

ls /usr/share/fonts/truetype/ 2>/dev/null; fc-list | grep -iE "dejavu|noto|liberation|open" | head -20; mkdir -p /tmp/workspace/vocab-handbook

Running Command

python3 -c "import matplotlib,os;d=os.path.join(matplotlib.get_data_path(),'fonts/ttf');print(d);print([f for f in os.listdir(d) if 'DejaVuSans' in f])"

Writing File

~/vocab-handbook/build.py

import os, matplotlib
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.pdfbase import pdfmetrics
from reportlab.pdfbase.ttfonts import TTFont
from reportlab.lib.colors import HexColor, white
from reportlab.platypus import Paragraph
from reportlab.lib.styles import ParagraphStyle

D = os.path.join(matplotlib.get_data_path(), 'fonts/ttf')
pdfmetrics.registerFont(TTFont('R', f'{D}/DejaVuSans.ttf'))
pdfmetrics.registerFont(TTFont('B', f'{D}/DejaVuSans-Bold.ttf'))
pdfmetrics.registerFont(TTFont('I', f'{D}/DejaVuSans-Oblique.ttf'))
pdfmetrics.registerFont(TTFont('BI', f'{D}/DejaVuSans-BoldOblique.ttf'))
pdfmetrics.registerFontFamily('R', normal='R', bold='B', italic='I', boldItalic='BI')

W, H = A4

# (word, respelling, pos, EN meaning, RU meaning, [(en, ru) x3], color, light)
WORDS = [
 ("twisting my words", "TWIS-ting my WURDZ", "phrase",
  "deliberately misunderstanding or changing what I am saying",
  "намеренно искажать мои слова; передёргивать то, что я сказал(а)",
  [("I never said I hated your cooking. Stop twisting my words!",
    "Я никогда не говорил, что ненавижу твою стряпню. Хватит искажать мои слова!"),
   ("She is twisting my words to make me look bad in front of the boss.",
    "Она передёргивает мои слова, чтобы выставить меня в плохом свете перед начальником."),
   ("You're twisting my words. I only asked if we could eat out less often.",
    "Ты искажаешь мои слова. Я лишь спросил, можем ли мы реже ужинать в ресторанах.")],
  "#E53935", "#FFEBEE"),
 ("got our wires crossed", "GOT OW-er WY-erz KRAWST", "phrase",
  "misunderstood each other",
  "не поняли друг друга; произошла путаница",
  [("I thought the meeting was at ten, but she said eleven. We must have got our wires crossed.",
    "Я думал, что встреча в десять, а она сказала - в одиннадцать. Мы, видимо, не поняли друг друга."),
   ("Sorry I'm late. We got our wires crossed about where to meet.",
    "Извини, что опоздал. Мы запутались, где нам встречаться."),
   ("My husband bought milk and so did I. We got our wires crossed again!",
    "Мой муж купил молоко, и я тоже. Мы опять не договорились!")],
  "#FB8C00", "#FFF3E0"),
 ("that came out all wrong", "THAT kaym OWT awl RAWNG", "phrase",
  "used to apologize for saying something rude or inappropriate",
  "используется, чтобы извиниться за неудачную или грубую фразу: я неправильно выразился(ась)",
  [("I didn't mean you look tired. Sorry, that came out all wrong.",
    "Я не хотел сказать, что ты выглядишь усталой. Прости, это вышло совсем не так."),
   ("Oh no, that came out all wrong! I love your new haircut.",
    "Ой, нет, я совсем не то сказал! Мне очень нравится твоя новая стрижка."),
   ("I wanted to thank you for helping, but that came out all wrong.",
    "Я хотел поблагодарить тебя за помощь, но получилось совсем неудачно.")],
  "#F9A825", "#FFFDE7"),
 ("it's not the end of the world", "its NOT thuh END uv thuh WURLD", "phrase",
  "whatever has upset you is not as important as you think",
  "то, что тебя расстроило, не так важно, как тебе кажется; это не конец света",
  [("You missed the bus? It's not the end of the world. Another one comes in ten minutes.",
    "Ты опоздал на автобус? Это не конец света. Следующий придёт через десять минут."),
   ("I burnt the toast, but it's not the end of the world. I'll make some more.",
    "Я сжёг тосты, но это не конец света. Я приготовлю ещё."),
   ("He failed his driving test, but it's not the end of the world. He can try again.",
    "Он не сдал экзамен по вождению, но это не конец света. Он может попробовать снова.")],
  "#43A047", "#E8F5E9"),
 ("no point crying over spilled milk", "NOH POYNT KRY-ing OH-ver SPILD MILK", "phrase",
  "no reason to worry about things that have already happened and cannot be changed",
  "нет смысла переживать из-за того, что уже случилось и чего не изменить; слезами горю не поможешь",
  [("My phone fell in the water. Oh well, there's no point crying over spilled milk.",
    "Мой телефон упал в воду. Ну что ж, слезами горю не поможешь."),
   ("I forgot to book the tickets, but there's no point crying over spilled milk. Let's find another way.",
    "Я забыл заказать билеты, но нет смысла жалеть об этом. Давай найдём другой вариант."),
   ("You lost your keys yesterday. There's no point crying over spilled milk. Let's call a locksmith.",
    "Ты вчера потерял ключи. Что было, то прошло. Давай вызовем слесаря.")],
  "#00ACC1", "#E0F7FA"),
 ("we're getting nowhere here / we're just going round in circles",
  "weer GET-ing NOH-wair HEER / weer JUST GOH-ing ROWND in SUR-kulz", "phrase",
  "we are repeating the same things without solving anything",
  "мы повторяем одно и то же и ни к чему не приходим; мы ходим по кругу",
  [("We've talked about the holiday for an hour. We're getting nowhere here.",
    "Мы говорим об отпуске уже час. Мы ни к чему не приходим."),
   ("My brother and I keep arguing about money. We're just going round in circles.",
    "Мы с братом постоянно спорим о деньгах. Мы просто ходим по кругу."),
   ("Let's stop for today. We're going round in circles, and we're tired.",
    "Давай на сегодня закончим. Мы ходим по кругу, и мы устали.")],
  "#1E88E5", "#E3F2FD"),
 ("as it happens", "az it HAP-enz", "phrase",
  "actually; as a matter of fact",
  "вообще-то; кстати; как раз так получилось",
  [("Do you like Italian food? As it happens, I'm going to an Italian restaurant tonight.",
    "Ты любишь итальянскую кухню? Как раз так получилось, что сегодня вечером я иду в итальянский ресторан."),
   ("I can't talk long. As it happens, my train leaves in ten minutes.",
    "Я не могу долго говорить. Вообще-то мой поезд отправляется через десять минут."),
   ("'Do you know a good dentist?' 'As it happens, my sister is one.'",
    "«Ты знаешь хорошего стоматолога?» «Кстати, моя сестра как раз стоматолог».")],
  "#5E35B1", "#EDE7F6"),
 ("get a grip of costs", "GET uh GRIP uv KOSTS", "verb + preposition + noun",
  "to minimize costs; to take control of spending",
  "сократить расходы; взять расходы под контроль",
  [("We need to get a grip of costs because we spend too much on food.",
    "Нам нужно взять расходы под контроль, потому что мы тратим слишком много на еду."),
   ("The manager wants to get a grip of costs before the end of the year.",
    "Менеджер хочет сократить расходы до конца года."),
   ("To save for our holiday, we decided to get a grip of costs at home.",
    "Чтобы накопить на отпуск, мы решили контролировать расходы дома.")],
  "#D81B60", "#FCE4EC"),
 ("left, right and centre", "LEFT RYT and SEN-ter", "phrase",
  "on every side; everywhere (in large amounts)",
  "повсюду; во все стороны; направо и налево",
  [("New cafés are opening left, right and centre in our neighbourhood.",
    "В нашем районе повсюду открываются новые кафе."),
   ("He has been spending money left, right and centre since he got the new job.",
    "Он тратит деньги направо и налево с тех пор, как получил новую работу."),
   ("At the sale, people were buying clothes left, right and centre.",
    "На распродаже люди скупали одежду со всех сторон.")],
  "#8E24AA", "#F3E5F5"),
 ("petulant", "PET-yuh-lunt", "adjective",
  "childishly bad-tempered; sulky and easily annoyed",
  "капризный; по-детски раздражительный; обидчивый",
  [("My little brother gets petulant when he doesn't get his own way.",
    "Мой младший брат становится капризным, когда всё идёт не по его."),
   ("Don't be so petulant. It's only a game.",
    "Не будь таким капризным. Это всего лишь игра."),
   ("She gave a petulant answer and slammed the door.",
    "Она капризно ответила и хлопнула дверью.")],
  "#6D4C41", "#EFEBE9"),
]

SRC = "Source: Outcomes Advanced 3rd ed. Teacher's Book, Unit 6 'Conflict and resolution', Language notes (pp. 89 and 91)"


def para(c, text, x, y_top, w, size, font='R', color='#222222', leading=None, align=0):
    st = ParagraphStyle('p', fontName=font, fontSize=size, leading=leading or size * 1.3,
                        textColor=HexColor(color), alignment=align)
    p = Paragraph(text, st)
    _, h = p.wrap(w, 1000)
    p.drawOn(c, x, y_top - h)
    return h


def height(text, w, size, font='R', leading=None):
    st = ParagraphStyle('p', fontName=font, fontSize=size, leading=leading or size * 1.3)
    return Paragraph(text, st).wrap(w, 1000)[1]


def rrect(c, x, y, w, h, r, fill, stroke=None, lw=1):
    c.setFillColor(HexColor(fill))
    if stroke:
        c.setStrokeColor(HexColor(stroke)); c.setLineWidth(lw)
    c.roundRect(x, y, w, h, r, fill=1, stroke=1 if stroke else 0)


def confetti(c, seed, colors):
    import random
    random.seed(seed)
    for _ in range(40):
        col = HexColor(random.choice(colors))
        c.setFillColor(col)
        x, y = random.uniform(0, W), random.uniform(0, H)
        r = random.uniform(3, 9)
        c.saveState()
        c.setFillAlpha(0.18)
        c.circle(x, y, r, fill=1, stroke=0)
        c.restoreState()


ALLC = [w[6] for w in WORDS]


def cover(c):
    c.setFillColor(HexColor('#FFF8E1')); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 1, ALLC)
    # rainbow bands
    bw = W / len(ALLC)
    for i, col in enumerate(ALLC):
        c.setFillColor(HexColor(col)); c.rect(i * bw, H - 28, bw, 28, fill=1, stroke=0)
        c.rect(i * bw, 0, bw, 28, fill=1, stroke=0)
    rrect(c, 45, H - 420, W - 90, 300, 30, '#1E88E5')
    rrect(c, 55, H - 410, W - 110, 280, 24, '#42A5F5')
    c.setFillColor(white); c.setFont('B', 40)
    c.drawCentredString(W / 2, H - 205, "Vocabulary")
    c.drawCentredString(W / 2, H - 258, "Handbook")
    c.setFont('B', 17)
    c.drawCentredString(W / 2, H - 305, "Arguments and Discussions")
    c.setFont('R', 14)
    c.drawCentredString(W / 2, H - 335, "Словарь: споры и обсуждения")
    c.setFont('R', 12)
    c.drawCentredString(W / 2, H - 375, "English  +  respelling  +  Русский перевод  +  3 examples")
    # colored chips
    y = H - 470
    for i, wd in enumerate(WORDS):
        col = wd[6]
        cx = 60 if i % 2 == 0 else W / 2 + 5
        cy = y - (i // 2) * 52
        rrect(c, cx, cy - 36, W / 2 - 65, 42, 14, col)
        c.setFillColor(white)
        nm = wd[0] if len(wd[0]) < 28 else "we're getting nowhere here..."
        fs = 12.5 if len(nm) < 24 else 10.5
        c.setFont('B', fs)
        c.drawCentredString(cx + (W / 2 - 65) / 2, cy - 20, nm)
    c.setFillColor(HexColor('#555555')); c.setFont('R', 8.5)
    c.drawCentredString(W / 2, 42, SRC)
    c.showPage()


def contents(c):
    c.setFillColor(HexColor('#F3F8FF')); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 2, ALLC)
    rrect(c, 40, H - 110, W - 80, 70, 20, '#5E35B1')
    c.setFillColor(white); c.setFont('B', 26)
    c.drawCentredString(W / 2, H - 85, "Word List  /  Список слов")
    y = H - 140
    for i, wd in enumerate(WORDS):
        col, light = wd[6], wd[7]
        h = 62
        rrect(c, 40, y - h, W - 80, h, 14, light, col, 2)
        rrect(c, 40, y - h, 52, h, 14, col)
        c.setFillColor(white); c.setFont('B', 22)
        c.drawCentredString(66, y - h / 2 - 8, str(i + 1))
        nm = wd[0]
        fs = 16 if len(nm) < 30 else 11.5
        para(c, f"<b>{nm}</b>", 105, y - 8, W - 190, fs, 'B', col, leading=fs * 1.25)
        hh = height(f"<b>{nm}</b>", W - 190, fs, 'B', fs * 1.25)
        para(c, f"({wd[1]})", 105, y - 10 - hh, W - 190, 9.5, 'I', '#444444')
        c.setFillColor(HexColor('#444444')); c.setFont('R', 10)
        c.drawRightString(W - 52, y - h + 10, f"p. {i + 3}")
        y -= h + 8
    c.setFillColor(HexColor('#555555')); c.setFont('R', 8.5)
    c.drawCentredString(W / 2, 28, SRC)
    c.showPage()


def word_page(c, idx, wd):
    word, resp, pos, en, ru, ex, col, light = wd
    c.setFillColor(HexColor(light)); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 10 + idx, [col])
    # banner
    bx, bw = 30, W - 60
    wsize = 38 if len(word) < 20 else (30 if len(word) < 30 else 25)
    # word text height
    wh = height(f"<b>{word}</b>", bw - 60, wsize, 'B', wsize * 1.2)
    rh = height(f"({resp})", bw - 60, 16, 'B', 20)
    banner_h = wh + rh + 70
    top = H - 30
    rrect(c, bx, top - banner_h, bw, banner_h, 28, col)
    # number badge
    c.setFillColor(white); c.circle(bx + 34, top - 34, 20, fill=1, stroke=0)
    c.setFillColor(HexColor(col)); c.setFont('B', 20)
    c.drawCentredString(bx + 34, top - 41, str(idx + 1))
    # pos tag
    c.setFillColor(white); c.setFont('I', 11)
    c.drawRightString(bx + bw - 25, top - 38, pos)
    y = top - 56
    h1 = para(c, f"<b>{word}</b>", bx + 30, y, bw - 60, wsize, 'B', '#FFFFFF', wsize * 1.2, align=1)
    y -= h1 + 6
    rrect(c, bx + 30, y - rh - 8, bw - 60, rh + 10, 12, '#FFFFFF')
    para(c, f"({resp})", bx + 40, y - 4, bw - 80, 16, 'B', col, 20, align=1)
    y = top - banner_h - 18

    def box(label_en, label_ru, text, y, font='R'):
        th = height(text, bw - 50, 13.5, font, 18)
        h = th + 42
        rrect(c, bx, y - h, bw, h, 16, '#FFFFFF', col, 2.5)
        rrect(c, bx + 14, y - 8 - 6, 0, 0, 0, '#FFFFFF')
        c.setFillColor(HexColor(col)); c.roundRect(bx + 16, y - 24, 118, 18, 9, fill=1, stroke=0)
        c.setFillColor(white); c.setFont('B', 10)
        c.drawCentredString(bx + 75, y - 18, label_en)
        para(c, text, bx + 25, y - 30, bw - 50, 13.5, font, '#222222', 18)
        return y - h - 12

    y = box("MEANING", "", en, y)
    y = box("ЗНАЧЕНИЕ", "", ru, y)
    # examples
    c.setFillColor(HexColor(col)); c.setFont('B', 14)
    c.drawString(bx + 4, y - 14, "Daily-life examples  /  Примеры из жизни")
    y -= 24
    for k, (e, r) in enumerate(ex):
        eh = height(e, bw - 70, 12.5, 'B', 16)
        rh2 = height(r, bw - 70, 11.5, 'R', 15)
        h = eh + rh2 + 26
        rrect(c, bx, y - h, bw, h, 14, '#FFFFFF', col, 1.5)
        c.setFillColor(HexColor(col)); c.circle(bx + 24, y - h / 2, 14, fill=1, stroke=0)
        c.setFillColor(white); c.setFont('B', 14)
        c.drawCentredString(bx + 24, y - h / 2 - 5, str(k + 1))
        para(c, e, bx + 50, y - 10, bw - 70, 12.5, 'B', '#222222', 16)
        para(c, r, bx + 50, y - 14 - eh, bw - 70, 11.5, 'R', col, 15)
        y -= h + 9
    c.setFillColor(HexColor('#777777')); c.setFont('R', 8)
    c.drawCentredString(W / 2, 20, f"Vocabulary Handbook  |  page {idx + 3}")
    assert y > 30, (word, y)
    c.showPage()


def review(c):
    c.setFillColor(HexColor('#FFFDE7')); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 99, ALLC)
    rrect(c, 40, H - 110, W - 80, 70, 20, '#E53935')
    c.setFillColor(white); c.setFont('B', 22)
    c.drawCentredString(W / 2, H - 82, "Quick Review  /  Быстрое повторение")
    y = H - 130
    for i, wd in enumerate(WORDS):
        col, light = wd[6], wd[7]
        nm = wd[0]
        ru = wd[4].split(';')[0]
        fs = 13 if len(nm) < 30 else 9.5
        th = height(f"<b>{nm}</b>", 200, fs, 'B', fs * 1.2)
        rt = height(ru, 235, 10.5, 'R', 13.5)
        h = max(th, rt) + 18
        rrect(c, 40, y - h, W - 80, h, 12, col)
        rrect(c, 250, y - h + 4, W - 80 - 214, h - 8, 9, '#FFFFFF')
        para(c, f"<b>{nm}</b>", 52, y - 9, 190, fs, 'B', '#FFFFFF', fs * 1.2)
        para(c, ru, 258, y - 9, 235, 10.5, 'R', col, 13.5)
        y -= h + 7
    c.setFillColor(HexColor('#555555')); c.setFont('R', 9)
    c.drawCentredString(W / 2, 50, "Tip: say each example aloud, then cover the Russian line and translate it back.")
    c.drawCentredString(W / 2, 36, "Совет: прочитайте пример вслух, закройте перевод и переведите его сами.")
    c.showPage()


out = '/tmp/workspace/vocab-handbook/Vocabulary_Handbook_Arguments_EN_RU.pdf'
c = canvas.Canvas(out, pagesize=A4)
c.setTitle("Vocabulary Handbook: Arguments and Discussions (EN-RU)")
cover(c); contents(c)
for i, wd in enumerate(WORDS):
    word_page(c, i, wd)
review(c)
c.save()
print(out)
Running Command

cd /tmp/workspace/vocab-handbook && python3 build.py

Running Command

ls -la /tmp/workspace/ /tmp/workspace/vocab-handbook/

The file exists now (the two calls ran in parallel). Running the build.
Running Command

cd /tmp/workspace/vocab-handbook && python3 build.py && python3 -c " import pypdf;r=pypdf.PdfReader('Vocabulary_Handbook_Arguments_EN_RU.pdf');print(len(r.pages))"

Running Command

cd /tmp/workspace/vocab-handbook && python3 -c " try: import fitz d=fitz.open('Vocabulary_Handbook_Arguments_EN_RU.pdf') for i in (0,1,7,12): d[i].get_pixmap(dpi=60).save(f'p{i}.png') print('fitz ok') except Exception as e: print('fitz',e) import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Arguments_EN_RU.pdf') for i in (0,1,7,12): d[i].render(scale=0.9).to_pil().save(f'p{i}.png') print('pdfium ok') "; ls

Reading File
Reading File
Reading File
Running Command

cd /tmp/workspace/vocab-handbook && python3 - <<'E' s=open('build.py').read() # cover chips s=s.replace(''' nm = wd[0] if len(wd[0]) < 28 else "we're getting nowhere here..." fs = 12.5 if len(nm) < 24 else 10.5 c.setFont('B', fs)''',''' nm = wd[0] if len(wd[0]) < 40 else "getting nowhere / going round in circles" fs = 12.5 while pdfmetrics.stringWidth(nm, 'B', fs) > W / 2 - 85: fs -= 0.5 c.setFont('B', fs)''') # word sizes s=s.replace("wsize = 38 if len(word) < 20 else (30 if len(word) < 30 else 25)","wsize = 46 if len(word) < 14 else (40 if len(word) < 24 else (32 if len(word) < 40 else 27))") s=s.replace("th = height(text, bw - 50, 13.5, font, 18)","th = height(text, bw - 50, 15, font, 20)") s=s.replace("para(c, text, bx + 25, y - 30, bw - 50, 13.5, font, '#222222', 18)","para(c, text, bx + 25, y - 30, bw - 50, 15, font, '#222222', 20)") s=s.replace("height(e, bw - 70, 12.5, 'B', 16)","height(e, bw - 70, 14, 'B', 18)") s=s.replace("height(r, bw - 70, 11.5, 'R', 15)","height(r, bw - 70, 12.5, 'R', 16)") s=s.replace("para(c, e, bx + 50, y - 10, bw - 70, 12.5, 'B', '#222222', 16)","para(c, e, bx + 50, y - 10, bw - 70, 14, 'B', '#222222', 18)") s=s.replace("para(c, r, bx + 50, y - 14 - eh, bw - 70, 11.5, 'R', col, 15)","para(c, r, bx + 50, y - 14 - eh, bw - 70, 12.5, 'R', '#1a1a1a', 16)") s=s.replace("h = eh + rh2 + 26","h = eh + rh2 + 30") s=s.replace("rrect(c, bx, y - h, bw, h, 14, '#FFFFFF', col, 1.5)","rrect(c, bx, y - h, bw, h, 14, '#FFFFFF', col, 2)") # review russian dark s=s.replace("para(c, ru, 258, y - 9, 235, 10.5, 'R', col, 13.5)","para(c, ru, 258, y - 9, 235, 10.5, 'R', '#1a1a1a', 13.5)") open('build.py','w').write(s) E python3 build.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Arguments_EN_RU.pdf') for i in (0,2,6,7,10): d[i].render(scale=0.9).to_pil().save(f'p{i}.png') "

Reading File
Reading File
Reading File
Running Command

cd /tmp/workspace/vocab-handbook && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Arguments_EN_RU.pdf') d[1].render(scale=0.9).to_pil().save('c1.png')"

Reading File
Running Command

cd /tmp/workspace/vocab-handbook && sed -i 's/ h = 62$/ h = 57/; s/ y -= h + 8$/ y -= h + 6/; s/ y = H - 140$/ y = H - 128/' build.py && python3 build.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Arguments_EN_RU.pdf') d[1].render(scale=0.9).to_pil().save('c1.png')"

Reading File
Your handbook is ready:

Vocabulary Handbook Arguments EN RU

PDF Document · PDF

. It has 13 A4 pages.
What's in it
  • Cover: a colourful title page with all 10 entries shown as coloured chips.
  • Word list: a contents page where each entry has its own colour and page number.
  • Pages 3 to 12: one page per entry, each in its own colour.
    • A large coloured banner with the entry, its respelling in brackets, and its part of speech.
    • Meaning in English, then Значение in Russian.
    • 3 daily-life example sentences, each with the Russian translation under it.
  • Page 13: a quick-review page with each entry and its short Russian meaning.
Entries used These come only from the "Language notes" on page 89 and page 91 of your file:
  1. twisting my words
  2. got our wires crossed
  3. that came out all wrong
  4. it's not the end of the world
  5. no point crying over spilled milk
  6. we're getting nowhere here / we're just going round in circles
  7. as it happens
  8. get a grip of costs
  9. left, right and centre
  10. petulant
  • Skipped notes: the "Language notes" on page 92 (modal verbs, ellipsis and substitution) are left out, as you asked. Pages 90 and 93 have no vocabulary "Language notes".
  • Entry 6: the book groups its two phrases together, so I kept them as one entry.
  • Respellings: these are simple English-style respellings that I wrote myself, since the file doesn't give pronunciation. Check them if you need a specific accent.
  • Example sentences: the sentences and Russian translations are mine, not from the file. Please read the Russian over if you plan to teach from it.

Please recreate the same colorful pdf with address vocabulary words as follows , Briny , comical, perilous, idle vs ideal, profound.

Writing File

~/vocab-handbook/build2.py

import re
old = open('/tmp/workspace/vocab-handbook/build.py').read()
head = old.split("# (word, respelling")[0]                      # imports + fonts
helpers = old[old.index("SRC ="):old.index("ALLC =")]          # para/height/rrect/confetti
wp = old[old.index("def word_page"):old.index("def review")]   # word page function
wp = wp.replace("def word_page(c, idx, wd):", "def word_page(c, idx, wd, pg):").replace("page {idx + 3}", "page {pg}")
exec(head + helpers.replace(
    "SRC = \"Source: Outcomes Advanced 3rd ed. Teacher's Book, Unit 6 'Conflict and resolution', Language notes (pp. 89 and 91)\"",
    "SRC = 'Vocabulary Handbook  |  English + respelling + Русский перевод'"))
exec(wp)

WORDS = [
 ("briny", "BRY-nee", "adjective",
  "very salty, like sea water",
  "очень солёный, как морская вода; солёный (рассольный)",
  [("The sea air was briny, and I could taste salt on my lips.",
    "Морской воздух был солёным, и я чувствовал соль на губах."),
   ("We ate briny olives with our bread at lunch.",
    "На обед мы ели солёные оливки с хлебом."),
   ("Don't drink the briny water at the beach. It will make you thirstier.",
    "Не пей солёную воду на пляже. От неё захочется пить ещё сильнее.")],
  "#00ACC1", "#E0F7FA"),
 ("comical", "KOM-ih-kul", "adjective",
  "funny in a way that makes people laugh, often because it looks silly",
  "смешной, забавный; вызывающий смех (часто из-за нелепого вида)",
  [("My cat's attempt to catch the laser dot was so comical that we all laughed.",
    "Попытка моего кота поймать лазерную точку была такой смешной, что мы все засмеялись."),
   ("He made a comical face to cheer up the baby.",
    "Он скорчил забавную рожицу, чтобы развеселить малыша."),
   ("It looked comical when the tall man sat on the tiny child's chair.",
    "Выглядело комично, когда высокий мужчина сел на крошечный детский стульчик.")],
  "#FB8C00", "#FFF3E0"),
 ("perilous", "PER-ih-lus", "adjective",
  "full of danger or risk",
  "опасный; рискованный; полный опасностей",
  [("The road over the mountain is narrow and perilous in winter.",
    "Горная дорога зимой узкая и опасная."),
   ("Crossing the river on a broken bridge is perilous.",
    "Переходить реку по сломанному мосту очень опасно."),
   ("The fisherman made a perilous journey home through the storm.",
    "Рыбак совершил опасное путешествие домой сквозь шторм.")],
  "#E53935", "#FFEBEE"),
 ("idle", "Y-dul", "adjective",
  "not working or not being used; doing nothing; (of talk) without real purpose",
  "праздный, бездельничающий; не работающий, простаивающий; (о разговорах) пустой",
  [("Don't leave the car engine idle for too long. It wastes petrol.",
    "Не оставляй двигатель автомобиля надолго работать вхолостую. Это впустую тратит бензин."),
   ("On Sunday afternoon I like to sit idle in the garden and do nothing.",
    "В воскресенье днём я люблю сидеть в саду без дела и ничего не делать."),
   ("It was just idle talk. He never really planned to move.",
    "Это были просто пустые разговоры. Он на самом деле не собирался переезжать.")],
  "#43A047", "#E8F5E9"),
 ("ideal", "y-DEE-ul", "adjective / noun",
  "perfect or the best possible for a purpose; a perfect example or standard (noun)",
  "идеальный, наилучший, самый подходящий; идеал (сущ.)",
  [("This quiet flat is ideal for students.",
    "Эта тихая квартира идеально подходит для студентов."),
   ("Sunny weather is ideal for a picnic in the park.",
    "Солнечная погода идеальна для пикника в парке."),
   ("She is the ideal person to ask for cooking advice.",
    "Она идеальный человек, у которого можно спросить совета по кулинарии.")],
  "#1E88E5", "#E3F2FD"),
 ("profound", "pruh-FOWND", "adjective",
  "very deep or strong; showing great thought or understanding",
  "глубокий; сильный; проникновенный; выражающий глубокую мысль",
  [("The film had a profound effect on me. I thought about it for days.",
    "Фильм оказал на меня глубокое влияние. Я думал о нём несколько дней."),
   ("She felt profound sadness when her old friend moved away.",
    "Она почувствовала глубокую грусть, когда её старый друг уехал."),
   ("He asked a profound question that made everyone stop and think.",
    "Он задал глубокий вопрос, который заставил всех остановиться и задуматься.")],
  "#8E24AA", "#F3E5F5"),
]
ALLC = [w[6] for w in WORDS]
# page numbers: cover 1, contents 2, briny 3, comical 4, perilous 5, idle 6, ideal 7, compare 8, profound 9, review 10
PAGES = [3, 4, 5, 6, 7, 9]
CMP = 8
CMPCOL, CMPLIGHT = '#D81B60', '#FCE4EC'


def cover(c):
    c.setFillColor(HexColor('#FFF8E1')); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 1, ALLC)
    bw = W / len(ALLC)
    for i, col in enumerate(ALLC):
        c.setFillColor(HexColor(col)); c.rect(i * bw, H - 28, bw, 28, fill=1, stroke=0)
        c.rect(i * bw, 0, bw, 28, fill=1, stroke=0)
    rrect(c, 45, H - 420, W - 90, 300, 30, '#8E24AA')
    rrect(c, 55, H - 410, W - 110, 280, 24, '#AB47BC')
    c.setFillColor(white); c.setFont('B', 40)
    c.drawCentredString(W / 2, H - 205, "Vocabulary")
    c.drawCentredString(W / 2, H - 258, "Handbook")
    c.setFont('B', 17)
    c.drawCentredString(W / 2, H - 305, "Words in Context")
    c.setFont('R', 14)
    c.drawCentredString(W / 2, H - 335, "Слова в контексте")
    c.setFont('R', 12)
    c.drawCentredString(W / 2, H - 375, "English  +  respelling  +  Русский перевод  +  3 examples")
    y = H - 480
    cw = W / 2 - 65
    for i, wd in enumerate(WORDS):
        cx = 60 if i % 2 == 0 else W / 2 + 5
        cy = y - (i // 2) * 74
        rrect(c, cx, cy - 56, cw, 62, 18, wd[6])
        c.setFillColor(white); c.setFont('B', 22)
        c.drawCentredString(cx + cw / 2, cy - 36, wd[0])
    c.setFillColor(HexColor('#555555')); c.setFont('R', 9)
    c.drawCentredString(W / 2, 42, "briny  |  comical  |  perilous  |  idle vs ideal  |  profound")
    c.showPage()


def contents(c):
    c.setFillColor(HexColor('#F3F8FF')); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 2, ALLC)
    rrect(c, 40, H - 110, W - 80, 70, 20, '#5E35B1')
    c.setFillColor(white); c.setFont('B', 26)
    c.drawCentredString(W / 2, H - 85, "Word List  /  Список слов")
    rows = []
    for i, wd in enumerate(WORDS):
        rows.append((wd[0], wd[1], wd[6], wd[7], PAGES[i]))
        if wd[0] == 'ideal':
            rows.append(("idle vs ideal", "compare the two words / сравнение", CMPCOL, CMPLIGHT, CMP))
    y = H - 140
    n = 0
    for name, resp, col, light, pg in rows:
        h = 70
        rrect(c, 40, y - h, W - 80, h, 14, light, col, 2)
        rrect(c, 40, y - h, 56, h, 14, col)
        c.setFillColor(white); c.setFont('B', 22)
        is_cmp = name == "idle vs ideal"
        if not is_cmp:
            n += 1
        c.drawCentredString(68, y - h / 2 - 8, "VS" if is_cmp else str(n))
        c.setFillColor(HexColor(col)); c.setFont('B', 24)
        c.drawString(115, y - 32, name)
        c.setFillColor(HexColor('#444444')); c.setFont('I', 12)
        c.drawString(115, y - 52, resp if is_cmp else f"({resp})")
        c.setFont('R', 11)
        c.drawRightString(W - 55, y - h + 12, f"p. {pg}")
        y -= h + 12
    c.setFillColor(HexColor('#555555')); c.setFont('R', 8.5)
    c.drawCentredString(W / 2, 28, SRC)
    c.showPage()


def compare(c):
    c.setFillColor(HexColor(CMPLIGHT)); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 55, ['#43A047', '#1E88E5', CMPCOL])
    rrect(c, 30, H - 110, W - 60, 80, 28, CMPCOL)
    c.setFillColor(white); c.setFont('B', 38)
    c.drawCentredString(W / 2, H - 78, "idle  vs  ideal")
    c.setFont('R', 12)
    c.drawCentredString(W / 2, H - 98, "Похожие по звучанию, но разные по смыслу")
    # two cards
    cw = (W - 60 - 16) / 2
    top = H - 130
    cards = [(WORDS[3], 30), (WORDS[4], 30 + cw + 16)]
    ch = 250
    for wd, x in cards:
        col, light = wd[6], wd[7]
        rrect(c, x, top - ch, cw, ch, 20, '#FFFFFF', col, 3)
        rrect(c, x, top - 70, cw, 70, 20, col)
        c.setFillColor(white); c.setFont('B', 34)
        c.drawCentredString(x + cw / 2, top - 48, wd[0])
        para(c, f"({wd[1]})", x + 10, top - 82, cw - 20, 15, 'B', col, 19, align=1)
        short_en = {"idle": "doing nothing; not in use", "ideal": "perfect; the best possible"}[wd[0]]
        short_ru = {"idle": "бездельничающий; не используемый; пустой", "ideal": "идеальный; наилучший"}[wd[0]]
        hh = para(c, short_en, x + 12, top - 112, cw - 24, 13.5, 'B', '#222222', 18, align=1)
        para(c, short_ru, x + 12, top - 118 - hh, cw - 24, 12.5, 'R', '#1a1a1a', 16, align=1)
        ex = {"idle": ("The machine sat idle all week.", "Станок простаивал всю неделю."),
              "ideal": ("It was the ideal place for a holiday.", "Это было идеальное место для отпуска.")}[wd[0]]
        yy = top - 180
        rrect(c, x + 10, yy - 58, cw - 20, 66, 12, light)
        hh = para(c, ex[0], x + 18, yy, cw - 36, 12, 'I', '#222222', 15, align=1)
        para(c, ex[1], x + 18, yy - hh - 3, cw - 36, 11, 'R', '#1a1a1a', 14, align=1)
    y = top - ch - 20
    # memory tips
    tips = [("Spelling / Написание", "idle = 4 letters + e at the end, 2 syllables (Y-dul).  ideal = 5 letters, 3 syllables (y-DEE-ul).",
             "idle: 2 слога (Y-dul); ideal: 3 слога (y-DEE-ul). В ideal есть буквы e-a."),
            ("Memory trick / Подсказка", "ideal has 'idea' inside it: an ideal is the best idea. idle = nothing happens.",
             "В ideal спрятано слово idea («идея»): идеал - лучшая идея. Idle - ничего не происходит.")]
    for t, en, ru in tips:
        eh = height(en, W - 100, 12.5, 'R', 16); rh = height(ru, W - 100, 11.5, 'R', 15)
        h = eh + rh + 44
        rrect(c, 30, y - h, W - 60, h, 16, '#FFFFFF', CMPCOL, 2.5)
        c.setFillColor(HexColor(CMPCOL)); c.setFont('B', 12)
        c.drawString(45, y - 20, t)
        para(c, en, 45, y - 28, W - 100, 12.5, 'R', '#222222', 16)
        para(c, ru, 45, y - 32 - eh, W - 100, 11.5, 'R', '#1a1a1a', 15)
        y -= h + 12
    # quiz
    c.setFillColor(HexColor(CMPCOL)); c.setFont('B', 14)
    c.drawString(34, y - 12, "Quick quiz: idle or ideal?  /  Мини-тест")
    y -= 22
    quiz = [("1. The workers were ______ because there was no job to do.", "Рабочие бездельничали, потому что не было работы.", "idle"),
            ("2. This is the ______ spot for a picnic.", "Это идеальное место для пикника.", "ideal"),
            ("3. The shop's machines stay ______ at night.", "Станки магазина ночью простаивают.", "idle")]
    for q, r, a in quiz:
        qh = height(q, W - 220, 12, 'B', 15)
        h = qh + 14 + 15 + 8
        rrect(c, 30, y - h, W - 60, h, 12, '#FFFFFF', CMPCOL, 1.5)
        para(c, q, 42, y - 8, W - 220, 12, 'B', '#222222', 15)
        para(c, r, 42, y - 10 - qh, W - 220, 10.5, 'R', '#1a1a1a', 14)
        rrect(c, W - 170, y - h + 8, 130, h - 16, 10, '#43A047' if a == 'idle' else '#1E88E5')
        c.setFillColor(white); c.setFont('B', 11)
        c.drawCentredString(W - 105, y - h / 2 - 4, f"Answer: {a}")
        y -= h + 8
    assert y > 30, y
    c.setFillColor(HexColor('#777777')); c.setFont('R', 8)
    c.drawCentredString(W / 2, 18, f"Vocabulary Handbook  |  page {CMP}")
    c.showPage()


def review(c):
    c.setFillColor(HexColor('#FFFDE7')); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 99, ALLC)
    rrect(c, 40, H - 110, W - 80, 70, 20, '#E53935')
    c.setFillColor(white); c.setFont('B', 22)
    c.drawCentredString(W / 2, H - 82, "Quick Review  /  Быстрое повторение")
    y = H - 140
    for wd in WORDS:
        col = wd[6]
        h = 64
        rrect(c, 40, y - h, W - 80, h, 14, col)
        c.setFillColor(white); c.setFont('B', 24)
        c.drawString(56, y - 30, wd[0])
        c.setFont('I', 11)
        c.drawString(56, y - 50, f"({wd[1]})")
        rrect(c, 235, y - h + 6, W - 80 - 205, h - 12, 10, '#FFFFFF')
        para(c, wd[3], 245, y - 10, W - 80 - 225, 10.5, 'B', '#222222', 13)
        eh = height(wd[3], W - 80 - 225, 10.5, 'B', 13)
        para(c, wd[4].split(';')[0], 245, y - 12 - eh, W - 80 - 225, 10.5, 'R', col, 13)
        y -= h + 9
    c.setFillColor(HexColor('#555555')); c.setFont('R', 9)
    c.drawCentredString(W / 2, 50, "Tip: say each example aloud, then cover the Russian line and translate it back.")
    c.drawCentredString(W / 2, 36, "Совет: прочитайте пример вслух, закройте перевод и переведите его сами.")
    c.showPage()


out = '/tmp/workspace/vocab-handbook/Vocabulary_Handbook_Briny_Comical_Perilous_Idle_Ideal_Profound.pdf'
c = canvas.Canvas(out, pagesize=A4)
c.setTitle("Vocabulary Handbook: briny, comical, perilous, idle vs ideal, profound (EN-RU)")
cover(c); contents(c)
for i, wd in enumerate(WORDS):
    word_page(c, i, wd, PAGES[i])
    if wd[0] == 'ideal':
        compare(c)
review(c)
c.save()
print(out)
Running Command

cd /tmp/workspace/vocab-handbook && python3 build2.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Briny_Comical_Perilous_Idle_Ideal_Profound.pdf') print(len(d)) for i in (0,1,2,7,9): d[i].render(scale=0.9).to_pil().save(f'n{i}.png')"

Running Command

cd /tmp/workspace/vocab-handbook && python3 - <<'E' s=open('build2.py').read() s=s.replace(" top = H - 130\n cards"," top = H - 122\n cards") s=s.replace("rrect(c, 30, H - 110, W - 60, 80, 28, CMPCOL)\n c.setFillColor(white); c.setFont('B', 38)\n c.drawCentredString(W / 2, H - 78","rrect(c, 30, H - 102, W - 60, 72, 26, CMPCOL)\n c.setFillColor(white); c.setFont('B', 36)\n c.drawCentredString(W / 2, H - 72") s=s.replace("c.drawCentredString(W / 2, H - 98, \"Похожие","c.drawCentredString(W / 2, H - 91, \"Похожие") s=s.replace("y = top - ch - 20","y = top - ch - 12") s=s.replace("h = eh + rh + 44","h = eh + rh + 38") s=s.replace("y -= h + 12\n # quiz","y -= h + 8\n # quiz") s=s.replace("h = qh + 14 + 15 + 8","h = qh + 12 + 14 + 4") s=s.replace("ch = 250","ch = 246") open('build2.py','w').write(s) E python3 build2.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Briny_Comical_Perilous_Idle_Ideal_Profound.pdf') print(len(d)) for i in (0,1,2,7,9): d[i].render(scale=0.9).to_pil().save(f'n{i}.png')"

Reading File
Reading File
Reading File
Reading File
Running Command

cd /tmp/workspace/vocab-handbook && python3 - <<'E' s=open('build2.py').read() s=s.replace("idle = 4 letters + e at the end, 2 syllables (Y-dul). ideal = 5 letters, 3 syllables (y-DEE-ul).","idle = 4 letters, 2 syllables (Y-dul). ideal = 5 letters, 3 syllables (y-DEE-ul).") s=s.replace("'R', col, 13)","'R', '#1a1a1a', 13)") open('build2.py','w').write(s) E python3 build2.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Briny_Comical_Perilous_Idle_Ideal_Profound.pdf') d[5].render(scale=0.9).to_pil().save('n5.png')"

Reading File
Running Command

cd /tmp/workspace/vocab-handbook && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Briny_Comical_Perilous_Idle_Ideal_Profound.pdf') print(len(d)) d[5].render(scale=0.9).to_pil().save('n5.png')" && ls

Reading File
The new handbook is ready:

Vocabulary Handbook Briny Comical Perilous Idle Ideal Profound

PDF Document · PDF

. It has 10 pages and the same colourful design as the first one.
Pages
  1. Cover, with each word in its own colour.
  2. Word list with respellings and page numbers.
  3. briny (BRY-nee)
  4. comical (KOM-ih-kul)
  5. perilous (PER-ih-lus)
  6. idle (Y-dul)
  7. ideal (y-DEE-ul)
  8. idle vs ideal, a comparison page (added by me)
  9. profound (pruh-FOWND)
  10. Quick review of all words, with the Russian meanings.
  • Word pages: each one has a large coloured banner with the word and its respelling in brackets. Below it are the meaning in English and in Russian, then 3 daily-life sentences with the Russian translation under each.
  • idle vs ideal: this page puts the two words side by side with their respellings, meanings and one example each. It also has a spelling and syllable tip, a memory trick, and a 3-question "idle or ideal?" quiz with answers. Only 2 of the 3 quiz sentences use Russian translations written by me that you should check. All three quiz questions carry a Russian line.
  • Respellings, sentences and translations: I wrote these myself, so check them if you plan to use the handbook for teaching.
  • Wording choices: I used "idle" in its main senses (doing nothing, not in use, and idle talk). I listed "ideal" as both an adjective and a noun, and "briny" as "very salty, like sea water".

Just add those new words in the same fashion like other vocabulary words already there in colorful handbook pdf.

Writing File
old = open('/tmp/workspace/vocab-handbook/build.py').read()
new = open('/tmp/workspace/vocab-handbook/build2.py').read()
head = old.split("# (word, respelling")[0]
helpers = old[old.index("SRC ="):old.index("ALLC =")]
wp = old[old.index("def word_page"):old.index("def review")]
wp = wp.replace("def word_page(c, idx, wd):", "def word_page(c, idx, wd, pg):").replace("page {idx + 3}", "page {pg}")
exec(head + helpers)
exec(wp)
ns = {}
w1 = old[old.index("WORDS = ["):old.index("SRC =")]
w2 = new[new.index("WORDS = ["):new.index("ALLC =")]
exec(w1, ns); OLDW = ns['WORDS']
exec(w2, ns); NEWW = ns['WORDS']
WORDS = OLDW + NEWW            # 16 entries
ALLC = [w[6] for w in WORDS]
SRC = "Vocabulary Handbook  |  English + respelling + Русский перевод"
CMPCOL, CMPLIGHT = '#D81B60', '#FCE4EC'

# page plan
PAGES = {}
p = 4
for i in range(len(WORDS)):
    PAGES[i] = p
    p += 1
    if WORDS[i][0] == 'ideal':
        CMP = p
        p += 1
REVIEW_START = p
IDLE_I = [w[0] for w in WORDS].index('idle')
IDEAL_I = [w[0] for w in WORDS].index('ideal')


def short(nm):
    return nm if len(nm) < 40 else "getting nowhere / going round in circles"


def cover(c):
    c.setFillColor(HexColor('#FFF8E1')); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 1, ALLC)
    bw = W / len(ALLC)
    for i, col in enumerate(ALLC):
        c.setFillColor(HexColor(col)); c.rect(i * bw, H - 28, bw, 28, fill=1, stroke=0)
        c.rect(i * bw, 0, bw, 28, fill=1, stroke=0)
    rrect(c, 45, H - 330, W - 90, 260, 30, '#1E88E5')
    rrect(c, 55, H - 320, W - 110, 240, 24, '#42A5F5')
    c.setFillColor(white); c.setFont('B', 40)
    c.drawCentredString(W / 2, H - 150, "Vocabulary")
    c.drawCentredString(W / 2, H - 203, "Handbook")
    c.setFont('B', 16)
    c.drawCentredString(W / 2, H - 245, "Arguments, Discussions and Words in Context")
    c.setFont('R', 13)
    c.drawCentredString(W / 2, H - 270, "Словарь: споры, обсуждения и слова в контексте")
    c.setFont('R', 11)
    c.drawCentredString(W / 2, H - 298, "English  +  respelling  +  Русский перевод  +  3 examples")
    cw = W / 2 - 60
    y = H - 360
    for i, wd in enumerate(WORDS):
        cx = 45 if i % 2 == 0 else W / 2 + 15
        cy = y - (i // 2) * 52
        rrect(c, cx, cy - 40, cw, 44, 14, wd[6])
        c.setFillColor(white)
        nm = short(wd[0]); fs = 15
        while pdfmetrics.stringWidth(nm, 'B', fs) > cw - 16: fs -= 0.5
        c.setFont('B', fs)
        c.drawCentredString(cx + cw / 2, cy - 25, nm)
    c.setFillColor(HexColor('#555555')); c.setFont('R', 9)
    c.drawCentredString(W / 2, 42, "16 words and phrases  |  16 слов и выражений")
    c.showPage()


def rows_list():
    rows, n = [], 0
    for i, wd in enumerate(WORDS):
        n += 1
        rows.append((str(n), wd[0], wd[1], wd[6], wd[7], PAGES[i], False))
        if wd[0] == 'ideal':
            rows.append(("VS", "idle vs ideal", "compare the two words / сравнение", CMPCOL, CMPLIGHT, CMP, True))
    return rows


def contents(c):
    rows = rows_list()
    chunks = [rows[:10], rows[10:]]
    for ci, chunk in enumerate(chunks):
        c.setFillColor(HexColor('#F3F8FF')); c.rect(0, 0, W, H, fill=1, stroke=0)
        confetti(c, 2 + ci, ALLC)
        rrect(c, 40, H - 110, W - 80, 70, 20, '#5E35B1')
        c.setFillColor(white); c.setFont('B', 26)
        c.drawCentredString(W / 2, H - 85, "Word List  /  Список слов" + ("" if ci == 0 else "  (2)"))
        y = H - 128
        for num, name, resp, col, light, pg, is_cmp in chunk:
            h = 57
            rrect(c, 40, y - h, W - 80, h, 14, light, col, 2)
            rrect(c, 40, y - h, 52, h, 14, col)
            c.setFillColor(white); c.setFont('B', 20 if is_cmp else 22)
            c.drawCentredString(66, y - h / 2 - 8, num)
            fs = 16 if len(name) < 30 else 11.5
            t = f"<b>{name}</b>"
            para(c, t, 105, y - 8, W - 190, fs, 'B', col, leading=fs * 1.25)
            hh = height(t, W - 190, fs, 'B', fs * 1.25)
            para(c, resp if is_cmp else f"({resp})", 105, y - 10 - hh, W - 190, 9.5, 'I', '#444444')
            c.setFillColor(HexColor('#444444')); c.setFont('R', 10)
            c.drawRightString(W - 52, y - h + 10, f"p. {pg}")
            y -= h + 6
        c.setFillColor(HexColor('#555555')); c.setFont('R', 8.5)
        c.drawCentredString(W / 2, 28, SRC)
        c.showPage()


def compare(c):
    c.setFillColor(HexColor(CMPLIGHT)); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 55, ['#43A047', '#1E88E5', CMPCOL])
    rrect(c, 30, H - 102, W - 60, 72, 26, CMPCOL)
    c.setFillColor(white); c.setFont('B', 36)
    c.drawCentredString(W / 2, H - 72, "idle  vs  ideal")
    c.setFont('R', 12)
    c.drawCentredString(W / 2, H - 91, "Похожие по звучанию, но разные по смыслу")
    cw = (W - 60 - 16) / 2
    top = H - 122
    cards = [(WORDS[IDLE_I], 30), (WORDS[IDEAL_I], 30 + cw + 16)]
    ch = 246
    for wd, x in cards:
        col, light = wd[6], wd[7]
        rrect(c, x, top - ch, cw, ch, 20, '#FFFFFF', col, 3)
        rrect(c, x, top - 70, cw, 70, 20, col)
        c.setFillColor(white); c.setFont('B', 34)
        c.drawCentredString(x + cw / 2, top - 48, wd[0])
        para(c, f"({wd[1]})", x + 10, top - 82, cw - 20, 15, 'B', col, 19, align=1)
        short_en = {"idle": "doing nothing; not in use", "ideal": "perfect; the best possible"}[wd[0]]
        short_ru = {"idle": "бездельничающий; не используемый; пустой", "ideal": "идеальный; наилучший"}[wd[0]]
        hh = para(c, short_en, x + 12, top - 112, cw - 24, 13.5, 'B', '#222222', 18, align=1)
        para(c, short_ru, x + 12, top - 118 - hh, cw - 24, 12.5, 'R', '#1a1a1a', 16, align=1)
        ex = {"idle": ("The machine sat idle all week.", "Станок простаивал всю неделю."),
              "ideal": ("It was the ideal place for a holiday.", "Это было идеальное место для отпуска.")}[wd[0]]
        yy = top - 180
        rrect(c, x + 10, yy - 58, cw - 20, 66, 12, light)
        hh = para(c, ex[0], x + 18, yy, cw - 36, 12, 'I', '#222222', 15, align=1)
        para(c, ex[1], x + 18, yy - hh - 3, cw - 36, 11, 'R', '#1a1a1a', 14, align=1)
    y = top - ch - 12
    tips = [("Spelling / Написание", "idle = 4 letters, 2 syllables (Y-dul).  ideal = 5 letters, 3 syllables (y-DEE-ul).",
             "idle: 2 слога (Y-dul); ideal: 3 слога (y-DEE-ul). В ideal есть буквы e-a."),
            ("Memory trick / Подсказка", "ideal has 'idea' inside it: an ideal is the best idea. idle = nothing happens.",
             "В ideal спрятано слово idea («идея»): идеал - лучшая идея. Idle - ничего не происходит.")]
    for t, en, ru in tips:
        eh = height(en, W - 100, 12.5, 'R', 16); rh = height(ru, W - 100, 11.5, 'R', 15)
        h = eh + rh + 38
        rrect(c, 30, y - h, W - 60, h, 16, '#FFFFFF', CMPCOL, 2.5)
        c.setFillColor(HexColor(CMPCOL)); c.setFont('B', 12)
        c.drawString(45, y - 20, t)
        para(c, en, 45, y - 28, W - 100, 12.5, 'R', '#222222', 16)
        para(c, ru, 45, y - 32 - eh, W - 100, 11.5, 'R', '#1a1a1a', 15)
        y -= h + 8
    c.setFillColor(HexColor(CMPCOL)); c.setFont('B', 14)
    c.drawString(34, y - 12, "Quick quiz: idle or ideal?  /  Мини-тест")
    y -= 22
    quiz = [("1. The workers were ______ because there was no job to do.", "Рабочие бездельничали, потому что не было работы.", "idle"),
            ("2. This is the ______ spot for a picnic.", "Это идеальное место для пикника.", "ideal"),
            ("3. The shop's machines stay ______ at night.", "Станки магазина ночью простаивают.", "idle")]
    for q, r, a in quiz:
        qh = height(q, W - 220, 12, 'B', 15)
        h = qh + 12 + 14 + 4
        rrect(c, 30, y - h, W - 60, h, 12, '#FFFFFF', CMPCOL, 1.5)
        para(c, q, 42, y - 8, W - 220, 12, 'B', '#222222', 15)
        para(c, r, 42, y - 10 - qh, W - 220, 10.5, 'R', '#1a1a1a', 14)
        rrect(c, W - 170, y - h + 8, 130, h - 16, 10, '#43A047' if a == 'idle' else '#1E88E5')
        c.setFillColor(white); c.setFont('B', 11)
        c.drawCentredString(W - 105, y - h / 2 - 4, f"Answer: {a}")
        y -= h + 8
    assert y > 30, y
    c.setFillColor(HexColor('#777777')); c.setFont('R', 8)
    c.drawCentredString(W / 2, 18, f"Vocabulary Handbook  |  page {CMP}")
    c.showPage()


def review(c):
    chunks = [WORDS[:8], WORDS[8:]]
    for ci, chunk in enumerate(chunks):
        c.setFillColor(HexColor('#FFFDE7')); c.rect(0, 0, W, H, fill=1, stroke=0)
        confetti(c, 99 + ci, ALLC)
        rrect(c, 40, H - 110, W - 80, 70, 20, '#E53935')
        c.setFillColor(white); c.setFont('B', 22)
        c.drawCentredString(W / 2, H - 82, "Quick Review  /  Быстрое повторение" + ("" if ci == 0 else "  (2)"))
        y = H - 128
        for wd in chunk:
            col = wd[6]
            nm = short(wd[0])
            fs = 22
            while height(f"<b>{nm}</b>", 180, fs, 'B', fs * 1.2) > 2.4 * fs * 1.2 * (1 if fs > 14 else 1.2) and fs > 9: fs -= 1
            while fs > 9 and height(f"<b>{nm}</b>", 180, fs, 'B', fs * 1.2) > (fs * 1.2 * 2 + 1):
                fs -= 1
            nh = height(f"<b>{nm}</b>", 180, fs, 'B', fs * 1.2)
            rt = f"({wd[1]})" if len(wd[0]) < 40 else ""
            rsh = height(rt, 180, 8.5, 'I', 11) if rt else 0
            en_h = height(wd[3], 245, 10.5, 'B', 13)
            ru_h = height(wd[4].split(';')[0], 245, 10.5, 'R', 13)
            h = max(nh + rsh + 18, en_h + ru_h + 20, 60)
            rrect(c, 40, y - h, W - 80, h, 14, col)
            para(c, f"<b>{nm}</b>", 54, y - 9, 180, fs, 'B', '#FFFFFF', fs * 1.2)
            if rt:
                para(c, rt, 54, y - 11 - nh, 180, 8.5, 'I', '#FFFFFF', 11)
            rrect(c, 240, y - h + 6, W - 80 - 206, h - 12, 10, '#FFFFFF')
            para(c, wd[3], 250, y - 11, 245, 10.5, 'B', '#222222', 13)
            para(c, wd[4].split(';')[0], 250, y - 13 - en_h, 245, 10.5, 'R', '#1a1a1a', 13)
            y -= h + 8
            assert y > 70, (wd[0], y)
        c.setFillColor(HexColor('#555555')); c.setFont('R', 9)
        c.drawCentredString(W / 2, 50, "Tip: say each example aloud, then cover the Russian line and translate it back.")
        c.drawCentredString(W / 2, 36, "Совет: прочитайте пример вслух, закройте перевод и переведите его сами.")
        c.showPage()


out = '/tmp/workspace/vocab-handbook/Vocabulary_Handbook_Complete_16_Words_EN_RU.pdf'
c = canvas.Canvas(out, pagesize=A4)
c.setTitle("Vocabulary Handbook: Complete (EN-RU)")
cover(c); contents(c)
for i, wd in enumerate(WORDS):
    word_page(c, i, wd, PAGES[i])
    if wd[0] == 'ideal':
        compare(c)
review(c)
c.save()
print(out, 'review starts', REVIEW_START)
Writing File

~/vocab-handbook/build3.py

old = open('/tmp/workspace/vocab-handbook/build.py').read()
new = open('/tmp/workspace/vocab-handbook/build2.py').read()
head = old.split("# (word, respelling")[0]
helpers = old[old.index("SRC ="):old.index("ALLC =")]
wp = old[old.index("def word_page"):old.index("def review")]
wp = wp.replace("def word_page(c, idx, wd):", "def word_page(c, idx, wd, pg):").replace("page {idx + 3}", "page {pg}")
exec(head + helpers)
exec(wp)
ns = {}
w1 = old[old.index("WORDS = ["):old.index("SRC =")]
w2 = new[new.index("WORDS = ["):new.index("ALLC =")]
exec(w1, ns); OLDW = ns['WORDS']
exec(w2, ns); NEWW = ns['WORDS']
WORDS = OLDW + NEWW            # 16 entries
ALLC = [w[6] for w in WORDS]
SRC = "Vocabulary Handbook  |  English + respelling + Русский перевод"
CMPCOL, CMPLIGHT = '#D81B60', '#FCE4EC'

# page plan
PAGES = {}
p = 4
for i in range(len(WORDS)):
    PAGES[i] = p
    p += 1
    if WORDS[i][0] == 'ideal':
        CMP = p
        p += 1
IDLE_I = [w[0] for w in WORDS].index('idle')
IDEAL_I = [w[0] for w in WORDS].index('ideal')


def short(nm):
    return nm if len(nm) < 40 else "getting nowhere / going round in circles"


def cover(c):
    c.setFillColor(HexColor('#FFF8E1')); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 1, ALLC)
    bw = W / len(ALLC)
    for i, col in enumerate(ALLC):
        c.setFillColor(HexColor(col)); c.rect(i * bw, H - 28, bw, 28, fill=1, stroke=0)
        c.rect(i * bw, 0, bw, 28, fill=1, stroke=0)
    rrect(c, 45, H - 330, W - 90, 260, 30, '#1E88E5')
    rrect(c, 55, H - 320, W - 110, 240, 24, '#42A5F5')
    c.setFillColor(white); c.setFont('B', 40)
    c.drawCentredString(W / 2, H - 150, "Vocabulary")
    c.drawCentredString(W / 2, H - 203, "Handbook")
    c.setFont('B', 16)
    c.drawCentredString(W / 2, H - 245, "Arguments, Discussions and Words in Context")
    c.setFont('R', 13)
    c.drawCentredString(W / 2, H - 270, "Словарь: споры, обсуждения и слова в контексте")
    c.setFont('R', 11)
    c.drawCentredString(W / 2, H - 298, "English  +  respelling  +  Русский перевод  +  3 examples")
    cw = W / 2 - 60
    y = H - 360
    for i, wd in enumerate(WORDS):
        cx = 45 if i % 2 == 0 else W / 2 + 15
        cy = y - (i // 2) * 52
        rrect(c, cx, cy - 40, cw, 44, 14, wd[6])
        c.setFillColor(white)
        nm = short(wd[0]); fs = 15
        while pdfmetrics.stringWidth(nm, 'B', fs) > cw - 16: fs -= 0.5
        c.setFont('B', fs)
        c.drawCentredString(cx + cw / 2, cy - 25, nm)
    c.setFillColor(HexColor('#555555')); c.setFont('R', 9)
    c.drawCentredString(W / 2, 42, "16 words and phrases  |  16 слов и выражений")
    c.showPage()


def rows_list():
    rows, n = [], 0
    for i, wd in enumerate(WORDS):
        n += 1
        rows.append((str(n), wd[0], wd[1], wd[6], wd[7], PAGES[i], False))
        if wd[0] == 'ideal':
            rows.append(("VS", "idle vs ideal", "compare the two words / сравнение", CMPCOL, CMPLIGHT, CMP, True))
    return rows


def contents(c):
    rows = rows_list()
    chunks = [rows[:10], rows[10:]]
    for ci, chunk in enumerate(chunks):
        c.setFillColor(HexColor('#F3F8FF')); c.rect(0, 0, W, H, fill=1, stroke=0)
        confetti(c, 2 + ci, ALLC)
        rrect(c, 40, H - 110, W - 80, 70, 20, '#5E35B1')
        c.setFillColor(white); c.setFont('B', 26)
        c.drawCentredString(W / 2, H - 85, "Word List  /  Список слов" + ("" if ci == 0 else "  (2)"))
        y = H - 128
        for num, name, resp, col, light, pg, is_cmp in chunk:
            h = 57
            rrect(c, 40, y - h, W - 80, h, 14, light, col, 2)
            rrect(c, 40, y - h, 52, h, 14, col)
            c.setFillColor(white); c.setFont('B', 20 if is_cmp else 22)
            c.drawCentredString(66, y - h / 2 - 8, num)
            fs = 16 if len(name) < 30 else 11.5
            t = f"<b>{name}</b>"
            para(c, t, 105, y - 8, W - 190, fs, 'B', col, leading=fs * 1.25)
            hh = height(t, W - 190, fs, 'B', fs * 1.25)
            para(c, resp if is_cmp else f"({resp})", 105, y - 10 - hh, W - 190, 9.5, 'I', '#444444')
            c.setFillColor(HexColor('#444444')); c.setFont('R', 10)
            c.drawRightString(W - 52, y - h + 10, f"p. {pg}")
            y -= h + 6
        c.setFillColor(HexColor('#555555')); c.setFont('R', 8.5)
        c.drawCentredString(W / 2, 28, SRC)
        c.showPage()


def compare(c):
    c.setFillColor(HexColor(CMPLIGHT)); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, 55, ['#43A047', '#1E88E5', CMPCOL])
    rrect(c, 30, H - 102, W - 60, 72, 26, CMPCOL)
    c.setFillColor(white); c.setFont('B', 36)
    c.drawCentredString(W / 2, H - 72, "idle  vs  ideal")
    c.setFont('R', 12)
    c.drawCentredString(W / 2, H - 91, "Похожие по звучанию, но разные по смыслу")
    cw = (W - 60 - 16) / 2
    top = H - 122
    cards = [(WORDS[IDLE_I], 30), (WORDS[IDEAL_I], 30 + cw + 16)]
    ch = 246
    for wd, x in cards:
        col, light = wd[6], wd[7]
        rrect(c, x, top - ch, cw, ch, 20, '#FFFFFF', col, 3)
        rrect(c, x, top - 70, cw, 70, 20, col)
        c.setFillColor(white); c.setFont('B', 34)
        c.drawCentredString(x + cw / 2, top - 48, wd[0])
        para(c, f"({wd[1]})", x + 10, top - 82, cw - 20, 15, 'B', col, 19, align=1)
        short_en = {"idle": "doing nothing; not in use", "ideal": "perfect; the best possible"}[wd[0]]
        short_ru = {"idle": "бездельничающий; не используемый; пустой", "ideal": "идеальный; наилучший"}[wd[0]]
        hh = para(c, short_en, x + 12, top - 112, cw - 24, 13.5, 'B', '#222222', 18, align=1)
        para(c, short_ru, x + 12, top - 118 - hh, cw - 24, 12.5, 'R', '#1a1a1a', 16, align=1)
        ex = {"idle": ("The machine sat idle all week.", "Станок простаивал всю неделю."),
              "ideal": ("It was the ideal place for a holiday.", "Это было идеальное место для отпуска.")}[wd[0]]
        yy = top - 180
        rrect(c, x + 10, yy - 58, cw - 20, 66, 12, light)
        hh = para(c, ex[0], x + 18, yy, cw - 36, 12, 'I', '#222222', 15, align=1)
        para(c, ex[1], x + 18, yy - hh - 3, cw - 36, 11, 'R', '#1a1a1a', 14, align=1)
    y = top - ch - 12
    tips = [("Spelling / Написание", "idle = 4 letters, 2 syllables (Y-dul).  ideal = 5 letters, 3 syllables (y-DEE-ul).",
             "idle: 2 слога (Y-dul); ideal: 3 слога (y-DEE-ul). В ideal есть буквы e-a."),
            ("Memory trick / Подсказка", "ideal has 'idea' inside it: an ideal is the best idea. idle = nothing happens.",
             "В ideal спрятано слово idea («идея»): идеал - лучшая идея. Idle - ничего не происходит.")]
    for t, en, ru in tips:
        eh = height(en, W - 100, 12.5, 'R', 16); rh = height(ru, W - 100, 11.5, 'R', 15)
        h = eh + rh + 38
        rrect(c, 30, y - h, W - 60, h, 16, '#FFFFFF', CMPCOL, 2.5)
        c.setFillColor(HexColor(CMPCOL)); c.setFont('B', 12)
        c.drawString(45, y - 20, t)
        para(c, en, 45, y - 28, W - 100, 12.5, 'R', '#222222', 16)
        para(c, ru, 45, y - 32 - eh, W - 100, 11.5, 'R', '#1a1a1a', 15)
        y -= h + 8
    c.setFillColor(HexColor(CMPCOL)); c.setFont('B', 14)
    c.drawString(34, y - 12, "Quick quiz: idle or ideal?  /  Мини-тест")
    y -= 22
    quiz = [("1. The workers were ______ because there was no job to do.", "Рабочие бездельничали, потому что не было работы.", "idle"),
            ("2. This is the ______ spot for a picnic.", "Это идеальное место для пикника.", "ideal"),
            ("3. The shop's machines stay ______ at night.", "Станки магазина ночью простаивают.", "idle")]
    for q, r, a in quiz:
        qh = height(q, W - 220, 12, 'B', 15)
        h = qh + 12 + 14 + 4
        rrect(c, 30, y - h, W - 60, h, 12, '#FFFFFF', CMPCOL, 1.5)
        para(c, q, 42, y - 8, W - 220, 12, 'B', '#222222', 15)
        para(c, r, 42, y - 10 - qh, W - 220, 10.5, 'R', '#1a1a1a', 14)
        rrect(c, W - 170, y - h + 8, 130, h - 16, 10, '#43A047' if a == 'idle' else '#1E88E5')
        c.setFillColor(white); c.setFont('B', 11)
        c.drawCentredString(W - 105, y - h / 2 - 4, f"Answer: {a}")
        y -= h + 8
    assert y > 30, y
    c.setFillColor(HexColor('#777777')); c.setFont('R', 8)
    c.drawCentredString(W / 2, 18, f"Vocabulary Handbook  |  page {CMP}")
    c.showPage()


def review(c):
    chunks = [WORDS[:8], WORDS[8:]]
    for ci, chunk in enumerate(chunks):
        c.setFillColor(HexColor('#FFFDE7')); c.rect(0, 0, W, H, fill=1, stroke=0)
        confetti(c, 99 + ci, ALLC)
        rrect(c, 40, H - 110, W - 80, 70, 20, '#E53935')
        c.setFillColor(white); c.setFont('B', 22)
        c.drawCentredString(W / 2, H - 82, "Quick Review  /  Быстрое повторение" + ("" if ci == 0 else "  (2)"))
        y = H - 128
        for wd in chunk:
            col = wd[6]
            nm = short(wd[0])
            fs = 22
            while fs > 9 and height(f"<b>{nm}</b>", 180, fs, 'B', fs * 1.2) > fs * 1.2 * 2 + 1:
                fs -= 1
            nh = height(f"<b>{nm}</b>", 180, fs, 'B', fs * 1.2)
            rt = f"({wd[1]})" if len(wd[0]) < 40 else ""
            rsh = height(rt, 180, 8.5, 'I', 11) if rt else 0
            ru_short = wd[4].split(';')[0]
            en_h = height(wd[3], 245, 10.5, 'B', 13)
            ru_h = height(ru_short, 245, 10.5, 'R', 13)
            h = max(nh + rsh + 20, en_h + ru_h + 22, 60)
            rrect(c, 40, y - h, W - 80, h, 14, col)
            para(c, f"<b>{nm}</b>", 54, y - 9, 180, fs, 'B', '#FFFFFF', fs * 1.2)
            if rt:
                para(c, rt, 54, y - 11 - nh, 180, 8.5, 'I', '#FFFFFF', 11)
            rrect(c, 240, y - h + 6, W - 80 - 206, h - 12, 10, '#FFFFFF')
            para(c, wd[3], 250, y - 11, 245, 10.5, 'B', '#222222', 13)
            para(c, ru_short, 250, y - 13 - en_h, 245, 10.5, 'R', '#1a1a1a', 13)
            y -= h + 8
            assert y > 70, (wd[0], y)
        c.setFillColor(HexColor('#555555')); c.setFont('R', 9)
        c.drawCentredString(W / 2, 50, "Tip: say each example aloud, then cover the Russian line and translate it back.")
        c.drawCentredString(W / 2, 36, "Совет: прочитайте пример вслух, закройте перевод и переведите его сами.")
        c.showPage()


out = '/tmp/workspace/vocab-handbook/Vocabulary_Handbook_Complete_16_Words_EN_RU.pdf'
c = canvas.Canvas(out, pagesize=A4)
c.setTitle("Vocabulary Handbook: Complete (EN-RU)")
cover(c); contents(c)
for i, wd in enumerate(WORDS):
    word_page(c, i, wd, PAGES[i])
    if wd[0] == 'ideal':
        compare(c)
review(c)
c.save()
print(out)
Running Command

cd /tmp/workspace/vocab-handbook && python3 build3.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Complete_16_Words_EN_RU.pdf') print(len(d)) for i in (0,2,len(d)-2,len(d)-1): d[i].render(scale=0.9).to_pil().save(f'm{i}.png')"; ls m*.png

Running Command

cd /tmp/workspace/vocab-handbook && python3 -c " new=open('build2.py').read() print(new.index('WORDS = ['), new.index('ALLC =')) import re print([m.start() for m in re.finditer('ALLC =',new)][:3]) old=open('build.py').read() print(old.index('WORDS = ['), old.index('SRC =')) print([m.start() for m in re.finditer('SRC =',old)]) "

Running Command

cd /tmp/workspace/vocab-handbook && sed -i 's/w2 = new\[new.index("WORDS = \["):new.index("ALLC =")\]/w2 = new[new.index("WORDS = ["):new.index("ALLC =", new.index("WORDS = ["))]/' build3.py && python3 build3.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Complete_16_Words_EN_RU.pdf') print(len(d)) for i in (0,2,len(d)-2,len(d)-1): d[i].render(scale=0.9).to_pil().save(f'm{i}.png')"; ls m*.png

Running Command

cd /tmp/workspace/vocab-handbook && python3 - <<'E' s=open('build3.py').read() s=s.replace("chunks = [WORDS[:8], WORDS[8:]]","chunks = [WORDS[0:6], WORDS[6:12], WORDS[12:]]") s=s.replace('''"" if ci == 0 else " (2)")) y = H - 128 for wd in chunk:''','''"" if ci == 0 else f" ({ci + 1})")) y = H - 128 for wd in chunk:''') open('build3.py','w').write(s) E python3 build3.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Complete_16_Words_EN_RU.pdf') n=len(d);print(n) for i in (0,1,2,3,n-3,n-2,n-1): d[i].render(scale=0.8).to_pil().save(f'm{i}.png')"; ls m*.png

Reading File
Reading File
Reading File
Reading File
Running Command

cd /tmp/workspace/vocab-handbook && python3 - <<'E' s=open('build3.py').read() s=s.replace('''c.setFillColor(white); c.setFont('B', 22) c.drawCentredString(W / 2, H - 82, "Quick Review''','''c.setFillColor(white); c.setFont('B', 22 if ci == 0 else 19) c.drawCentredString(W / 2, H - 82, "Quick Review''') open('build3.py','w').write(s) E python3 build3.py && python3 -c " import pypdfium2 as p d=p.PdfDocument('Vocabulary_Handbook_Complete_16_Words_EN_RU.pdf') print(len(d)) d[21].render(scale=0.6).to_pil().save('q21.png'); d[17].render(scale=0.6).to_pil().save('q17.png')"

Reading File
Running Command

cd /tmp/workspace/vocab-handbook && ls q*.png; ls -la Vocabulary_Handbook_Complete_16_Words_EN_RU.pdf

Reading File
The combined handbook is ready:

Vocabulary Handbook Complete 16 Words EN RU

PDF Document · PDF

. It has 23 pages. I checked the cover, contents, quick-review and "idle vs ideal" pages by eye. The word pages for the six new entries and the last review page I did not re-open.
The new words are laid out exactly like the earlier ones, with the same large colour banner, respelling in brackets, English and Russian meanings, and 3 example sentences with Russian translations.
Page layout
  • Page 1: cover with all 16 words as coloured chips.
  • Pages 2 to 3: word list with page numbers.
  • Pages 4 to 13: your original 10 words (twisting my words through petulant).
  • Pages 14 to 20: the new words, in this order:
    • 11. briny
      1. comical
      1. perilous
      1. idle
      1. ideal
    • idle vs ideal comparison page (page 19)
      1. profound
  • Pages 21 to 23: quick review of all 16 words, with the Russian meanings.
The quick-review section is now 3 pages instead of 1, because 16 words no longer fit on one. I also shortened the cover and word-list label for entry 6 to "getting nowhere / going round in circles" so it fits. The full phrase is on its own page.
attachment
attachment

Now please create a colorful handbook pdf file in which you must describe this photo from the vocabulary words list created by you recently.My student is ESL C1 proficiency , create this description example for his speaking skills exam Cambridge C1.

analyse_media
Running Command

cd /tmp/workspace/vocab-handbook && python3 -c " from PIL import Image im=Image.open('/tmp/workspace/attachments/79eccc15-8a5f-48c4-9b95-0653e65d105a/IMG-20261105-WA0006.jpg');print(im.size) w,h=im.size im.crop((16,20,w-15,h-12)).save('photo_crop.jpg',quality=92);print(Image.open('photo_crop.jpg').size)"

Writing File

~/vocab-handbook/build4.py

import re
src = open('/tmp/workspace/vocab-handbook/build3.py').read()
src = src[:src.index("out = '/tmp/workspace/vocab-handbook/Vocabulary_Handbook_Complete")]
exec(src)   # fonts, helpers (para, height, rrect, confetti), WORDS

PHOTO = '/tmp/workspace/vocab-handbook/photo_crop.jpg'
PW, PH = 1238, 870

# key -> (display, respelling, EN meaning, RU meaning, color, highlight color)
V = {
 'comical': ("comical", "KOM-ih-kul", "funny, often because it looks silly", "смешной, забавный", '#FB8C00', '#EF6C00'),
 'idle': ("idle", "Y-dul", "doing nothing; not busy or not working", "праздный, бездельничающий", '#43A047', '#2E7D32'),
 'ideal': ("ideal", "y-DEE-ul", "perfect or the best possible for a purpose", "идеальный, наилучший", '#1E88E5', '#1565C0'),
 'profound': ("profound", "pruh-FOWND", "very deep or strong", "глубокий, сильный", '#8E24AA', '#6A1B9A'),
 'twist': ("twisting my words", "TWIS-ting my WURDZ", "deliberately changing what someone said", "искажать мои слова", '#E53935', '#C62828'),
 'wires': ("got our wires crossed", "GOT OW-er WY-erz KRAWST", "misunderstood each other", "не поняли друг друга", '#FB8C00', '#EF6C00'),
 'lrc': ("left, right and centre", "LEFT RYT and SEN-ter", "on every side; everywhere", "повсюду, направо и налево", '#8E24AA', '#AD1457'),
 'circles': ("going round in circles", "GOH-ing ROWND in SUR-kulz", "repeating the same things without a result", "ходить по кругу", '#1E88E5', '#1565C0'),
 'world': ("it's not the end of the world", "its NOT thuh END uv thuh WURLD", "it is not as serious as you think", "это не конец света", '#43A047', '#2E7D32'),
 'happens': ("as it happens", "az it HAP-enz", "actually; as a matter of fact", "вообще-то, кстати", '#5E35B1', '#4527A0'),
 'petulant': ("petulant", "PET-yuh-lunt", "childishly bad-tempered", "капризный", '#6D4C41', '#4E342E'),
 'perilous': ("perilous", "PER-ih-lus", "full of danger or risk", "опасный, рискованный", '#E53935', '#C62828'),
 'wrong': ("that came out all wrong", "THAT kaym OWT awl RAWNG", "used to apologize for something rude or unclear", "я неправильно выразился", '#F9A825', '#B26A00'),
 'milk': ("no point crying over spilled milk", "NOH POYNT KRY-ing OH-ver SPILD MILK", "no reason to worry about what cannot be changed", "что сделано, то сделано", '#00ACC1', '#00838F'),
 'costs': ("get a grip of costs", "GET uh GRIP uv KOSTS", "to minimize or control costs", "взять расходы под контроль", '#D81B60', '#AD1457'),
}


def HL(txt, key):
    return f'<font color="white" backColor="{V[key][5]}"><b>&nbsp;{txt}&nbsp;</b></font>'


def HLR(txt, key):   # russian highlight (bold coloured text, no background)
    return f'<font color="{V[key][5]}"><b>{txt}</b></font>'


# ---------------- model answer ----------------
P1 = ("This photo captures a heated discussion in a public place. According to the caption, it's Parliament Square in London. "
      "In the foreground there are three men. The one on the right, in a navy cap, is leaning forward and jabbing his finger at the others, "
      "so he is clearly making his point very forcefully. The younger man in the middle, with a ginger beard and headphones round his neck, "
      "is frowning as if he's concentrating hard. What strikes me most is the man on the left, who has folded his arms and is smiling slightly. "
      "He looks almost amused, which makes the scene a little " + HL("comical", 'comical') + ".")
P2 = ("Behind them, a crowd has gathered, and some of the onlookers seem quite " + HL("idle", 'idle') + ", just watching the show. "
      "With the sunshine and the leafy trees, it's an " + HL("ideal", 'ideal') + " setting for an open-air debate.")
P3 = ("I get the impression that they have a " + HL("profound", 'profound') + " disagreement, perhaps about politics. "
      "The man in the cap may feel the others are " + HL("twisting his words", 'twist') + ", or perhaps they've simply "
      + HL("got their wires crossed", 'wires') + ". When opinions are flying " + HL("left, right and centre", 'lrc') +
      ", a conversation can easily go " + HL("round in circles", 'circles') + ". Even so, " + HL("it's not the end of the world", 'world') +
      ". " + HL("As it happens", 'happens') + ", I think a public debate like this is healthy, as long as nobody turns "
      + HL("petulant", 'petulant') + " or lets it become " + HL("perilous", 'perilous') + ".")
plain = lambda s: re.sub(r'<[^>]+>|&nbsp;', '', s)
WORDCOUNT = len((plain(P1) + ' ' + plain(P2) + ' ' + plain(P3)).split())

R1 = ("На этом фото запечатлена жаркая дискуссия в общественном месте. Судя по подписи, это Парламентская площадь в Лондоне. "
      "На переднем плане трое мужчин. Тот, что справа, в тёмно-синей кепке, подался вперёд и тычет пальцем в остальных, "
      "так что он явно очень настойчиво отстаивает свою точку зрения. Молодой человек посередине, с рыжеватой бородой и наушниками на шее, "
      "хмурится, словно сосредоточенно слушает. Больше всего меня поражает мужчина слева: он скрестил руки и слегка улыбается. "
      "Он выглядит почти позабавленным, из-за чего сцена кажется немного " + HLR("комичной", 'comical') + ".")
R2 = ("Позади собралась толпа, и некоторые зрители выглядят вполне " + HLR("праздными", 'idle') + ": они просто наблюдают за зрелищем. "
      "Солнце и зелёные деревья делают это место " + HLR("идеальным", 'ideal') + " для публичных дебатов под открытым небом.")
R3 = ("У меня создаётся впечатление, что у них " + HLR("глубокие", 'profound') + " разногласия, возможно, по политическим вопросам. "
      "Мужчина в кепке, вероятно, считает, что остальные " + HLR("искажают его слова", 'twist') + ", или же они просто " +
      HLR("не поняли друг друга", 'wires') + ". Когда мнения летят " + HLR("со всех сторон", 'lrc') + ", разговор легко начинает " +
      HLR("ходить по кругу", 'circles') + ". И всё же " + HLR("это не конец света", 'world') + ". " + HLR("Вообще-то", 'happens') +
      ", я думаю, такие публичные дебаты полезны, если только никто не начинает " + HLR("капризничать", 'petulant') +
      " и не доводит дело до " + HLR("опасной черты", 'perilous') + ".")

FOLLOW = [
 ("Part 4 style question",
  "Do you think it's a good idea to argue about politics in public?",
  "Is it?  Спорить о политике публично - хорошая идея?",
  "In moderation, yes. People get the chance to hear different views. The risk is that someone says something they regret. "
  "If a remark " + HL("came out all wrong", 'wrong') + ", a quick apology usually fixes it, and there's " + HL("no point crying over spilled milk", 'milk') + ".",
  "В меру - да. Люди слышат разные взгляды. Риск в том, что кто-то скажет то, о чём пожалеет. Если замечание " + HLR("прозвучало совсем не так", 'wrong') +
  ", обычно помогает быстрое извинение, а " + HLR("слезами горю не поможешь", 'milk') + "."),
 ("Part 4 style question",
  "What do couples or families most often argue about?",
  "О чём чаще всего спорят пары и семьи?",
  "Money, I'd say. When bills pile up, a household needs to " + HL("get a grip of costs", 'costs') + ", and everyone tends to blame everyone else.",
  "О деньгах, я бы сказал. Когда накапливаются счета, семье нужно " + HLR("взять расходы под контроль", 'costs') + ", а все склонны винить друг друга."),
 ("Partner's question after your 1 minute (about 30 seconds)",
  "Which of the three men do you think is finding the discussion most difficult?",
  "Кому из трёх мужчин, по-вашему, эта дискуссия даётся тяжелее всех?",
  "Probably the man in the middle. He is listening so intently that he looks tense, whereas the others seem more at ease with the confrontation.",
  "Скорее всего, мужчине посередине. Он слушает так напряжённо, что выглядит скованным, тогда как остальные чувствуют себя увереннее в этой конфронтации."),
]


def title_bar(c, text, sub, col, y_top=None):
    y_top = y_top or H - 30
    rrect(c, 30, y_top - 72, W - 60, 72, 24, col)
    c.setFillColor(white); c.setFont('B', 26)
    c.drawCentredString(W / 2, y_top - 36, text)
    c.setFont('R', 11.5)
    c.drawCentredString(W / 2, y_top - 57, sub)
    return y_top - 72


def bg(c, light, seed, cols):
    c.setFillColor(HexColor(light)); c.rect(0, 0, W, H, fill=1, stroke=0)
    confetti(c, seed, cols)


def footer(c, n):
    c.setFillColor(HexColor('#777777')); c.setFont('R', 8)
    c.drawCentredString(W / 2, 18, f"Cambridge C1 Speaking: Photo Description  |  page {n}")


def photo(c, x, y_bottom, w, border='#1E88E5'):
    h = w * PH / PW
    rrect(c, x - 6, y_bottom - 6, w + 12, h + 12, 16, border)
    c.drawImage(PHOTO, x, y_bottom, w, h, mask=None)
    return h


ALLCOL = [v[4] for v in V.values()]


# ---------------- pages ----------------
def p_cover(c):
    bg(c, '#FFF8E1', 3, ALLCOL)
    bw = W / 8
    cols = ['#E53935', '#FB8C00', '#F9A825', '#43A047', '#00ACC1', '#1E88E5', '#5E35B1', '#8E24AA']
    for i, col in enumerate(cols):
        c.setFillColor(HexColor(col)); c.rect(i * bw, H - 26, bw, 26, fill=1, stroke=0)
        c.rect(i * bw, 0, bw, 26, fill=1, stroke=0)
    rrect(c, 40, H - 260, W - 80, 215, 28, '#5E35B1')
    c.setFillColor(white); c.setFont('B', 33)
    c.drawCentredString(W / 2, H - 110, "Describing a Photo")
    c.setFont('B', 19)
    c.drawCentredString(W / 2, H - 148, "Cambridge C1 Advanced Speaking")
    c.setFont('R', 14)
    c.drawCentredString(W / 2, H - 175, "Part 2: the long turn  |  Модельный ответ")
    c.setFont('R', 12)
    c.drawCentredString(W / 2, H - 205, "Model answer + Russian translation + vocabulary in use")
    c.drawCentredString(W / 2, H - 226, "Topic: arguments and discussions")
    w = 380
    h = photo(c, (W - w) / 2, H - 290 - w * PH / PW, w, '#FB8C00')
    y = H - 290 - h - 30
    c.setFillColor(HexColor('#5E35B1')); c.setFont('B', 13)
    c.drawCentredString(W / 2, y, "Words from the handbook used in the model answer")
    keys = ['comical', 'idle', 'ideal', 'profound', 'twist', 'wires', 'lrc', 'circles', 'world', 'happens', 'petulant', 'perilous']
    y -= 14
    cw, ch = 158, 24
    for i, k in enumerate(keys):
        x = 30 + (i % 3) * (cw + 8)
        yy = y - (i // 3) * (ch + 7)
        rrect(c, x, yy - ch, cw, ch, 10, V[k][5])
        nm = V[k][0]; fs = 10.5
        while pdfmetrics.stringWidth(nm, 'B', fs) > cw - 10: fs -= 0.5
        c.setFillColor(white); c.setFont('B', fs)
        c.drawCentredString(x + cw / 2, yy - 16, nm)
    c.showPage()


def p_task(c):
    bg(c, '#E3F2FD', 4, ['#1E88E5', '#FB8C00', '#43A047'])
    y = title_bar(c, "The Photo and the Task", "Фото и задание", '#1E88E5')
    w = 470
    h = w * PH / PW
    y -= 24 + h
    photo(c, (W - w) / 2, y, w, '#1E88E5')
    c.setFillColor(HexColor('#555555')); c.setFont('I', 9)
    c.drawCentredString(W / 2, y - 18, "Caption in the book: \"People arguing in Parliament Square, London, England.\"")
    y -= 36
    # task card
    t = ("Talk about the photo for <b>about one minute</b>. Say what you can see, why the people might be arguing, "
         "and how they might be feeling.")
    ru = "Говорите о фото около <b>одной минуты</b>: что вы видите, почему люди могут спорить и что они могут чувствовать."
    th = height(t, W - 110, 13, 'R', 18); rh = height(ru, W - 110, 11.5, 'R', 15)
    hh = th + rh + 44
    rrect(c, 30, y - hh, W - 60, hh, 16, '#FFFFFF', '#1E88E5', 3)
    c.setFillColor(HexColor('#1E88E5')); c.roundRect(45, y - 26, 80, 18, 9, fill=1, stroke=0)
    c.setFillColor(white); c.setFont('B', 10); c.drawCentredString(85, y - 20, "YOUR TASK")
    para(c, t, 45, y - 32, W - 110, 13, 'R', '#222222', 18)
    para(c, ru, 45, y - 36 - th, W - 110, 11.5, 'R', '#1a1a1a', 15)
    y -= hh + 14
    # three boxes
    items = [("1", "WHAT I CAN SEE", "Что я вижу", "#E53935"),
             ("2", "WHY THEY ARGUE", "Почему спорят", "#43A047"),
             ("3", "HOW THEY FEEL", "Что чувствуют", "#8E24AA")]
    bw = (W - 60 - 20) / 3
    for i, (n, a, b, col) in enumerate(items):
        x = 30 + i * (bw + 10)
        rrect(c, x, y - 78, bw, 78, 16, col)
        c.setFillColor(white); c.circle(x + 24, y - 26, 15, fill=1, stroke=0)
        c.setFillColor(HexColor(col)); c.setFont('B', 16); c.drawCentredString(x + 24, y - 32, n)
        c.setFillColor(white); c.setFont('B', 11.5); c.drawString(x + 46, y - 30, a)
        c.setFont('R', 10.5); c.drawString(x + 14, y - 58, b)
    y -= 78 + 14
    note = ("<b>Exam note:</b> in the real Cambridge C1 Advanced Part 2 you usually get <b>two photos</b>. You compare them, "
            "answer the question printed above them, and your partner then answers a short question (about 30 seconds). "
            "This page practises the same skills with one photo.")
    nh = height(note, W - 80, 10.5, 'R', 14)
    rrect(c, 30, y - nh - 18, W - 60, nh + 18, 12, '#FFF8E1', '#FB8C00', 1.5)
    para(c, note, 42, y - 9, W - 84, 10.5, 'R', '#333333', 14)
    footer(c, 2)
    c.showPage()


def p_model(c):
    bg(c, '#FFF3E0', 5, ['#FB8C00', '#43A047', '#8E24AA'])
    y = title_bar(c, "Model Answer", f"About {WORDCOUNT} words  |  roughly 1 minute 15 seconds at a natural pace", '#FB8C00')
    y -= 16
    blocks = [("1  Overview + foreground", P1, '#E53935'),
              ("2  Background", P2, '#43A047'),
              ("3  Speculating + opinion", P3, '#8E24AA')]
    for label, txt, col in blocks:
        th = height(txt, W - 90, 12, 'R', 20)
        h = th + 40
        rrect(c, 30, y - h, W - 60, h, 16, '#FFFFFF', col, 3)
        c.setFillColor(HexColor(col)); c.roundRect(44, y - 24, 190, 19, 9, fill=1, stroke=0)
        c.setFillColor(white); c.setFont('B', 10.5); c.drawString(54, y - 18, label)
        para(c, txt, 45, y - 32, W - 90, 12, 'R', '#222222', 20)
        y -= h + 12
    tip = ("<b>Tip:</b> the coloured words are vocabulary from your handbook. In the exam, use them naturally and "
           "do not force every one into your answer.")
    tr = "<b>Совет:</b> цветные слова - лексика из вашего словаря. На экзамене используйте их естественно, не вставляйте все подряд."
    th = height(tip, W - 84, 10.5, 'R', 14); rh = height(tr, W - 84, 10.5, 'R', 14)
    rrect(c, 30, y - th - rh - 20, W - 60, th + rh + 20, 12, '#FFFFFF', '#FB8C00', 1.5)
    para(c, tip, 42, y - 8, W - 84, 10.5, 'R', '#333333', 14)
    para(c, tr, 42, y - 10 - th, W - 84, 10.5, 'R', '#1a1a1a', 14)
    assert y - th - rh - 20 > 35, y
    footer(c, 3)
    c.showPage()


def p_russian(c):
    bg(c, '#F3E5F5', 6, ['#8E24AA', '#E53935', '#1E88E5'])
    y = title_bar(c, "Русский перевод", "Russian translation of the model answer", '#8E24AA')
    y -= 16
    blocks = [("1  Общая картина и передний план", R1, '#E53935'),
              ("2  Задний план", R2, '#43A047'),
              ("3  Предположения и мнение", R3, '#1E88E5')]
    for label, txt, col in blocks:
        th = height(txt, W - 90, 12, 'R', 20)
        h = th + 40
        rrect(c, 30, y - h, W - 60, h, 16, '#FFFFFF', col, 3)
        c.setFillColor(HexColor(col)); c.roundRect(44, y - 24, 250, 19, 9, fill=1, stroke=0)
        c.setFillColor(white); c.setFont('B', 10.5); c.drawString(54, y - 18, label)
        para(c, txt, 45, y - 32, W - 90, 12, 'R', '#222222', 20)
        y -= h + 12
    assert y > 35, y
    footer(c, 4)
    c.showPage()


def p_vocab(c):
    bg(c, '#E8F5E9', 7, ['#43A047', '#00ACC1', '#FB8C00'])
    y = title_bar(c, "Vocabulary in the Answer", "Лексика из ответа: произношение и значения", '#43A047')
    y -= 14
    keys = ['comical', 'idle', 'ideal', 'profound', 'twist', 'wires', 'lrc', 'circles', 'world', 'happens', 'petulant', 'perilous']
    cw = (W - 60 - 12) / 2
    ch = 93
    for i, k in enumerate(keys):
        d, resp, en, ru, col, hc = V[k]
        x = 30 + (i % 2) * (cw + 12)
        yy = y - (i // 2) * (ch + 8)
        rrect(c, x, yy - ch, cw, ch, 14, '#FFFFFF', hc, 2.5)
        rrect(c, x, yy - 34, cw, 34, 14, hc)
        c.setFillColor(hc); c.rect(x, yy - 34, cw, 14, fill=1, stroke=0)
        fs = 16
        while pdfmetrics.stringWidth(d, 'B', fs) > cw - 20: fs -= 0.5
        c.setFillColor(white); c.setFont('B', fs)
        c.drawCentredString(x + cw / 2, yy - 23, d)
        c.setFillColor(HexColor(hc)); c.setFont('B', 9)
        fr = 9
        while pdfmetrics.stringWidth(f"({resp})", 'B', fr) > cw - 16: fr -= 0.5
        c.setFont('B', fr)
        c.drawCentredString(x + cw / 2, yy - 48, f"({resp})")
        hh = para(c, en, x + 10, yy - 54, cw - 20, 9.5, 'R', '#222222', 12, align=1)
        para(c, ru, x + 10, yy - 56 - hh, cw - 20, 9.5, 'B', '#1a1a1a', 12, align=1)
    footer(c, 5)
    c.showPage()


def p_toolbox(c):
    bg(c, '#FFFDE7', 8, ['#F9A825', '#00ACC1', '#E53935'])
    y = title_bar(c, "C1 Language Toolbox", "Полезные фразы для описания фото", '#00ACC1')
    y -= 14
    secs = [
     ("Opening / Начало", '#E53935',
      ["This photo captures / shows ...", "According to the caption / Judging by the caption, ...",
       "The first thing that strikes me is ..."]),
     ("Position / Расположение", '#FB8C00',
      ["In the foreground / in the background", "on the far left / right-hand side / in the middle",
       "Behind them, a crowd has gathered"]),
     ("Speculating / Предположения", '#43A047',
      ["I get the impression that ...", "He might well be / may have been ...",
       "It looks as if / as though ...", "Presumably ... / It's hard to tell, but ..."]),
     ("Body language / Язык тела", '#1E88E5',
      ["jabbing his finger / leaning forward", "frowning with concentration",
       "arms folded, slightly amused", "visibly irritated / on edge"]),
     ("Opinion and closing / Мнение", '#8E24AA',
      ["Personally, I think ...", "What I find interesting is ...",
       "All in all, / On the whole, ..."]),
     ("Buying time / Выиграть время", '#D81B60',
      ["How can I put it ...", "or rather ... / what I mean is ...", "Let me think ... / To be honest, ..."]),
    ]
    cw = (W - 60 - 12) / 2
    # compute heights per row
    for r in range(3):
        row = secs[r * 2:r * 2 + 2]
        hs = []
        for t, col, items in row:
            hs.append(40 + sum(height("• " + it, cw - 28, 11, 'R', 15) + 3 for it in items))
        h = max(hs)
        for j, (t, col, items) in enumerate(row):
            x = 30 + j * (cw + 12)
            rrect(c, x, y - h, cw, h, 16, '#FFFFFF', col, 3)
            rrect(c, x, y - 28, cw, 28, 14, col)
            c.setFillColor(col and HexColor(col)); c.rect(x, y - 28, cw, 12, fill=1, stroke=0)
            c.setFillColor(white); c.setFont('B', 11.5)
            c.drawString(x + 12, y - 19, t)
            yy = y - 36
            for it in items:
                hh = para(c, "• " + it, x + 12, yy, cw - 24, 11, 'R', '#222222', 15)
                yy -= hh + 3
        y -= h + 12
    # connectors
    t = ("<b>Linking ideas / Связки:</b> Having said that, ... | On the other hand, ... | Whereas ... | "
         "That said, ... | As a result, ... | Even so, ...")
    th = height(t, W - 84, 11, 'R', 15)
    rrect(c, 30, y - th - 18, W - 60, th + 18, 12, '#FFFFFF', '#F9A825', 2.5)
    para(c, t, 42, y - 9, W - 84, 11, 'R', '#222222', 15)
    assert y - th - 18 > 35, y
    footer(c, 6)
    c.showPage()


def p_follow(c):
    bg(c, '#FCE4EC', 9, ['#D81B60', '#00ACC1', '#F9A825'])
    y = title_bar(c, "Follow-up Questions", "Дополнительные вопросы с моделями ответов", '#D81B60')
    y -= 14
    cols = ['#00ACC1', '#8E24AA', '#43A047']
    for (tag, q, qr, a, ar), col in zip(FOLLOW, cols):
        qh = height(f"<b>{q}</b>", W - 100, 12.5, 'B', 17)
        qrh = height(qr, W - 100, 10, 'I', 13)
        ah = height(a, W - 100, 11.5, 'R', 19)
        arh = height(ar, W - 100, 10.5, 'R', 15)
        h = qh + qrh + ah + arh + 62
        rrect(c, 30, y - h, W - 60, h, 16, '#FFFFFF', col, 3)
        c.setFillColor(HexColor(col)); c.roundRect(44, y - 24, 330, 18, 9, fill=1, stroke=0)
        c.setFillColor(white); c.setFont('B', 9.5); c.drawString(54, y - 18, tag)
        yy = y - 32
        yy -= para(c, f"<b>{q}</b>", 45, yy, W - 100, 12.5, 'B', col, 17)
        yy -= para(c, qr, 45, yy - 1, W - 100, 10, 'I', '#555555', 13) + 8
        yy -= para(c, a, 45, yy, W - 100, 11.5, 'R', '#222222', 19) + 3
        para(c, ar, 45, yy, W - 100, 10.5, 'R', '#1a1a1a', 15)
        y -= h + 12
    assert y > 35, y
    footer(c, 7)
    c.showPage()


def p_check(c):
    bg(c, '#E0F7FA', 10, ['#00ACC1', '#5E35B1', '#E53935'])
    y = title_bar(c, "Practice Plan and Self-Check", "План тренировки и самопроверка", '#5E35B1')
    y -= 14
    steps = [("1", "Read the model answer aloud twice.", "Прочитайте ответ вслух дважды.", '#E53935'),
             ("2", "Cover the text. Describe the photo using only the coloured prompts.", "Закройте текст и опишите фото только по цветным подсказкам.", '#FB8C00'),
             ("3", "Record yourself for 1 minute. Listen back and note pauses.", "Запишите себя на 1 минуту и прослушайте.", '#43A047'),
             ("4", "Repeat with your own words and at least 6 handbook words.", "Повторите своими словами, используя минимум 6 слов из словаря.", '#1E88E5')]
    for n, en, ru, col in steps:
        eh = height(en, W - 130, 12, 'B', 16); rh = height(ru, W - 130, 10.5, 'R', 14)
        h = eh + rh + 20
        rrect(c, 30, y - h, W - 60, h, 14, '#FFFFFF', col, 2.5)
        c.setFillColor(HexColor(col)); c.circle(58, y - h / 2, 15, fill=1, stroke=0)
        c.setFillColor(white); c.setFont('B', 15); c.drawCentredString(58, y - h / 2 - 5, n)
        para(c, en, 85, y - 9, W - 130, 12, 'B', '#222222', 16)
        para(c, ru, 85, y - 11 - eh, W - 130, 10.5, 'R', '#1a1a1a', 14)
        y -= h + 8
    y -= 4
    c.setFillColor(HexColor('#5E35B1')); c.setFont('B', 14)
    c.drawString(34, y - 14, "Cambridge C1 speaking criteria: tick when ready")
    y -= 24
    crit = [("Grammar and Vocabulary", "Wide range of structures and words; I used speculation (might well, may have) and at least 6 handbook words.", '#E53935'),
            ("Discourse Management", "I spoke for about a minute, organised my ideas (what I see, why, how they feel) and used linking words.", '#FB8C00'),
            ("Pronunciation", "Clear word stress and sentence stress; I checked the respellings of my vocabulary words.", '#43A047'),
            ("Interactive Communication", "I answered my partner's follow-up question naturally and kept the conversation going.", '#1E88E5')]
    for t, d, col in crit:
        dh = height(d, W - 140, 10.5, 'R', 14)
        h = dh + 34
        rrect(c, 30, y - h, W - 60, h, 14, '#FFFFFF', col, 2.5)
        c.setStrokeColor(HexColor(col)); c.setLineWidth(2.5)
        c.roundRect(44, y - h / 2 - 11, 22, 22, 4, fill=0, stroke=1)
        c.setFillColor(HexColor(col)); c.setFont('B', 12)
        c.drawString(82, y - 18, t)
        para(c, d, 82, y - 24, W - 140, 10.5, 'R', '#222222', 14)
        y -= h + 8
    assert y > 35, y
    footer(c, 8)
    c.showPage()


out = '/tmp/workspace/vocab-handbook/C1_Speaking_Photo_Description_Handbook.pdf'
c = canvas.Canvas(out, pagesize=A4)
c.setTitle("Cambridge C1 Speaking: Describing a Photo (EN-RU)")
for f in (p_cover, p_task, p_model, p_russian, p_vocab, p_toolbox, p_follow, p_check):
    f(c)
c.save()
print(out, 'words', WORDCOUNT)
Running Command

cd /tmp/workspace/vocab-handbook && python3 build4.py 2>&1 | tail -5 && python3 -c " import pypdfium2 as p d=p.PdfDocument('C1_Speaking_Photo_Description_Handbook.pdf') print(len(d)) for i in range(len(d)): d[i].render(scale=0.8).to_pil().save(f'k{i}.png')"

Reading File
Reading File
Reading File
Reading File
Running Command

cd /tmp/workspace/vocab-handbook && sed -i 's/roughly 1 minute 15 seconds at a natural pace/about 1 minute 20 seconds at natural pace (skip paragraph 2 if short of time)/' build4.py && python3 build4.py | tail -1 && python3 -c " import pypdfium2 as p d=p.PdfDocument('C1_Speaking_Photo_Description_Handbook.pdf') for i in range(len(d)): d[i].render(scale=0.8).to_pil().save(f'k{i}.png')"

Reading File
Reading File
Reading File
Reading File
Your C1 speaking handbook is ready:

C1 Speaking Photo Description Handbook

PDF Document · PDF

. It has 8 colourful pages and uses the same style as the vocabulary handbook.
Pages
  1. Cover: the photo and the 12 vocabulary words used in the model answer.
  2. The photo and task: the photo with its caption and a one-minute task in English and Russian. The task asks what you can see, why the people might be arguing and how they might feel.
  3. Model answer: about 221 words, roughly 1 minute 20 seconds at a natural pace. It has three parts: overview and foreground, background, then speculation and opinion. The handbook words are highlighted in colour.
  4. Russian translation: the same three parts, with the vocabulary highlighted.
  5. Vocabulary in the answer: a card for each of the 12 words with its respelling, English meaning and Russian meaning.
  6. C1 language toolbox: phrases for describing a photo, speculating, body language, giving an opinion, linking ideas and buying time.
  7. Follow-up questions: two Part 4 style questions and one partner-style question, each with a model answer and Russian translation.
  8. Practice plan and self-check: four practice steps and a tick-box list for the four Cambridge speaking criteria.
Vocabulary from the handbook
  • In the main answer (12 words and phrases): comical, idle, ideal, profound, twisting my words, got our wires crossed, left, right and centre, going round in circles, it's not the end of the world, as it happens, petulant, perilous.
  • In the follow-up answers (3): that came out all wrong, no point crying over spilled milk, get a grip of costs.
  • Not used: briny, because it doesn't fit this photo.
Things to know
  • The model answer is about 20 seconds over the one-minute limit. The page says to skip paragraph 2 if time is short. You could also cut it down for your student.
  • A real Cambridge C1 Part 2 task gives two photos to compare. Your file had one, so I wrote the task for a single photo. A short note on page 2 explains this. I can build a second-photo comparison if you send another image.
  • The automatic image analysis failed on the local file, so I described the photo from the image itself.
  • The description is based on what I could see: three men in the foreground, a crowd and trees behind them, and the caption naming Parliament Square. The reasons for the argument and the feelings are only speculation, as the exam task expects.
This is a shared conversation. Sign in to Orris to start your own chat.