# Making a book for the cbook reader A guide for an AI assistant. Version 1.5, 2026-11-13. From the cbook project, (c) 2026 Bill Mandel. Free to download and to use with an AI assistant to make books for the cbook reader. ## 0. Read this first A person has attached this file to a chat and asked you to help them make a book. The book is one EPUB file that reads well in the cbook reader (a web page for reading EPUB books, at https://friendsservermb.mine.nu/cbooks/) and in other EPUB readers. You write the book with the person, then build the file, then hand it over. You may be an assistant with no access to the person's computer. Nothing here needs any tool except Python 3 with its standard library, which is used once, at the end, to write the file. If you cannot run code, or cannot give the person a file, say so plainly (see section 6). Do not pretend to have made a file. Tell the person, in plain words, what you are doing at each step. Keep your messages short. Use plain, clear language yourself. ## 1. How to work, in order 1. **Ask first.** Ask these questions in one short message (a few at a time is better than a long list), then wait for the answers. Do not start writing. - What is the book about, and what should a reader be able to do or understand at the end? - Who will read it, and what do they already know? - How long: roughly how many chapters, and how long should a chapter be to read in one sitting? - What tone, and what reading level? Is there a book they would like it to be like? - Which sources to use, and which to avoid? Can they give you links, notes or files? Can you search the web in this chat? - Do they have pictures they have the right to use? (If not, make a book without pictures.) - The name to put on the book as author, and the rights line (for example "(c) 2026 Name. All rights reserved."). - Is this a new book, or a later edition of a book they already made? For a later edition, ask for the identifier (section 7). If the person says to infer the answers from their first message, do that and state your assumptions in a few lines. The one thing you must not invent is the author's name. Ask for it when you show the outline, or say plainly that you are using the word "Author" as a placeholder and will rebuild when they tell you the name. 2. **Propose an outline.** The book's title, one line about the book, and the chapters, each with one sentence of what it will cover. Wait for the person to approve it or change it. 3. **Write.** One chapter at a time. Follow sections 2 to 5. When you have written a chapter, tell the person it is done and carry on unless they want to review it. Keep to the length you agreed. The builder prints the word count: compare it with the plan (for example 5 chapters of 1,500 words is about 7,500 words). If the book is more than a fifth short, say so plainly and offer to expand it. Do not call a short book finished. 4. **Build the file** with the script in section 6. Check it as described in section 7. 5. **Hand it over** with the report in section 7. ## 2. Be honest about facts The person will read this as a book. Readers trust a book more than a chat, so be careful. - Every factual claim should come from a source you actually used in this chat, not from memory. If you can search the web, do, and read the page, not only the snippet. If you cannot, say so to the person at the start, and treat everything as from memory. - Never invent a source, a link, a quotation, a statistic, a date or a name. If you do not know, say so in the text or leave it out. - Only put in a link you opened in this chat. Before you build, open every link in the book once more. Your code sandbox may have no network. If so, use your search or browse tool instead, one link at a time. Take out any link that does not open, and tell the person you did. If there is some link you could not re-check by either route, name it in your report (section 7). Do not just say "please check the links". - Anything from memory that you did not check against a source goes in the `from_memory` list of the builder (section 6), and is said plainly in the text where it matters ("from memory, not checked"). - Where sources disagree, say so in the text and say which you followed. Collect these in a last chapter called "Notes on facts and sources". - At the end of each chapter, add a "Sources for this chapter" list of real links with author, title and year. Only link to pages you looked at. - Say who made the book: "written by the author with an AI assistant", and that it has not been reviewed by a subject expert (unless it has). - What the book says about how it was researched must be true. If you searched the web in this chat, say so, and keep the links. If you could not search, say that the book is written from memory. If you can search in some steps and not others, say which. Do not write "researched with live web sources" unless you did. - Dates, numbers, names and units are the commonest mistakes. Check them twice. ## 3. Rights - Write in your own words from facts in sources. Do not copy passages from books, articles or web pages. A short quotation with its source is fine. - Pictures: use only pictures the person owns, or that are in the public domain or under a licence that allows it (put the licence and credit in the caption and on a credits page). If the rights are not clear, leave the picture out and tell the person. - Do not use song lyrics, or text from someone's copyrighted book, in any form. - If the person gives you someone else's text to turn into a book, ask what they intend (private reading, sharing with friends, or public), and tell them that someone else's work usually needs the owner's permission to share. If that text is a PDF and the person also attached the companion guide "Turning a PDF into a cbook book", follow that guide for the PDF. ## 4. What a good cbook book looks like The reader lays the text out fresh for every screen, font size and theme. So you write what things are, not how they look. - **Order:** a cover (section 6), a title page, a contents page, "About this book" (what it is, who made it, how far to trust it, in a few short paragraphs), the chapters, "Notes on facts and sources", a credits page if there are pictures, and last "How this edition was made". The builder makes the title page, contents and the last page for you. You write the rest. - **Chapters:** the title is a number and a name ("3. Two machines built to outlive their makers"). Size a chapter for one sitting: a few thousand words is typical. Do not make many tiny chapters. Group related points under `##` sections inside a chapter. - **Sections:** each `##` section should have several paragraphs. A run of very short sections is better as a list of bold lead-ins: `- **First point.** One or two sentences.` - **Paragraphs:** short, plain, one idea each. A phone screen is small. - **No page references.** Pages differ on every screen. Say "see chapter 4" or "see the section 'Sources'", never "see page 12" or "see below". - **Callouts:** a few short boxes where a reader should pause (a definition, a warning, a "why this matters"). Use `:::note Title` (section 6). Do not rely on a box for meaning. - **Lists:** a blank line before every list. Do not use tables (a wide table is squeezed on a phone): use a list, one item per row. - **Pictures:** each needs a caption (what it shows, with the credit and licence) and alt text (what a person who cannot see it needs to know, in two or three sentences). Keep each to one idea. - **Do not** set fonts, colours, sizes or page breaks. Do not write a contents page by hand. Do not add a "your notes" page: the reader has notes and marks of its own. **The look** sets how each chapter opens in the cbook reader. Choose one for the `look` field and say which and why: - `plain`: reference, reports, and anything without a clear genre. If unsure, use this. - `story`: a book read as a story (a large first letter). - `guide`: a reading book, a how-to, a travel guide (a smaller first letter). - `convert`: a book converted from a PDF or text file and left as it came. Rarely right for a new book. ## 5. Chapter text: the Markdown subset Write each chapter in this subset. The builder turns it into the right pages. Everything else is plain text. - A paragraph is lines of text, with a blank line between paragraphs. - `## Section title` and `### Smaller title`. Start each chapter's headings at `##`, never skip a level, and never use `#` inside a chapter: the chapter title comes from the `title` field. - `- item` for a bullet list, `1. item` for a numbered list. A blank line before and after. - `**bold**` and `*italic*`. - `[link text](https://example.org)`. Real links only, with words that say where they go. A web address may contain one pair of round brackets, such as `https://en.wikipedia.org/wiki/Genie_(world_model)`. - `> a quotation` (a line starting with `>`). - A callout: a line `:::note Title`, then the text, then a line with `:::`. - A picture: `![Alt text that describes it](images/name.jpg "Caption with the credit and licence")` on a line by itself. The file must exist next to the script. Skip pictures if you have none. - The sources list of a chapter: end the chapter with `## Sources for this chapter` and a bullet list of links. The builder keeps it out of the contents and sets it apart. Use real typographic characters where the text calls for them (curly quotes, en and em dashes, degree signs, the multiplication sign). Make sure special characters came out right. ## 6. Build the EPUB file If you can run Python 3, do this. Save the script below as `make_epub.py`. **Copy it exactly, character for character.** Do not retype it from memory and do not shorten it. In an earlier test an assistant mistyped one address inside it (the package namespace, which must be `http://www.idpf.org/2007/opf`) and the book came out with a broken package file that some readers cannot open. Every string that looks like a web address must stay exactly as written. Fill in the `BOOK` block at the top (title, author, rights, edition, look, description, who made it, the from-memory list, and every chapter). For a long book, write each chapter to its own file (`ch01.md`, `ch02.md`) and give `"md_file": "ch01.md"` instead of `"md"`, so no single step is too long. Run it with `python3 make_epub.py`. It checks its own work (every page is well-formed, every image has alt text) and tells you what is wrong in plain words. Fix that and run it again. It prints the file name and the identifier. If you cannot run code, or cannot give the person a file: say so plainly. Then write the chapters in the Markdown subset above, and give them to the person as text in the chat, with the BOOK block filled in. Tell them this is not an EPUB yet, and that a free converter such as pandoc, or another assistant that can run code, can make one from it. The script (tested with Python 3 on 2026-11-13; it has not been tried in every assistant's sandbox): ```python #!/usr/bin/env python3 # cbook book builder. Text in, EPUB 3 out. Python 3 standard library only. # Fill in BOOK below, then run: python3 make_epub.py import datetime, html, os, re, sys, uuid, zipfile import xml.dom.minidom BOOK = { "title": "Title of the book", "subtitle": "", # may be empty "author": "Author name", "language": "en-US", "identifier": None, # None makes a new one. For a later edition of the SAME book paste the old one here. "look": "plain", # plain, story, guide or convert (see the guide, section 5) "description": "One or two sentences about the book.", "subject": "", # a few words, such as "space history" "rights": "(c) 2026 Author name. All rights reserved.", "edition": "Version 1", "made_by": "Written by Author name with an AI assistant (name the assistant).", "review": "Not reviewed by a subject expert.", "from_memory": [], # statements that came from memory and were not checked against a source "cover": None, # path to a JPEG or PNG, or None "chapters": [ # Each chapter is {"title": "1. Name of the chapter", "md": "text in the Markdown subset described in the guide"} # or, for a long chapter kept in its own file: {"title": "...", "md_file": "ch01.md"} {"title": "About this book", "md": "What this book is, who made it, and how far to trust it.\n\nThis is a sample paragraph with **bold**, *italic* and a [link](https://example.org)."}, {"title": "1. First chapter", "md": "## A section\n\nA paragraph of text.\n\n- A point.\n- Another point.\n\n## Sources for this chapter\n\n- [Example source](https://example.org), 2026."}, ], } # ---------- Markdown subset to XHTML ---------- esc = lambda t: html.escape(t, quote=False) attr = lambda t: html.escape(t, quote=True) def inline(t): t = esc(t) t = re.sub(r'\[([^\]]+)\]\((https?://(?:[^()\s"]|\([^()\s"]*\))+|#[A-Za-z0-9_-]+)\)', r'\1', t) t = re.sub(r'\*\*(.+?)\*\*', r'\1', t) t = re.sub(r'(?\1', t) return t MEDIA = {'.jpg': 'image/jpeg', '.jpeg': 'image/jpeg', '.png': 'image/png', '.gif': 'image/gif', '.svg': 'image/svg+xml'} class Ctx: def __init__(self): self.n = 0; self.last = 1; self.images = {} # images: zip name -> source path; last: level of the last heading def hid(self): self.n += 1; return 'h%d' % self.n def convert(md, ctx, heads): lines = md.strip('\n').split('\n'); out = []; para = []; i = 0; in_sources = False def flush(): if para: out.append('

' + inline(' '.join(para)) + '

'); para.clear() while i < len(lines): ln = lines[i] if not ln.strip(): flush(); i += 1; continue if re.match(r'#\s', ln): raise ValueError('Use ## and ### inside a chapter. The chapter title comes from the title field: ' + ln) m = re.match(r'(#{2,4})\s+(.+?)\s*$', ln) if m: flush(); lvl = min(len(m.group(1)), ctx.last + 1); ctx.last = lvl; text = m.group(2) # never skip a level if text.lower().startswith('sources for this chapter'): if not in_sources: out.append('
'); in_sources = True out.append('%s' % (lvl, inline(text), lvl)) else: hid = ctx.hid(); heads.append((lvl, text, hid)); out.append('%s' % (lvl, hid, inline(text), lvl)) i += 1; continue m = re.match(r':::note\s*(.*)$', ln) if m: flush(); title = m.group(1).strip(); body = []; i += 1 while i < len(lines) and lines[i].strip() != ':::': body.append(lines[i]); i += 1 i += 1 out.append('') continue m = re.match(r'!\[([^\]]*)\]\(([^)\s"]+)(?:\s+"([^"]*)")?\)\s*$', ln) if m: flush(); alt, path, cap = m.group(1).strip(), m.group(2), (m.group(3) or '').strip() if not alt: raise ValueError('Every image needs alt text: ' + ln) if not cap: raise ValueError('Every image needs a caption, written as ![alt](path "caption"): ' + ln) if not os.path.isfile(path): raise ValueError('Image file not found: ' + path) ext = os.path.splitext(path)[1].lower() if ext not in MEDIA: raise ValueError('Image must be jpg, png, gif or svg: ' + path) name = 'media/img%03d%s' % (len(ctx.images) + 1, ext); ctx.images[name] = path out.append('
%s
%s
' % (name, attr(alt), inline(cap))) i += 1; continue if re.match(r'(- |\d+\.\s)', ln): flush(); tag = 'ul' if ln.startswith('- ') else 'ol'; items = [] while i < len(lines) and re.match(r'(- |\d+\.\s)', lines[i]): item = [re.sub(r'^(- |\d+\.\s)', '', lines[i])]; i += 1 while i < len(lines) and lines[i].startswith(' ') and lines[i].strip(): item.append(lines[i].strip()); i += 1 items.append(' '.join(item)) out.append('<%s>' % tag + ''.join('
  • %s
  • ' % inline(x) for x in items) + '' % tag); continue if ln.startswith('>'): flush(); q = [] while i < len(lines) and lines[i].startswith('>'): q.append(lines[i].lstrip('> ').rstrip()); i += 1 out.append('

    %s

    ' % inline(' '.join(q))); continue para.append(ln.strip()); i += 1 flush() if in_sources: out.append('
    ') return '\n'.join(out) # ---------- package ---------- CSS = """body { font-family: Georgia, "Times New Roman", serif; line-height: 1.5; margin: 0 5%; } h1 { font-size: 1.7em; line-height: 1.2; margin: 0.4em 0 1.2em; text-align: left; } p { margin: 0 0 0.9em; text-align: left; } ul, ol { margin: 0.6em 0 1em 1.2em; padding: 0; } li { margin-bottom: 0.5em; } blockquote { margin: 1.2em 1.5em; font-style: italic; } blockquote p { margin: 0; } figure { margin: 1.6em 0; text-align: center; } figure img { max-width: 100%; height: auto; } figcaption { font-size: 0.85em; line-height: 1.4; margin-top: 0.5em; text-align: left; font-style: italic; } aside.note { margin: 1.5em 0; padding: 0.7em 1em; border-left: 4px solid #c58a00; background: rgba(128,128,128,0.12); } aside.note p { margin: 0.3em 0; font-size: 0.95em; } .sources { margin-top: 2.5em; border-top: 1px solid #999; padding-top: 0.3em; } .sources li { font-size: 0.85em; margin-bottom: 0.3em; } nav ol, ol.toc { list-style: none; padding-left: 0; } nav ol ol { padding-left: 1.2em; } nav li { margin: 0.4em 0; } h2, h3, h4 { break-after: avoid; page-break-after: avoid; } p, li { orphans: 2; widows: 2; } figure, aside, blockquote, table { break-inside: avoid; page-break-inside: avoid; } """ def page(title, body): return ('\n\n\n' '%s\n\n%s\n\n\n') % (B['language'], B['language'], esc(title), body) def build(): global B B = BOOK for k in ('title', 'author', 'language', 'look', 'description', 'rights', 'edition', 'made_by', 'review'): if not str(B.get(k, '')).strip(): raise ValueError('BOOK["%s"] is empty' % k) if B['look'] not in ('plain', 'story', 'guide', 'convert'): raise ValueError('look must be plain, story, guide or convert') ident = B.get('identifier') or 'urn:uuid:' + str(uuid.uuid4()) if not re.match(r'urn:uuid:[0-9a-f-]{36}$', ident): raise ValueError('identifier must look like urn:uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx') now = datetime.datetime.now(datetime.timezone.utc); stamp = now.strftime('%Y-%m-%dT%H:%M:%SZ'); today = now.strftime('%Y-%m-%d') ctx = Ctx(); files = {}; chap = [] # chap: (file, title, heads) # title page sub = '

    %s

    ' % esc(B['subtitle']) if B.get('subtitle') else '' files['text/title_page.xhtml'] = page(B['title'], '

    %s

    %s

    %s

    ' % (esc(B['title']), sub, esc(B['author']))) # chapters for n, c in enumerate(B['chapters'], 1): ctx.last = 1; heads = []; md = open(c['md_file'], encoding='utf-8').read() if c.get('md_file') else c['md']; body = convert(md, ctx, heads); name = 'text/ch%03d.xhtml' % n files[name] = page(c['title'], '

    %s

    \n%s' % (inline(c['title']), body)); chap.append((name, c['title'], heads)) # colophon mem = ''.join('
  • %s
  • ' % inline(x) for x in B.get('from_memory', [])) colo = ('

    How this edition was made

    %s

    %s Built %s with a Python script from the cbook authoring guide (standard library only).

    ' '

    %s

    %s

    Look: %s. Identifier: %s.

    %s

    ') % ( inline(B['made_by']), esc(B['edition']) + '.', today, inline(B['review']), ('

    Statements from memory that were not checked against a source:

    ' % mem) if mem else '

    Nothing was marked as unchecked memory.

    ', esc(B['look']), esc(ident), inline(B['rights'])) files['text/colophon.xhtml'] = page('How this edition was made', colo) chap.append(('text/colophon.xhtml', 'How this edition was made', [])) # contents def li(f, t, hs): sub = ''.join('
  • %s
  • ' % (f, hid, inline(tx)) for lv, tx, hid in hs if lv == 2) return '
  • %s%s
  • ' % (f, inline(t), '
      ' + sub + '
    ' if sub else '') toc = ''.join(li(f, t, hs) for f, t, hs in chap) nav = ('\n\n\n' 'Contents\n\n' '\n\n\n') % (B['language'], B['language'], toc) order = [('text/title_page.xhtml', B['title'])] + [(f, t) for f, t, _ in chap] ncx_pts = ''.join('%s' % (k, k, esc(re.sub(r'<[^>]+>', '', t)), f) for k, (f, t) in enumerate(order, 1)) ncx = ('\n' '' '%s%s\n') % (B['language'], ident, esc(B['title']), ncx_pts) # manifest items = ['', '', ''] spine = ['', ''] items.append('') for k, (f, t, _) in enumerate(chap, 1): iid = 'colophon' if f.endswith('colophon.xhtml') else 'ch%03d' % k items.append('' % (iid, f)); spine.append('' % iid) for name, path in ctx.images.items(): items.append('' % (os.path.basename(name).replace('.', '_'), name, MEDIA[os.path.splitext(name)[1]])) cover_meta = '' if B.get('cover'): ext = os.path.splitext(B['cover'])[1].lower() if ext not in ('.jpg', '.jpeg', '.png') or not os.path.isfile(B['cover']): raise ValueError('cover must be an existing JPEG or PNG file') cname = 'media/cover' + ext; ctx.images[cname] = B['cover'] items.append('' % (cname, MEDIA[ext])); cover_meta = '' opf = ('\n\n' '\n%s\n%s\n%s\n%s\n' '%s\n%s%s\n%s\n%s\n%s' '%s\n%s\n%s\n%s\ntext/colophon.xhtml\n\n' '\n%s\n\n\n%s\n\n\n') % ( B['language'], ident, esc(B['title']), esc(B['author']), esc(B['language']), esc(B['description']), ('%s\n' % esc(B['subject'])) if B.get('subject') else '', esc(B['rights']), today, stamp, cover_meta, esc(B['edition']), today, B['look'], esc(B['made_by'] + ' ' + B['review']), '\n'.join(items), '\n'.join(spine)) # check every page is well formed, ids are unique, and images have alt text, before writing for name, text in list(files.items()) + [('nav.xhtml', nav), ('toc.ncx', ncx), ('content.opf', opf)]: try: xml.dom.minidom.parseString(text.encode('utf-8')) except Exception as e: raise ValueError('%s is not well-formed XML: %s' % (name, e)) if re.search(r']*\balt="[^"]+")', text): raise ValueError('an image in %s has no alt text' % name) out = os.environ.get('OUT') or re.sub(r'[^A-Za-z0-9]+', '_', B['title']).strip('_')[:60] + '_' + re.sub(r'[^A-Za-z0-9]+', '_', B['edition']).strip('_') + '.epub' with zipfile.ZipFile(out, 'w') as z: z.writestr(zipfile.ZipInfo('mimetype'), 'application/epub+zip', compress_type=zipfile.ZIP_STORED) def w(name, data): z.writestr(name, data, compress_type=zipfile.ZIP_DEFLATED) w('META-INF/container.xml', '\n\n') w('EPUB/content.opf', opf); w('EPUB/nav.xhtml', nav); w('EPUB/toc.ncx', ncx); w('EPUB/style.css', CSS) for name, text in files.items(): w('EPUB/' + name, text) for name, path in ctx.images.items(): z.write(path, 'EPUB/' + name, compress_type=zipfile.ZIP_DEFLATED) words = sum(len(re.sub(r'<[^>]+>', ' ', t).split()) for t in files.values()) print('Wrote', out); print('Identifier (keep it for later editions of this book):', ident) print('%d chapters, about %d words, %d images' % (len(B['chapters']), words, len(ctx.images) - (1 if B.get('cover') else 0))) return out if __name__ == '__main__': try: build() except ValueError as e: sys.exit('Could not build: %s' % e) ``` ### A cover A book with a cover has a face on the reader's shelf. Unless the person has a cover image of their own (it must be theirs to use), make a simple one. It needs Pillow (the `PIL` module), which most assistants' Python already has. If it is not available, skip the cover and tell the person the shelf will show the title as text instead. Order matters. In the BOOK block of `make_epub.py` set `"cover": "cover.jpg"`. Then save the script below as `make_cover.py` and run `python3 make_cover.py`. It writes `cover.jpg` from the title, subtitle, author and look in the BOOK block. If you can look at the picture, check that the title is whole and fits. Then run `make_epub.py`. Copy this script exactly, too. ```python # Makes cover.jpg (1600 by 2400) from the BOOK block in make_epub.py. Needs Pillow (PIL). Run it before make_epub.py. import os from PIL import Image, ImageDraw, ImageFont from make_epub import BOOK W, H = 1600, 2400 PALETTE = {'plain': ((34, 49, 63), (14, 22, 30)), 'story': ((92, 28, 40), (36, 10, 16)), 'guide': ((34, 78, 60), (12, 32, 24)), 'convert': ((70, 70, 74), (28, 28, 30))} CREAM, GOLD = (243, 232, 213), (214, 168, 70) FONTS = ['/usr/share/fonts/truetype/dejavu/DejaVuSerif-Bold.ttf', '/usr/share/fonts/TTF/DejaVuSerif-Bold.ttf', '/usr/share/fonts/truetype/liberation/LiberationSerif-Bold.ttf', '/usr/share/fonts/TTF/LiberationSerif-Bold.ttf', '/System/Library/Fonts/Supplemental/Georgia Bold.ttf', 'C:/Windows/Fonts/georgiab.ttf', 'DejaVuSerif-Bold.ttf'] def font(size): for f in FONTS: try: return ImageFont.truetype(f, size) except OSError: pass return ImageFont.load_default(size) # a plain font, if none of the above is installed def wrap(draw, text, fnt, width): lines, line = [], '' for word in text.split(): trial = (line + ' ' + word).strip() if draw.textlength(trial, font=fnt) <= width or not line: line = trial else: lines.append(line); line = word return lines + ([line] if line else []) top, bottom = PALETTE.get(BOOK['look'], PALETTE['plain']) im = Image.new('RGB', (W, H)); d = ImageDraw.Draw(im) for y in range(H): # a soft top-to-bottom gradient t = y / (H - 1); d.line([(0, y), (W, y)], fill=tuple(round(top[i] + (bottom[i] - top[i]) * t) for i in range(3))) size = 170 while True: # the biggest title size that fits in six lines f = font(size); lines = wrap(d, BOOK['title'], f, W - 260) if len(lines) <= 6 or size <= 70: break size -= 10 y = 520 for ln in lines: d.text((W / 2, y), ln, font=f, fill=CREAM, anchor='ma'); y += int(size * 1.25) y += 50; d.line([(W / 2 - 220, y), (W / 2 + 220, y)], fill=GOLD, width=6); y += 90 if BOOK.get('subtitle'): f2 = font(70) for ln in wrap(d, BOOK['subtitle'], f2, W - 360): d.text((W / 2, y), ln, font=f2, fill=CREAM, anchor='ma'); y += 95 d.text((W / 2, H - 330), BOOK['author'], font=font(84), fill=GOLD, anchor='ma') im.save('cover.jpg', quality=88, optimize=True) print('Wrote cover.jpg. Now set "cover": "cover.jpg" in the BOOK block and run make_epub.py.') ``` ## 7. Before you hand it over Run the short check below on the finished file. It is separate from the builder on purpose, so a slip in the builder is caught. Save it as `verify_epub.py` and run `python3 verify_epub.py YourBook.epub`. If it stops with an error, fix the cause (usually a mistyped string in `make_epub.py`), rebuild and run it again. Do not hand over a file that does not pass. ```python import sys, zipfile import xml.etree.ElementTree as ET OPF, DC = '{http://www.idpf.org/2007/opf}', '{http://purl.org/dc/elements/1.1/}' z = zipfile.ZipFile(sys.argv[1]); names = z.namelist() assert names[0] == 'mimetype' and z.getinfo('mimetype').compress_type == 0, 'mimetype must be the first file and stored' assert z.read('mimetype') == b'application/epub+zip', 'mimetype has the wrong text' c = ET.fromstring(z.read('META-INF/container.xml')) path = c.find('.//{urn:oasis:names:tc:opendocument:xmlns:container}rootfile').get('full-path') pkg = ET.fromstring(z.read(path)) assert pkg.tag == OPF + 'package', 'wrong package namespace: ' + pkg.tag md = pkg.find(OPF + 'metadata') for t in ('identifier', 'title', 'language', 'creator'): assert md.find(DC + t) is not None, 'missing dc:' + t base = path.rsplit('/', 1)[0] + '/' if '/' in path else '' for item in pkg.iter(OPF + 'item'): assert base + item.get('href') in names, 'a listed file is missing: ' + item.get('href') for n in names: if n.endswith(('.xhtml', '.opf', '.ncx')): ET.fromstring(z.read(n)) print('EPUB check passed:', len(names), 'files') ``` Then check these, and fix what is wrong. - Every chapter has the title and a few paragraphs, and the contents list matches the book. - No `[?]`, `TODO` or placeholder text is left. The "About this book" chapter says who made the book, what it is from, and the review status. - Every factual claim has a source or is on the `from_memory` list. Every link is one you looked at. - Every picture has a caption, alt text, and clear rights. The cover shows the right title and author. - The title and the identifier are the ones for this book. Then give the person the file and a short report in plain words: 1. The file name, the title, and the identifier. **Tell the person to keep the identifier**: for a later edition of the same book, they give it to you again (it goes in `"identifier"` in the BOOK block) and keep the same title, so the reader treats the new file as the same book and carries their notes across. Without it a new file is treated as a different book. 2. The number of chapters and the length, set against what was agreed. If it is short, say so. 3. What came from memory and was not checked, and any links you could not open again before building (name each one). 4. What you are least sure of, and what they should check before sharing the book with anyone. 5. How to open it: in the cbook reader, tap **Add books** in the Library and choose the file. In other EPUB readers, open the file as usual. ## 8. Later editions, and forks - A later edition of the same book: same title, same identifier, a new `edition` ("Version 2"). The reader keeps the person's notes and finds each marked passage again by its text. Avoid rewording passages without a reason, because a rewritten passage's note cannot be found again. - A fork (a separate version that should have its own notes and its own card): a new title and a new identifier. If only one of the two changes, the reader treats it as a different book. - The newer of two files of the same book is the one the reader keeps. The builder writes the build time into the file, so a rebuild is newer. ## 9. What this guide does not cover Books from Word files, equations, wide tables, embedded fonts, and checks in readers other than the cbook reader were not tried with this short guide. Books from PDFs and scans have their own companion guide, "Turning a PDF into a cbook book", which the person attaches together with this one. A fuller guide, for an assistant that can run tools on the author's own machine, exists. If the person wants something this guide does not cover, tell them what you can and cannot do instead of guessing.