From 6dcfd59f73d756048ced9de4100f8b520cdb410e Mon Sep 17 00:00:00 2001 From: Antigravity Date: Tue, 6 Oct 2026 10:16:40 +0300 Subject: [PATCH] feat(reader): add PDF reading support with outline chapters, CLI blocks, and noise filtering (task 7) --- TODO.md | 18 +- app/main.py | 56 +- app/reader.py | 317 + app/static/index.html | 10 +- app/static/js/app.js | 4 +- app/vendor/pypdf/__init__.py | 53 + app/vendor/pypdf/_cmap.py | 509 + app/vendor/pypdf/_codecs/__init__.py | 59 + app/vendor/pypdf/_codecs/_codecs.py | 284 + app/vendor/pypdf/_codecs/adobe_glyphs.py | 17307 ++++++++++++++++ app/vendor/pypdf/_codecs/core_font_metrics.py | 4483 ++++ app/vendor/pypdf/_codecs/pdfdoc.py | 264 + app/vendor/pypdf/_codecs/std.py | 258 + app/vendor/pypdf/_codecs/symbol.py | 281 + app/vendor/pypdf/_codecs/zapfding.py | 281 + app/vendor/pypdf/_configuration.py | 296 + app/vendor/pypdf/_crypt_providers/__init__.py | 80 + app/vendor/pypdf/_crypt_providers/_base.py | 38 + .../pypdf/_crypt_providers/_cryptography.py | 184 + .../pypdf/_crypt_providers/_fallback.py | 93 + .../pypdf/_crypt_providers/_pycryptodome.py | 110 + app/vendor/pypdf/_doc_common.py | 1646 ++ app/vendor/pypdf/_encryption.py | 1305 ++ app/vendor/pypdf/_font.py | 884 + app/vendor/pypdf/_page.py | 2652 +++ app/vendor/pypdf/_page_labels.py | 338 + app/vendor/pypdf/_protocols.py | 90 + app/vendor/pypdf/_reader.py | 1649 ++ app/vendor/pypdf/_text_extraction/__init__.py | 269 + .../_text_extraction/_layout_mode/__init__.py | 16 + .../_layout_mode/_fixed_width_page.py | 447 + .../_layout_mode/_text_state_manager.py | 204 + .../_layout_mode/_text_state_params.py | 161 + .../pypdf/_text_extraction/_text_extractor.py | 360 + app/vendor/pypdf/_utils.py | 690 + app/vendor/pypdf/_version.py | 1 + app/vendor/pypdf/_writer.py | 3520 ++++ app/vendor/pypdf/actions/__init__.py | 17 + app/vendor/pypdf/actions/_actions.py | 217 + app/vendor/pypdf/annotations/__init__.py | 42 + app/vendor/pypdf/annotations/_base.py | 29 + .../pypdf/annotations/_markup_annotations.py | 351 + .../annotations/_non_markup_annotations.py | 107 + app/vendor/pypdf/constants.py | 828 + app/vendor/pypdf/errors.py | 74 + app/vendor/pypdf/filters.py | 892 + app/vendor/pypdf/generic/__init__.py | 115 + .../pypdf/generic/_appearance_stream.py | 894 + app/vendor/pypdf/generic/_base.py | 968 + app/vendor/pypdf/generic/_color.py | 85 + app/vendor/pypdf/generic/_data_structures.py | 1900 ++ app/vendor/pypdf/generic/_files.py | 465 + app/vendor/pypdf/generic/_fit.py | 175 + app/vendor/pypdf/generic/_image_inline.py | 365 + app/vendor/pypdf/generic/_image_xobject.py | 730 + app/vendor/pypdf/generic/_link.py | 150 + app/vendor/pypdf/generic/_outline.py | 33 + app/vendor/pypdf/generic/_rectangle.py | 135 + app/vendor/pypdf/generic/_utils.py | 225 + app/vendor/pypdf/generic/_viewerpref.py | 185 + app/vendor/pypdf/pagerange.py | 220 + app/vendor/pypdf/papersizes.py | 52 + app/vendor/pypdf/py.typed | 0 app/vendor/pypdf/types.py | 80 + app/vendor/pypdf/xmp.py | 812 + requirements.txt | 1 + tests/test_api.py | 120 + 67 files changed, 49444 insertions(+), 40 deletions(-) create mode 100644 app/vendor/pypdf/__init__.py create mode 100644 app/vendor/pypdf/_cmap.py create mode 100644 app/vendor/pypdf/_codecs/__init__.py create mode 100644 app/vendor/pypdf/_codecs/_codecs.py create mode 100644 app/vendor/pypdf/_codecs/adobe_glyphs.py create mode 100644 app/vendor/pypdf/_codecs/core_font_metrics.py create mode 100644 app/vendor/pypdf/_codecs/pdfdoc.py create mode 100644 app/vendor/pypdf/_codecs/std.py create mode 100644 app/vendor/pypdf/_codecs/symbol.py create mode 100644 app/vendor/pypdf/_codecs/zapfding.py create mode 100644 app/vendor/pypdf/_configuration.py create mode 100644 app/vendor/pypdf/_crypt_providers/__init__.py create mode 100644 app/vendor/pypdf/_crypt_providers/_base.py create mode 100644 app/vendor/pypdf/_crypt_providers/_cryptography.py create mode 100644 app/vendor/pypdf/_crypt_providers/_fallback.py create mode 100644 app/vendor/pypdf/_crypt_providers/_pycryptodome.py create mode 100644 app/vendor/pypdf/_doc_common.py create mode 100644 app/vendor/pypdf/_encryption.py create mode 100644 app/vendor/pypdf/_font.py create mode 100644 app/vendor/pypdf/_page.py create mode 100644 app/vendor/pypdf/_page_labels.py create mode 100644 app/vendor/pypdf/_protocols.py create mode 100644 app/vendor/pypdf/_reader.py create mode 100644 app/vendor/pypdf/_text_extraction/__init__.py create mode 100644 app/vendor/pypdf/_text_extraction/_layout_mode/__init__.py create mode 100644 app/vendor/pypdf/_text_extraction/_layout_mode/_fixed_width_page.py create mode 100644 app/vendor/pypdf/_text_extraction/_layout_mode/_text_state_manager.py create mode 100644 app/vendor/pypdf/_text_extraction/_layout_mode/_text_state_params.py create mode 100644 app/vendor/pypdf/_text_extraction/_text_extractor.py create mode 100644 app/vendor/pypdf/_utils.py create mode 100644 app/vendor/pypdf/_version.py create mode 100644 app/vendor/pypdf/_writer.py create mode 100644 app/vendor/pypdf/actions/__init__.py create mode 100644 app/vendor/pypdf/actions/_actions.py create mode 100644 app/vendor/pypdf/annotations/__init__.py create mode 100644 app/vendor/pypdf/annotations/_base.py create mode 100644 app/vendor/pypdf/annotations/_markup_annotations.py create mode 100644 app/vendor/pypdf/annotations/_non_markup_annotations.py create mode 100644 app/vendor/pypdf/constants.py create mode 100644 app/vendor/pypdf/errors.py create mode 100644 app/vendor/pypdf/filters.py create mode 100644 app/vendor/pypdf/generic/__init__.py create mode 100644 app/vendor/pypdf/generic/_appearance_stream.py create mode 100644 app/vendor/pypdf/generic/_base.py create mode 100644 app/vendor/pypdf/generic/_color.py create mode 100644 app/vendor/pypdf/generic/_data_structures.py create mode 100644 app/vendor/pypdf/generic/_files.py create mode 100644 app/vendor/pypdf/generic/_fit.py create mode 100644 app/vendor/pypdf/generic/_image_inline.py create mode 100644 app/vendor/pypdf/generic/_image_xobject.py create mode 100644 app/vendor/pypdf/generic/_link.py create mode 100644 app/vendor/pypdf/generic/_outline.py create mode 100644 app/vendor/pypdf/generic/_rectangle.py create mode 100644 app/vendor/pypdf/generic/_utils.py create mode 100644 app/vendor/pypdf/generic/_viewerpref.py create mode 100644 app/vendor/pypdf/pagerange.py create mode 100644 app/vendor/pypdf/papersizes.py create mode 100644 app/vendor/pypdf/py.typed create mode 100644 app/vendor/pypdf/types.py create mode 100644 app/vendor/pypdf/xmp.py diff --git a/TODO.md b/TODO.md index fb49cee..f874aa5 100644 --- a/TODO.md +++ b/TODO.md @@ -155,22 +155,22 @@ --- -### 7. 📑 Поддержка чтения PDF (Cisco CCNA, O'Reilly & Tech Books с оглавлением и текстовым слоем) -- [ ] **Легковесный Pure-Python парсер (`pypdf`, Zero C-Dependency)**: +### 7. 📑 Поддержка чтения PDF (Cisco CCNA, O'Reilly & Tech Books с оглавлением и текстовым слоем) (Completed) +- [x] **Легковесный Pure-Python парсер (`pypdf`, Zero C-Dependency)**: - Подключение `pypdf` (< 400 КБ, чистый Python, без раздувания Docker-образа и без системных библиотек `poppler-utils` / `mupdf`). -- [ ] **Извлечение оглавления и закладок PDF (`reader.outline`)**: +- [x] **Извлечение оглавления и закладок PDF (`reader.outline`)**: - Извлечение авторского оглавления (ToC / Bookmarks), стандартного для книг Cisco Press (CCNA Official Cert Guide), O'Reilly и Wiley. - Разбивка книги на логические главы и разделы (`Part I`, `Chapter 1`, `Chapter 2`) точно по оглавлению автора. -- [ ] **Fallback-разбивка при отсутствии оглавления**: +- [x] **Fallback-разбивка при отсутствии оглавления**: - При отсутствии PDF-закладок — автоопределение заголовков через regex (`Chapter \d+`, `Глава \d+`) или группировка страниц порциями по ~12–15 страниц. -- [ ] **Очистка шума и колонтитулов**: +- [x] **Очистка шума и колонтитулов**: - Фильтрация повторяющихся верхних/нижних колонтитулов («CCNA 200-301 Official Cert Guide», «Page 45 of 900»). -- [ ] **Сохранение CLI/Console блоков**: +- [x] **Сохранение CLI/Console блоков**: - Распознавание консольного вывода и команд сетевого оборудования (`Router# show ip interface brief`, `Switch(config)#`) и оборачивание в блоки кода Markdown для читалки с кнопкой «Копировать». -- [ ] **Защита от сканов без OCR (Scanned PDF Guard)**: +- [x] **Защита от сканов без OCR (Scanned PDF Guard)**: - Валидация наличия текстового слоя: если суммарный объём извлечённого текста < 100 слов, возврат информативной ошибки с предложением использовать текстовый PDF или OCR. -- [ ] **Интеграция в интерфейс читалки**: - - Разрешение загрузки `.pdf` в ``. +- [x] **Интеграция в интерфейс читалки**: + - Разрешение загрузки `.pdf` в ``. - Обновление кнопки в библиотеке: `📁 Загрузить EPUB / PDF / TXT`. --- diff --git a/app/main.py b/app/main.py index f271a2b..edee002 100644 --- a/app/main.py +++ b/app/main.py @@ -28,7 +28,7 @@ from app.auth import ( from app.dictionary import lookup_term, USER_AGENT from app.export import export_csv, export_json, export_anki_tsv, export_markdown from app.srs import calculate_sm2, generate_quiz_options, build_activity_heatmap, calculate_cefr_level -from app.reader import parse_epub, parse_text, count_words +from app.reader import parse_epub, parse_text, parse_pdf, count_words @asynccontextmanager @@ -1088,29 +1088,37 @@ async def api_create_book( stem = Path(filename).stem lower_name = filename.lower() - if lower_name.endswith(".epub"): - source_type = "epub" - parsed_title, parsed_author, chapters = parse_epub(file_bytes) - title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem - author = form_author or parsed_author - elif lower_name.endswith(".md"): - source_type = "md" - text_str = file_bytes.decode("utf-8", errors="replace") - parsed_title, parsed_author, chapters = parse_text(text_str, title=form_title or "Untitled", author=form_author) - title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem - author = form_author or parsed_author - elif lower_name.endswith(".txt"): - source_type = "txt" - text_str = file_bytes.decode("utf-8", errors="replace") - parsed_title, parsed_author, chapters = parse_text(text_str, title=form_title or "Untitled", author=form_author) - title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem - author = form_author or parsed_author - else: - source_type = "text" - text_str = file_bytes.decode("utf-8", errors="replace") - parsed_title, parsed_author, chapters = parse_text(text_str, title=form_title or "Untitled", author=form_author) - title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem - author = form_author or parsed_author + try: + if lower_name.endswith(".epub"): + source_type = "epub" + parsed_title, parsed_author, chapters = parse_epub(file_bytes) + title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem + author = form_author or parsed_author + elif lower_name.endswith(".pdf"): + source_type = "pdf" + parsed_title, parsed_author, chapters = parse_pdf(file_bytes, filename=filename) + title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem + author = form_author or parsed_author + elif lower_name.endswith(".md"): + source_type = "md" + text_str = file_bytes.decode("utf-8", errors="replace") + parsed_title, parsed_author, chapters = parse_text(text_str, title=form_title or "Untitled", author=form_author) + title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem + author = form_author or parsed_author + elif lower_name.endswith(".txt"): + source_type = "txt" + text_str = file_bytes.decode("utf-8", errors="replace") + parsed_title, parsed_author, chapters = parse_text(text_str, title=form_title or "Untitled", author=form_author) + title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem + author = form_author or parsed_author + else: + source_type = "text" + text_str = file_bytes.decode("utf-8", errors="replace") + parsed_title, parsed_author, chapters = parse_text(text_str, title=form_title or "Untitled", author=form_author) + title = form_title or (parsed_title if parsed_title != "Untitled" else "") or stem + author = form_author or parsed_author + except ValueError as ve: + raise HTTPException(status_code=400, detail=str(ve)) else: try: diff --git a/app/reader.py b/app/reader.py index 7f26db8..2c51b8a 100644 --- a/app/reader.py +++ b/app/reader.py @@ -1,9 +1,11 @@ import io import re +import sys import html import zipfile import posixpath import urllib.parse +from pathlib import Path import xml.etree.ElementTree as ET from html.parser import HTMLParser from typing import Tuple, List, Dict @@ -496,3 +498,318 @@ def parse_text(content: str, title: str = "Untitled", author: str = "") -> Tuple return title, author, chapters + +# --------------------------------------------------------------------------- +# PDF Parsing & Chapter Extraction (pypdf, Bookmarks/Outline, Cisco CLI Blocks) +# --------------------------------------------------------------------------- + +CLI_PROMPT_RE = re.compile( + r"^(?:[A-Za-z0-9_.\-]+(?:\([a-zA-Z0-9_\-\.\s]+\))?[>#]|" + r"[A-Za-z0-9_.\-]+@[A-Za-z0-9_.\-]+:[~/\w\.\-]*[$#]|" + r"(?:[A-Z]):\\[^>\n\r]*>|" + r"PS\s+[A-Z]:\\[^>\n\r]*>)" +) + + +def extract_outline_entries(outline, reader) -> List[Tuple[str, int]]: + """ + Recursively extracts bookmarks / outline entries from PDF. + Returns a list of (title, 0-indexed page_number). + """ + entries: List[Tuple[str, int]] = [] + + def _traverse(items): + for item in items: + if isinstance(item, list): + _traverse(item) + else: + try: + title = getattr(item, "title", None) or (item.get("/Title") if hasattr(item, "get") else None) + page_num = reader.get_destination_page_number(item) + if title and page_num is not None: + clean_title = str(title).strip() + if clean_title: + entries.append((clean_title, int(page_num))) + except Exception: + continue + + if outline: + _traverse(outline) + return entries + + +def clean_page_lines(raw_pages_text: List[str]) -> List[str]: + """ + Filters running headers, footers, isolated page numbers, and copyright noise. + """ + if not raw_pages_text: + return [] + + num_pages = len(raw_pages_text) + top_candidates: Dict[str, int] = {} + bottom_candidates: Dict[str, int] = {} + + for pt in raw_pages_text: + lines = [l.strip() for l in pt.split("\n") if l.strip()] + if not lines: + continue + if len(lines) >= 1: + top_candidates[lines[0]] = top_candidates.get(lines[0], 0) + 1 + bottom_candidates[lines[-1]] = bottom_candidates.get(lines[-1], 0) + 1 + + # Lines repeated on >= 20% of pages (min 3 pages) are running headers/footers + threshold = max(3, int(num_pages * 0.20)) + common_headers = {l for l, c in top_candidates.items() if c >= threshold} + common_footers = {l for l, c in bottom_candidates.items() if c >= threshold} + + page_num_re = re.compile(r"^(?:page\s+)?\d+(?:\s*(?:of|/)\s*\d+)?$", re.IGNORECASE) + url_re = re.compile(r"^(?:https?://|www\.)\S+$", re.IGNORECASE) + copyright_re = re.compile(r"^(?:©|\(c\)|copyright)\s*\d{4}.*$", re.IGNORECASE) + + cleaned_pages: List[str] = [] + for pt in raw_pages_text: + lines = pt.split("\n") + filtered: List[str] = [] + n_lines = len(lines) + for idx, l in enumerate(lines): + stripped = l.strip() + if not stripped: + filtered.append("") + continue + is_top = (idx <= 2) + is_bottom = (idx >= n_lines - 3) + + if (is_top or is_bottom) and page_num_re.match(stripped): + continue + if is_top and stripped in common_headers: + continue + if is_bottom and (stripped in common_footers or url_re.match(stripped) or copyright_re.match(stripped)): + continue + filtered.append(l) + cleaned_pages.append("\n".join(filtered)) + + return cleaned_pages + + +def format_cli_blocks(text: str) -> str: + """ + Detects Cisco IOS / CLI terminal sessions in text and wraps them + in Markdown fenced code blocks (```text ... ```) for syntax highlighting and copying. + """ + if not text: + return "" + + lines = text.split("\n") + out: List[str] = [] + in_cli = False + cli_lines: List[str] = [] + + def is_likely_cli_line(line: str) -> bool: + s = line.strip() + if not s: + return True + if CLI_PROMPT_RE.match(s): + return True + if s.startswith(("%", "!", "--", ">>", "Enter configuration", "Building configuration", "Current configuration")): + return True + if len(re.findall(r"\S\s{2,}\S", line)) >= 2: + return True + if re.match(r"^(?:show|ip|no|interface|switchport|vlan|router|network|exit|end|enable|disable|configure|ping|traceroute|debug)\s+", s, re.IGNORECASE): + return True + return False + + for line in lines: + s = line.strip() + if not in_cli: + if CLI_PROMPT_RE.match(s): + in_cli = True + cli_lines = [line] + else: + out.append(line) + else: + if is_likely_cli_line(line): + cli_lines.append(line) + else: + while cli_lines and not cli_lines[-1].strip(): + cli_lines.pop() + if cli_lines: + out.append("```text") + out.extend(cli_lines) + out.append("```") + in_cli = False + cli_lines = [] + out.append(line) + + if in_cli and cli_lines: + while cli_lines and not cli_lines[-1].strip(): + cli_lines.pop() + if cli_lines: + out.append("```text") + out.extend(cli_lines) + out.append("```") + + return "\n".join(out) + + +def parse_pdf(file_bytes: bytes, filename: str = "") -> Tuple[str, str, List[Dict[str, str]]]: + """ + Parses a PDF file into readable chapters: + - Extracts title and author from metadata or filename. + - Enforces Scanned PDF Guard (raises ValueError if total_words < 100). + - Cleans repetitive headers, footers, and page numbers. + - Wraps Cisco IOS / CLI terminal blocks in Markdown code blocks. + - Splits chapters by PDF outline / bookmarks (Cisco Press, O'Reilly). + - Fallback to heading regex or ~10-15 page chunks if no bookmarks exist. + """ + try: + import pypdf + except ImportError: + vendor_dir = Path(__file__).resolve().parent / "vendor" + if str(vendor_dir) not in sys.path: + sys.path.insert(0, str(vendor_dir)) + import pypdf + + try: + reader = pypdf.PdfReader(io.BytesIO(file_bytes)) + except Exception as e: + raise ValueError(f"Не удалось открыть PDF документ: {e}") + + if reader.is_encrypted: + try: + reader.decrypt("") + except Exception: + raise ValueError("Этот PDF документ защищен паролем. Пожалуйста, снимите пароль для чтения.") + + num_pages = len(reader.pages) + if num_pages == 0: + raise ValueError("PDF документ не содержит страниц") + + # Metadata extraction + meta = reader.metadata or {} + meta_title = getattr(meta, "title", None) or (meta.get("/Title") if hasattr(meta, "get") else None) or "" + meta_author = getattr(meta, "author", None) or (meta.get("/Author") if hasattr(meta, "get") else None) or "" + + stem = Path(filename).stem if filename else "Untitled Document" + clean_title = str(meta_title).strip() + if not clean_title or clean_title.lower() in {"untitled", "untitled document", "default", "temp", "output"}: + title = stem + else: + title = clean_title + + author = str(meta_author).strip() + + # Page-by-page text extraction + raw_pages_text: List[str] = [] + for p in reader.pages: + try: + pt = p.extract_text() or "" + except Exception: + pt = "" + raw_pages_text.append(pt) + + # Scanned PDF Guard (< 100 words) + total_words = sum(count_words(t) for t in raw_pages_text) + if total_words < 100: + raise ValueError( + "Этот PDF-документ содержит сканы или изображения без текстового слоя (OCR). " + "Пожалуйста, используйте документ с распознанным текстом." + ) + + # Clean noise lines (headers, footers, page numbers) + cleaned_pages = clean_page_lines(raw_pages_text) + + # Apply CLI formatting to each page + formatted_pages = [format_cli_blocks(p) for p in cleaned_pages] + + # Chapter splitting: + # 1. Try outline / bookmarks + outline_entries = extract_outline_entries(getattr(reader, "outline", None), reader) + chapters: List[Dict[str, str]] = [] + + if outline_entries: + valid_entries = [(t, p) for t, p in outline_entries if 0 <= p < num_pages] + valid_entries.sort(key=lambda x: x[1]) + + # Filter duplicates pointing to the same page + deduped_entries: List[Tuple[str, int]] = [] + for t, p in valid_entries: + if not deduped_entries or deduped_entries[-1][1] != p: + deduped_entries.append((t, p)) + + if deduped_entries: + first_p = deduped_entries[0][1] + if first_p > 0: + intro_content = "\n\n".join(formatted_pages[p].strip() for p in range(0, first_p) if formatted_pages[p].strip()) + if count_words(intro_content) >= 20: + is_ru = any(ord(c) > 127 for c in intro_content[:300]) + chapters.append({ + "title": "Введение" if is_ru else "Introduction", + "content": intro_content + }) + + for i, (entry_title, start_p) in enumerate(deduped_entries): + end_p = deduped_entries[i + 1][1] if i + 1 < len(deduped_entries) else num_pages + ch_content = "\n\n".join(formatted_pages[p].strip() for p in range(start_p, end_p) if formatted_pages[p].strip()) + if ch_content: + chapters.append({ + "title": entry_title, + "content": ch_content + }) + + # 2. Fallback: No outline found, check for page-level chapter headings + if not chapters: + chapter_heading_re = re.compile( + r"^\s*(?:chapter|глава|part|часть|section|раздел|module|модуль)\s+([0-9ivxlcdm]+)[\s:.\-–—\n]+(.*)$", + re.IGNORECASE + ) + detected_pages: List[Tuple[str, int]] = [] + for p_idx, p_text in enumerate(formatted_pages): + first_lines = [l.strip() for l in p_text.split("\n") if l.strip()][:3] + for l in first_lines: + m = chapter_heading_re.match(l) + if m: + h_title = l.strip("#").strip() + detected_pages.append((h_title, p_idx)) + break + + if detected_pages: + if detected_pages[0][1] > 0: + intro_content = "\n\n".join(formatted_pages[p].strip() for p in range(0, detected_pages[0][1]) if formatted_pages[p].strip()) + if count_words(intro_content) >= 20: + is_ru = any(ord(c) > 127 for c in intro_content[:300]) + chapters.append({ + "title": "Введение" if is_ru else "Introduction", + "content": intro_content + }) + + for i, (h_title, start_p) in enumerate(detected_pages): + end_p = detected_pages[i + 1][1] if i + 1 < len(detected_pages) else num_pages + ch_content = "\n\n".join(formatted_pages[p].strip() for p in range(start_p, end_p) if formatted_pages[p].strip()) + if ch_content: + chapters.append({ + "title": h_title, + "content": ch_content + }) + + # 3. Fallback: Group pages in chunks of 12-15 pages + if not chapters: + chunk_size = 12 + sample_text = "".join(formatted_pages[:3]) + is_ru = any(ord(c) > 127 for c in sample_text[:300]) + prefix = "Страницы" if is_ru else "Pages" + + for start_p in range(0, num_pages, chunk_size): + end_p = min(start_p + chunk_size, num_pages) + ch_content = "\n\n".join(formatted_pages[p].strip() for p in range(start_p, end_p) if formatted_pages[p].strip()) + if ch_content: + chapters.append({ + "title": f"{prefix} {start_p + 1}–{end_p}", + "content": ch_content + }) + + if not chapters: + full_content = "\n\n".join(p.strip() for p in formatted_pages if p.strip()) + chapters = [{"title": title, "content": full_content}] + + return title, author, chapters + diff --git a/app/static/index.html b/app/static/index.html index de7f02f..26abce6 100644 --- a/app/static/index.html +++ b/app/static/index.html @@ -5,7 +5,7 @@ Notes — Персональный тренажер и словарь английского языка - + @@ -534,8 +534,8 @@
@@ -544,7 +544,7 @@
@@ -1075,7 +1075,7 @@ - + diff --git a/app/static/js/app.js b/app/static/js/app.js index b233b85..2f4e739 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -2077,12 +2077,12 @@ document.addEventListener('DOMContentLoaded', () => { card.innerHTML = `
-
${b.source_type === 'epub' ? '📕' : '📄'}
+
${b.source_type === 'epub' ? '📕' : (b.source_type === 'pdf' ? '📑' : '📄')}

${escapeHtml(b.title)}

${escapeHtml(b.author || 'Неизвестный автор')}

- ${escapeHtml(b.source_type)} + ${escapeHtml((b.source_type || 'text').toUpperCase())} ${b.total_words ? b.total_words.toLocaleString() + ' слов' : ''}
diff --git a/app/vendor/pypdf/__init__.py b/app/vendor/pypdf/__init__.py new file mode 100644 index 0000000..e602500 --- /dev/null +++ b/app/vendor/pypdf/__init__.py @@ -0,0 +1,53 @@ +""" +pypdf is a free and open-source pure-python PDF library capable of splitting, +merging, cropping, and transforming the pages of PDF files. It can also add +custom data, viewing options, and passwords to PDF files. pypdf can retrieve +text and metadata from PDFs as well. + +You can read the full docs at https://pypdf.readthedocs.io/. +""" + +from ._configuration import Configuration, apply_configuration, get_configuration, overwrite_configuration +from ._crypt_providers import crypt_provider +from ._doc_common import DocumentInformation +from ._encryption import PasswordType +from ._page import PageObject, Transformation +from ._reader import PdfReader +from ._text_extraction import mult +from ._version import __version__ +from ._writer import ObjectDeletionFlag, PdfWriter +from .constants import ImageType +from .pagerange import PageRange, parse_filename_page_ranges +from .papersizes import PaperSize + +try: + import PIL + + pil_version = PIL.__version__ +except ImportError: + pil_version = "none" + +_debug_versions = ( + f"pypdf=={__version__}, {crypt_provider=}, PIL={pil_version}" +) + +__all__ = [ + "Configuration", + "DocumentInformation", + "ImageType", + "ObjectDeletionFlag", + "PageObject", + "PageRange", + "PaperSize", + "PasswordType", + "PdfReader", + "PdfWriter", + "Transformation", + "__version__", + "_debug_versions", + "apply_configuration", + "get_configuration", + "mult", + "overwrite_configuration", + "parse_filename_page_ranges", +] diff --git a/app/vendor/pypdf/_cmap.py b/app/vendor/pypdf/_cmap.py new file mode 100644 index 0000000..7c05299 --- /dev/null +++ b/app/vendor/pypdf/_cmap.py @@ -0,0 +1,509 @@ +import struct +from binascii import Error as BinasciiError +from binascii import unhexlify +from functools import partial +from io import BytesIO +from typing import Any, Union, cast + +from ._codecs import adobe_glyphs, charset_encoding +from ._utils import logger_error, logger_warning +from .errors import LimitReachedError +from .generic import ( + ArrayObject, + DecodedStreamObject, + DictionaryObject, + NullObject, + StreamObject, +) + +_predefined_cmap: dict[str, str] = { + "/Identity-H": "utf-16-be", + "/Identity-V": "utf-16-be", + "/GB-EUC-H": "gbk", + "/GB-EUC-V": "gbk", + "/GBpc-EUC-H": "gb2312", + "/GBpc-EUC-V": "gb2312", + "/GBK-EUC-H": "gbk", + "/GBK-EUC-V": "gbk", + "/GBK2K-H": "gb18030", + "/GBK2K-V": "gb18030", + "/ETen-B5-H": "cp950", + "/ETen-B5-V": "cp950", + "/ETenms-B5-H": "cp950", + "/ETenms-B5-V": "cp950", + "/UniCNS-UTF16-H": "utf-16-be", + "/UniCNS-UTF16-V": "utf-16-be", + "/UniGB-UTF16-H": "gb18030", + "/UniGB-UTF16-V": "gb18030", + # Japanese CMaps (PDF Reference 1.7, Appendix H) + "/90ms-RKSJ-H": "cp932", # Shift-JIS (JIS X 0208-1990), horizontal + "/90ms-RKSJ-V": "cp932", # Shift-JIS (JIS X 0208-1990), vertical + "/UniJIS-UTF16-H": "utf-16-be", # Unicode UTF-16BE -> JIS, horizontal + "/UniJIS-UTF16-V": "utf-16-be", # Unicode UTF-16BE -> JIS, vertical + # UCS2 in code +} + + +def get_encoding( + ft: DictionaryObject +) -> tuple[Union[str, dict[int, str]], dict[Any, Any]]: + encoding = _parse_encoding(ft) + map_dict, int_entry = _parse_to_unicode(ft) + + # Apply rule from PDF ref 1.7 §5.9.1, 1st bullet: + # if cmap not empty encoding should be discarded + # (here transformed into identity for those characters) + # If encoding is a string, it is expected to be an identity translation. + if isinstance(encoding, dict): + for x in int_entry: + if x <= 255: + encoding[x] = chr(x) + + return encoding, map_dict + + +def _parse_encoding( + ft: DictionaryObject +) -> Union[str, dict[int, str]]: + encoding: Union[str, list[str], dict[int, str]] = [] + # If ft["/Encoding"] exists, then use that for encoding. Otherwise, use StandardEncoding as a basis, + # and add what the embedded font file says, if present. See Table 114, PDF Reference 1.7 / 2.0 + if "/Encoding" not in ft: + if "/BaseFont" in ft and cast(str, ft["/BaseFont"]) in charset_encoding: + # This will match Symbol and ZapfDingBats + return dict( + zip(range(256), charset_encoding[cast(str, ft["/BaseFont"])]) + ) + + # Return StandardEncoding as fallback option. Note that a font's internal encoding can be used + # to overwrite this, which we do for Type1 fonts in _character_map_from_(cff_)type1_font_file. + return dict( + zip(range(256), charset_encoding["/StandardEncoding"]) + ) + + enc: Union[str, DictionaryObject, NullObject] = cast( + Union[str, DictionaryObject, NullObject], ft["/Encoding"].get_object() + ) + if isinstance(enc, str): + try: + # already done : enc = NameObject.unnumber(enc.encode()).decode() + # for #xx decoding + if enc in charset_encoding: + encoding = charset_encoding[enc].copy() + elif enc in _predefined_cmap: + encoding = _predefined_cmap[enc] + elif "-UCS2-" in enc: + encoding = "utf-16-be" + else: + raise Exception("not found") + except Exception: + logger_error("Advanced encoding %(encoding)s not implemented yet", source=__name__, encoding=enc) + encoding = enc + elif isinstance(enc, DictionaryObject) and "/BaseEncoding" in enc: + try: + encoding = charset_encoding[cast(str, enc["/BaseEncoding"])].copy() + except Exception: + logger_error( + "Advanced encoding %(encoding)s not implemented yet", + source=__name__, encoding=encoding + ) + encoding = charset_encoding["/StandardEncoding"].copy() + else: + encoding = charset_encoding["/StandardEncoding"].copy() + if isinstance(enc, DictionaryObject) and "/Differences" in enc: + x: int = 0 + o: Union[int, str] + differences = enc["/Differences"].get_object() + if not isinstance(differences, ArrayObject): + logger_warning( + "Font encoding differences are not an array: %(differences)s", + source=__name__, + differences=differences, + ) + differences = ArrayObject() + for o in differences: + if isinstance(o, int): + x = o + else: # isinstance(o, str): + try: + if x < len(encoding): + encoding[x] = adobe_glyphs[o] # type: ignore[index] + except Exception: + encoding[x] = o # type: ignore[index] + x += 1 + if isinstance(encoding, list): + encoding = dict(zip(range(256), encoding)) + return encoding + + +def _parse_to_unicode( + ft: DictionaryObject +) -> tuple[dict[Any, Any], list[int]]: + from ._font import HAS_FONTTOOLS # noqa: PLC0415 + + # We store all character mappings in map_dict. In map_dict[-1] we store the byte length + # of the character codes (or CIDs) encoded inside the ToUnicode stream. + map_dict: dict[Any, Any] = {} + + # We provide the list of cmap keys in int_entry to correct encoding later on in get_encoding(). + int_entry: list[int] = [] + + if "/ToUnicode" not in ft: + if ft.get("/Subtype", "") == "/Type1": + font_descriptor = ft.get("/FontDescriptor") + if not font_descriptor: + return map_dict, int_entry + + # We try to read encoding from an embedded font file, if we can. See Table 126 about embedded font + # file organization in the PDF specification 1.7 for details. + font_file_handlers = ( + # A normal Type1 font file, can be part of a Type1 or MMType1 font dictionary. + ( + "/FontFile", + lambda _: True, + _character_map_from_type1_font_file + ), + # A CFF Type1 font file, as part of a Type1 or MMType1 font dictionary, when subtype is Type1C. + ( + "/FontFile3", + lambda stream: stream.get("/Subtype") == "/Type1C", + _character_map_from_cff_type1_font_file, + ) + ) + for font_file, condition, font_file_processor in font_file_handlers: + if ( + font_file in font_descriptor and + isinstance(font_file_dict := font_descriptor[font_file], StreamObject) and + condition(font_file_dict) + ): + if font_file == "/FontFile3" and not HAS_FONTTOOLS: + logger_warning( + ( + "fontTools is required to fully parse the encoding of a CFF Type1 font in font " + "dictionary %(ft)s, but is not installed. Consider installing fontTools if you " + "encounter encoding problems." + ), + source=__name__, + ft=ft, + ) + return map_dict, int_entry + + font_file_data = font_file_dict.get_data() + if not font_file_data: + return map_dict, int_entry + + return font_file_processor(font_file_data, map_dict, int_entry) + + return map_dict, int_entry + + return {}, [] + + process_rg: bool = False + process_char: bool = False + multiline_rg: Union[ + tuple[int, int], None + ] = None # tuple = (current_char, remaining size) ; cf #1285 for example of file + cm = prepare_cm(ft) + for line in cm.split(b"\n"): + process_rg, process_char, multiline_rg = process_cm_line( + line.strip(b" \t"), + process_rg, + process_char, + multiline_rg, + map_dict, + int_entry, + ) + + map_dict.pop(-1, None) # Don't pass the -1 key, we only used it to temporarily store encoding length + + return map_dict, int_entry + + +def prepare_cm(ft: DictionaryObject) -> bytes: + tu = ft["/ToUnicode"] + cm: bytes + if isinstance(tu, StreamObject): + cm = cast(DecodedStreamObject, ft["/ToUnicode"]).get_data() + else: # if (tu is None) or cast(str, tu).startswith("/Identity"): + # the full range 0000-FFFF will be processed + cm = b"beginbfrange\n<0000> <0001> <0000>\nendbfrange" + if isinstance(cm, str): + cm = cm.encode() + # we need to prepare cm before due to missing return line in pdf printed + # to pdf from word + cm = ( + cm.strip() + .replace(b"beginbfchar", b"\nbeginbfchar\n") + .replace(b"endbfchar", b"\nendbfchar\n") + .replace(b"beginbfrange", b"\nbeginbfrange\n") + .replace(b"endbfrange", b"\nendbfrange\n") + .replace(b"<<", b"\n{\n") # text between << and >> not used but + .replace(b">>", b"\n}\n") # some solution to find it back + ) + ll = cm.split(b"<") + for i in range(len(ll)): + j = ll[i].find(b">") + if j >= 0: + if j == 0: + # string is empty: stash a placeholder here (see below) + # see https://github.com/py-pdf/pypdf/issues/1111 + content = b"." + else: + content = ll[i][:j].replace(b" ", b"") + ll[i] = content + b" " + ll[i][j + 1 :] + cm = ( + (b" ".join(ll)) + .replace(b"[", b" [ ") + .replace(b"]", b" ]\n ") + .replace(b"\r", b"\n") + ) + return cm + + +def process_cm_line( + line: bytes, + process_rg: bool, + process_char: bool, + multiline_rg: Union[tuple[int, int], None], + map_dict: dict[Any, Any], + int_entry: list[int], +) -> tuple[bool, bool, Union[tuple[int, int], None]]: + if line == b"" or line[0] == 37: # 37 = % + return process_rg, process_char, multiline_rg + line = line.replace(b"\t", b" ") + if b"beginbfrange" in line: + process_rg = True + elif b"endbfrange" in line: + process_rg = False + elif b"beginbfchar" in line: + process_char = True + elif b"endbfchar" in line: + process_char = False + elif process_rg: + try: + multiline_rg = parse_bfrange(line, map_dict, int_entry, multiline_rg) + except (ValueError, IndexError) as error: + logger_warning("Skipping broken line %(line)r: %(error)s", source=__name__, line=line, error=error) + elif process_char: + try: + parse_bfchar(line, map_dict, int_entry) + except ValueError as error: + logger_warning("Skipping broken line %(line)r: %(error)s", source=__name__, line=line, error=error) + return process_rg, process_char, multiline_rg + + +# Usual values should be up to 65_536. +MAPPING_DICTIONARY_SIZE_LIMIT = 100_000 + +# Typical /ToUnicode CMaps use 1-4 byte source codes. +# This is intentionally generous. +# The actual limit is doubled, as each byte is represented by two hex characters. +MAX_CMAP_CODE_BYTES = 8 +MAX_CMAP_STRING_BYTES = 512 +MAX_CMAP_CODE_BYTES_LIMIT = MAX_CMAP_CODE_BYTES * 2 +MAX_CMAP_STRING_BYTES_LIMIT = MAX_CMAP_STRING_BYTES * 2 + + +def _check_mapping_size(size: int) -> None: + if size > MAPPING_DICTIONARY_SIZE_LIMIT: + raise LimitReachedError(f"Maximum /ToUnicode size limit reached: {size} > {MAPPING_DICTIONARY_SIZE_LIMIT}.") + + +def _check_token_length(token: bytes, limit: int) -> None: + token_length = len(token) + if token_length > limit: + description = { + MAX_CMAP_CODE_BYTES_LIMIT: "code", + MAX_CMAP_STRING_BYTES_LIMIT: "string", + }.get(limit, "token") + + raise LimitReachedError( + f"Maximum /ToUnicode {description} length exceeded: {token_length} > {limit}." + ) + + +def __parse_bfrange__decode(map_dict: dict[Any, Any], code: int) -> str: + # `map_dict[-1]` is the number of bytes each source code occupies. Building + # the bytes directly with `int.to_bytes` avoids the hex round-trip of + # `unhexlify(b"%%0%dX" % (map_dict[-1] * 2) % code)` (format to hex, parse + # the hex back to bytes), which is measurably cheaper for large maps. + return code.to_bytes(map_dict[-1], "big").decode( + "charmap" if map_dict[-1] == 1 else "utf-16-be", + "surrogatepass", + ) + + +def parse_bfrange( + line: bytes, + map_dict: dict[Any, Any], + int_entry: list[int], + multiline_rg: Union[tuple[int, int], None], +) -> Union[tuple[int, int], None]: + lst = line.split() + closure_found = False + entry_count = len(int_entry) + _check_mapping_size(entry_count) + decode_utf16 = partial(bytes.decode, encoding="utf-16-be", errors="surrogatepass") + if multiline_rg is not None: + a = multiline_rg[0] # a, b not in the current line + b = multiline_rg[1] + for sq in lst: + if sq == b"]": + closure_found = True + break + _check_token_length(sq, limit=MAX_CMAP_STRING_BYTES_LIMIT) + entry_count += 1 + _check_mapping_size(entry_count) + map_dict[ + __parse_bfrange__decode(map_dict=map_dict, code=a) + ] = decode_utf16(unhexlify(sq)) + int_entry.append(a) + a += 1 + else: + _check_token_length(lst[0], limit=MAX_CMAP_CODE_BYTES_LIMIT) + _check_token_length(lst[1], limit=MAX_CMAP_CODE_BYTES_LIMIT) + a = int(lst[0], 16) + b = int(lst[1], 16) + nbi = max(len(lst[0]), len(lst[1])) + map_dict[-1] = (nbi + 1) // 2 + if lst[2] == b"[": + for sq in lst[3:]: + if sq == b"]": + closure_found = True + break + _check_token_length(sq, limit=MAX_CMAP_STRING_BYTES_LIMIT) + entry_count += 1 + _check_mapping_size(entry_count) + map_dict[ + __parse_bfrange__decode(map_dict=map_dict, code=a) + ] = decode_utf16(unhexlify(sq)) + int_entry.append(a) + a += 1 + else: # case without list + _check_token_length(lst[2], limit=MAX_CMAP_STRING_BYTES_LIMIT) + c = int(lst[2], 16) + fmt2 = b"%%0%dX" % max(4, len(lst[2])) + closure_found = True + range_size = max(0, b - a + 1) + _check_mapping_size(entry_count + range_size) # This can be checked beforehand. + while a <= b: + destination = unhexlify(fmt2 % c) + _check_token_length(destination, limit=MAX_CMAP_CODE_BYTES_LIMIT) + map_dict[ + __parse_bfrange__decode(map_dict=map_dict, code=a) + ] = decode_utf16(destination) + int_entry.append(a) + a += 1 + c += 1 + return None if closure_found else (a, b) + + +def parse_bfchar(line: bytes, map_dict: dict[Any, Any], int_entry: list[int]) -> None: + lst = [x for x in line.split(b" ") if x] + lst_length = len(lst) + if lst_length == 0: + logger_warning("Skipping broken line %(line)r: Line is empty.", source=__name__, line=line) + return + if lst_length % 2: + logger_warning("Ignoring final token of odd-length line %(line)r.", source=__name__, line=line) + + new_count = lst_length // 2 + _check_mapping_size(len(int_entry) + new_count) # This can be checked beforehand. + map_dict[-1] = len(lst[0]) // 2 + + while len(lst) > 1: + source = lst[0] + destination = lst[1] + + _check_token_length(source, limit=MAX_CMAP_CODE_BYTES_LIMIT) + + map_to = "" + # placeholder (see above) means empty string + if destination != b".": + _check_token_length(destination, limit=MAX_CMAP_STRING_BYTES_LIMIT) + try: + map_to = unhexlify(destination).decode( + "charmap" if len(destination) < 4 else "utf-16-be", "surrogatepass" + ) # join is here as some cases where the code was split + except BinasciiError as exception: + logger_warning( + "Got invalid hex string: %(exception)s (%(destination)r)", + source=__name__, + exception=exception, + destination=destination, + ) + map_dict[ + unhexlify(source).decode( + "charmap" if map_dict[-1] == 1 else "utf-16-be", "surrogatepass" + ) + ] = map_to + int_entry.append(int(source, 16)) + lst = lst[2:] + + +def _glyph_name_to_unicode(glyph_name: str) -> Union[str, None]: + try: + return adobe_glyphs[glyph_name] + except KeyError: + if not glyph_name.startswith("/uni"): + return None + try: + return chr(int(glyph_name[4:], 16)) + except ValueError: # pragma: no cover + return None + + +def _character_map_from_cff_type1_font_file( + font_data: bytes, + map_dict: dict[Any, Any], + int_entry: list[int], +) -> tuple[dict[Any, Any], list[int]]: + try: + from fontTools.cffLib import CFFFontSet # noqa: PLC0415 + cff_set = CFFFontSet() + cff_set.decompile(BytesIO(font_data), None) # This can raise ValueError, AssertionError, struct.error. + cff_font = cff_set.topDictIndex[0] # First font in CFF set; Can raise AttributeError or IndexError. + cff_encoding = cff_font.Encoding # Can raise AttributeError. + # Encoding can fall back to literal strings "StandardEncoding" or "ExpertEncoding", which we do not parse. + if isinstance(cff_encoding, str): + return map_dict, int_entry + for i in range(min(len(cff_encoding), 256)): + glyph_name = cff_encoding[i] + if not glyph_name or glyph_name == ".notdef": + continue + if unipoint := _glyph_name_to_unicode(f"/{glyph_name}"): + map_dict[chr(i)] = unipoint + int_entry.append(i) + return map_dict, int_entry + + except (struct.error, AssertionError, AttributeError, IndexError, NotImplementedError, ValueError): + return map_dict, int_entry + + +def _character_map_from_type1_font_file( + font_data: bytes, + map_dict: dict[Any, Any], + int_entry: list[int], +) -> tuple[dict[Any, Any], list[int]]: + txt = font_data.split(b"eexec\n")[0] # Only the clear part + encoding_part = txt.split(b"/Encoding") + if len(encoding_part) < 2: + return map_dict, int_entry + txt = encoding_part[1] # To get the encoding part + lines = txt.replace(b"\r", b"\n").split(b"\n") + for li in lines: + if li.startswith(b"dup"): + words = [_w for _w in li.split(b" ") if _w != b""] + if len(words) < 3 or (len(words) > 3 and words[3] != b"put"): + continue + try: + i = int(words[1]) + except ValueError: + continue + + unipoint = _glyph_name_to_unicode(words[2].decode()) + if unipoint: + map_dict[chr(i)] = unipoint + int_entry.append(i) + + return map_dict, int_entry diff --git a/app/vendor/pypdf/_codecs/__init__.py b/app/vendor/pypdf/_codecs/__init__.py new file mode 100644 index 0000000..af7fb8c --- /dev/null +++ b/app/vendor/pypdf/_codecs/__init__.py @@ -0,0 +1,59 @@ +from .adobe_glyphs import adobe_glyphs +from .pdfdoc import _pdfdoc_encoding +from .std import _std_encoding +from .symbol import _symbol_encoding +from .zapfding import _zapfding_encoding + + +def fill_from_encoding(enc: str) -> list[str]: + lst: list[str] = [] + for x in range(256): + try: + lst += (bytes((x,)).decode(enc),) + except Exception: + lst += (chr(x),) + return lst + + +def rev_encoding(enc: list[str]) -> dict[str, int]: + rev: dict[str, int] = {} + for i in range(256): + char = enc[i] + if char == "\u0000": + continue + assert char not in rev, f"{char} at {i} already at {rev[char]}" + rev[char] = i + return rev + + +def encoding_dict_from_named_encoding(encoding: str) -> dict[int, str]: + return dict(zip(range(256), fill_from_encoding(encoding))) + + +_win_encoding = fill_from_encoding("cp1252") +_mac_encoding = fill_from_encoding("mac_roman") + + +_pdfdoc_encoding_rev: dict[str, int] = rev_encoding(_pdfdoc_encoding) + + +charset_encoding: dict[str, list[str]] = { + "/StandardEncoding": _std_encoding, + "/WinAnsiEncoding": _win_encoding, + "/MacRomanEncoding": _mac_encoding, + "/PDFDocEncoding": _pdfdoc_encoding, + "/Symbol": _symbol_encoding, + "/ZapfDingbats": _zapfding_encoding, +} + +__all__ = [ + "_mac_encoding", + "_pdfdoc_encoding", + "_pdfdoc_encoding_rev", + "_std_encoding", + "_symbol_encoding", + "_win_encoding", + "_zapfding_encoding", + "adobe_glyphs", + "charset_encoding", +] diff --git a/app/vendor/pypdf/_codecs/_codecs.py b/app/vendor/pypdf/_codecs/_codecs.py new file mode 100644 index 0000000..4924058 --- /dev/null +++ b/app/vendor/pypdf/_codecs/_codecs.py @@ -0,0 +1,284 @@ +""" +This module is for codecs only. + +While the codec implementation can contain details of the PDF specification, +the module should not do any PDF parsing. +""" + +import io +from abc import ABC, abstractmethod + +from pypdf._utils import logger_warning +from pypdf.errors import LimitReachedError, PdfStreamError + + +class Codec(ABC): + """Abstract base class for all codecs.""" + + @abstractmethod + def encode(self, data: bytes) -> bytes: + """ + Encode the input data. + + Args: + data: Data to encode. + + Returns: + Encoded data. + + """ + + @abstractmethod + def decode(self, data: bytes) -> bytes: + """ + Decode the input data. + + Args: + data: Data to decode. + + Returns: + Decoded data. + + """ + + +class LzwCodec(Codec): + """Lempel-Ziv-Welch (LZW) adaptive compression codec.""" + + CLEAR_TABLE_MARKER = 256 # Special code to indicate table reset + EOD_MARKER = 257 # End-of-data marker + INITIAL_BITS_PER_CODE = 9 # Initial code bit width + MAX_BITS_PER_CODE = 12 # Maximum code bit width + + def __init__(self, max_output_length: int = 75_000_000) -> None: + self.max_output_length = max_output_length + + def _initialize_encoding_table(self) -> None: + """Initialize the encoding table and state to initial conditions.""" + self.encoding_table: dict[bytes, int] = {bytes([i]): i for i in range(256)} + self.next_code = self.EOD_MARKER + 1 + self.bits_per_code = self.INITIAL_BITS_PER_CODE + self.max_code_value = (1 << self.bits_per_code) - 1 + + def _increase_next_code(self) -> None: + """Update bits_per_code and max_code_value if necessary.""" + self.next_code += 1 + if ( + self.next_code > self.max_code_value + and self.bits_per_code < self.MAX_BITS_PER_CODE + ): + self.bits_per_code += 1 + self.max_code_value = (1 << self.bits_per_code) - 1 + + def encode(self, data: bytes) -> bytes: + """ + Encode data using the LZW compression algorithm. + + Taken from PDF 1.7 specs, "7.4.4.2 Details of LZW Encoding". + """ + result_codes: list[int] = [] + + # The encoder shall begin by issuing a clear-table code + result_codes.append(self.CLEAR_TABLE_MARKER) + self._initialize_encoding_table() + + current_sequence = b"" + for byte in data: + next_sequence = current_sequence + bytes([byte]) + + if next_sequence in self.encoding_table: + # Extend current sequence if already in the table + current_sequence = next_sequence + else: + # Output code for the current sequence + result_codes.append(self.encoding_table[current_sequence]) + + # Add the new sequence to the table if there's room + if self.next_code <= (1 << self.MAX_BITS_PER_CODE) - 1: + self.encoding_table[next_sequence] = self.next_code + self._increase_next_code() + else: + # If the table is full, emit a clear-table command + result_codes.append(self.CLEAR_TABLE_MARKER) + self._initialize_encoding_table() + + # Start new sequence + current_sequence = bytes([byte]) + + # Ensure everything actually is encoded + if current_sequence: + result_codes.append(self.encoding_table[current_sequence]) + result_codes.append(self.EOD_MARKER) + + return self._pack_codes_into_bytes(result_codes) + + def _pack_codes_into_bytes(self, codes: list[int]) -> bytes: + """ + Convert the list of result codes into a continuous byte stream, with codes packed as per the code bit-width. + The bit-width starts at 9 bits and expands as needed. + """ + self._initialize_encoding_table() + buffer = 0 + bits_in_buffer = 0 + output = bytearray() + + for code in codes: + buffer = (buffer << self.bits_per_code) | code + bits_in_buffer += self.bits_per_code + + # Codes shall be packed into a continuous bit stream, high-order bit + # first. This stream shall then be divided into bytes, high-order bit + # first. + while bits_in_buffer >= 8: + bits_in_buffer -= 8 + output.append((buffer >> bits_in_buffer) & 0xFF) + + if code == self.CLEAR_TABLE_MARKER: + self._initialize_encoding_table() + elif code == self.EOD_MARKER: + continue + else: + self._increase_next_code() + + # Flush any remaining bits in the buffer + if bits_in_buffer > 0: + output.append((buffer << (8 - bits_in_buffer)) & 0xFF) + + return bytes(output) + + def _initialize_decoding_table(self) -> None: + self.max_code_value = (1 << self.MAX_BITS_PER_CODE) - 1 + self.decoding_table = [bytes([i]) for i in range(self.CLEAR_TABLE_MARKER)] + [ + b"" + ] * (self.max_code_value - self.CLEAR_TABLE_MARKER + 1) + self._table_index = self.EOD_MARKER + 1 + self._bits_to_get = 9 + + def _next_code_decode(self, data: bytes) -> int: + self._next_data: int + try: + while self._next_bits < self._bits_to_get: + self._next_data = (self._next_data << 8) | ( + data[self._byte_pointer] + ) + self._byte_pointer += 1 + self._next_bits += 8 + + code = ( + self._next_data >> (self._next_bits - self._bits_to_get) + ) & self._and_table[self._bits_to_get - 9] + self._next_bits -= self._bits_to_get + + # Reduce data to get rid of the overhead, + # which increases performance on large streams significantly. + self._next_data = self._next_data & 0xFFFFF + + return code + except IndexError: + return self.EOD_MARKER + + # The following method has been converted to Python from PDFsharp: + # https://github.com/empira/PDFsharp/blob/5fbf6ed14740bc4e16786816882d32e43af3ff5d/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Filters/LzwDecode.cs + # + # Original license: + # + # ------------------------------------------------------------------------- + # Copyright (c) 2001-2024 empira Software GmbH, Troisdorf (Cologne Area), + # Germany + # + # http://docs.pdfsharp.net + # + # MIT License + # + # Permission is hereby granted, free of charge, to any person obtaining a + # copy of this software and associated documentation files (the "Software"), + # to deal in the Software without restriction, including without limitation + # the rights to use, copy, modify, merge, publish, distribute, sublicense, + # and/or sell copies of the Software, and to permit persons to whom the + # Software is furnished to do so, subject to the following conditions: + # + # The above copyright notice and this permission notice shall be included + # in all copies or substantial portions of the Software. + # + # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + # THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + # DEALINGS IN THE SOFTWARE. + # -------------------------------------------------------------------------- + def decode(self, data: bytes) -> bytes: + """ + The following code was converted to Python from the following code: + https://github.com/empira/PDFsharp/blob/master/src/foundation/src/PDFsharp/src/PdfSharp/Pdf.Filters/LzwDecode.cs + """ + self._and_table = [511, 1023, 2047, 4095] + self._table_index = 0 + self._bits_to_get = 9 + self._byte_pointer = 0 + self._next_data = 0 + self._next_bits = 0 + + output_stream = io.BytesIO() + output_length = 0 + + self._initialize_decoding_table() + self._byte_pointer = 0 + self._next_data = 0 + self._next_bits = 0 + old_code = self.CLEAR_TABLE_MARKER + + while True: + code = self._next_code_decode(data) + if code == self.EOD_MARKER: + break + + if code == self.CLEAR_TABLE_MARKER: + self._initialize_decoding_table() + code = self._next_code_decode(data) + if code == self.EOD_MARKER: + break + output_stream.write(decoded := self.decoding_table[code]) + old_code = code + elif code < self._table_index: + decoded = self.decoding_table[code] + output_stream.write(decoded) + if old_code != self.CLEAR_TABLE_MARKER: + self._add_entry_decode(self.decoding_table[old_code], decoded[0]) + old_code = code + else: + # The code is not in the table and not one of the special codes + base = self.decoding_table[old_code] + if not base: + raise PdfStreamError( + f"LZW code {code} out of range with empty base at table index {self._table_index}." + ) + decoded = base + base[:1] + output_stream.write(decoded) + self._add_entry_decode(base, decoded[0]) + old_code = code + + output_length += len(decoded) + if output_length > self.max_output_length: + raise LimitReachedError( + f"Limit reached while decompressing: {output_length} > {self.max_output_length}" + ) + + return output_stream.getvalue() + + def _add_entry_decode(self, old_string: bytes, new_char: int) -> None: + new_string = old_string + bytes([new_char]) + if self._table_index > self.max_code_value: + logger_warning("Ignoring too large LZW table index.", source=__name__) + return + self.decoding_table[self._table_index] = new_string + self._table_index += 1 + + # Update the number of bits to get based on the table index + if self._table_index == 511: + self._bits_to_get = 10 + elif self._table_index == 1023: + self._bits_to_get = 11 + elif self._table_index == 2047: + self._bits_to_get = 12 diff --git a/app/vendor/pypdf/_codecs/adobe_glyphs.py b/app/vendor/pypdf/_codecs/adobe_glyphs.py new file mode 100644 index 0000000..1631724 --- /dev/null +++ b/app/vendor/pypdf/_codecs/adobe_glyphs.py @@ -0,0 +1,17307 @@ +# Generated from https://github.com/adobe-type-tools/agl-aglfn/blob/4036a9ca80a62f64f9de4f7321a9a045ad0ecfd6/glyphlist.txt +# +# Name: Adobe Glyph List +# Table version: 2.0 +# Date: September 20, 2002 +# URL: https://github.com/adobe-type-tools/agl-aglfn +# +# Copyright 2002-2019 Adobe (http://www.adobe.com/). +# +# Redistribution and use in source and binary forms, with or +# without modification, are permitted provided that the +# following conditions are met: +# +# Redistributions of source code must retain the above +# copyright notice, this list of conditions and the following +# disclaimer. +# +# Redistributions in binary form must reproduce the above +# copyright notice, this list of conditions and the following +# disclaimer in the documentation and/or other materials +# provided with the distribution. +# +# Neither the name of Adobe nor the names of its contributors +# may be used to endorse or promote products derived from this +# software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND +# CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, +# INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT +# NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR +# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# +# ----------------------------------------------------------------------------- +# +# Additional data has been derived from https://github.com/LettError/glyphNameFormatter/blob/3cd467e6ff9d7b3f6484d3460c5e79ab4713cbb1/Lib/glyphNameFormatter/names/glyphNamesToUnicode.txt +# +# Glyph Name Formatted Unicode List - GNFUL +# GlyphNameFormatter version 0.8.1 - git commit: 511 +# Unicode version: Unicode 16.0.0 +# Source code: https://github.com/LettError/glyphNameFormatter/tree/380a129 +# Generated on 2025 08 08 17:00:14 +# +# Copyright (c) 2016, LettError and Erik van Blokland, TypeMyType and Frederik Berlaen +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation and/or +# other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its contributors may +# be used to endorse or promote products derived from this software without specific +# prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, +# INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, +# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED +# OF THE POSSIBILITY OF SUCH DAMAGE. +# +# ----------------------------------------------------------------------------- +# +# GlyphNameFormatter uses data retrieved from https://www.unicode.org/Public/16.0.0/ucdxml/ucd.all.flat.zip +# +# UNICODE LICENSE V3 +# +# COPYRIGHT AND PERMISSION NOTICE +# +# © 2023 Unicode®, Inc. +# Copyright © 1991-2026 Unicode, Inc. +# +# NOTICE TO USER: Carefully read the following legal agreement. BY +# DOWNLOADING, INSTALLING, COPYING OR OTHERWISE USING DATA FILES, AND/OR +# SOFTWARE, YOU UNEQUIVOCALLY ACCEPT, AND AGREE TO BE BOUND BY, ALL OF THE +# TERMS AND CONDITIONS OF THIS AGREEMENT. IF YOU DO NOT AGREE, DO NOT +# DOWNLOAD, INSTALL, COPY, DISTRIBUTE OR USE THE DATA FILES OR SOFTWARE. +# +# Permission is hereby granted, free of charge, to any person obtaining a +# copy of data files and any associated documentation (the "Data Files") or +# software and any associated documentation (the "Software") to deal in the +# Data Files or Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, and/or sell +# copies of the Data Files or Software, and to permit persons to whom the +# Data Files or Software are furnished to do so, provided that either (a) +# this copyright and permission notice appear with all copies of the Data +# Files or Software, or (b) this copyright and permission notice appear in +# associated Documentation. +# +# THE DATA FILES AND SOFTWARE ARE PROVIDED "AS IS", WITHOUT WARRANTY OF ANY +# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF +# THIRD PARTY RIGHTS. +# +# IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS NOTICE +# BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL DAMAGES, +# OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, +# ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THE DATA +# FILES OR SOFTWARE. +# +# Except as contained in this notice, the name of a copyright holder shall +# not be used in advertising or otherwise to promote the sale, use or other +# dealings in these Data Files or Software without prior written +# authorization of the copyright holder. +# + +adobe_glyphs = { + # glyphlist.txt + "/A": "\u0041", + "/AE": "\u00C6", + "/AEacute": "\u01FC", + "/AEmacron": "\u01E2", + "/AEsmall": "\uF7E6", + "/Aacute": "\u00C1", + "/Aacutesmall": "\uF7E1", + "/Abreve": "\u0102", + "/Abreveacute": "\u1EAE", + "/Abrevecyrillic": "\u04D0", + "/Abrevedotbelow": "\u1EB6", + "/Abrevegrave": "\u1EB0", + "/Abrevehookabove": "\u1EB2", + "/Abrevetilde": "\u1EB4", + "/Acaron": "\u01CD", + "/Acircle": "\u24B6", + "/Acircumflex": "\u00C2", + "/Acircumflexacute": "\u1EA4", + "/Acircumflexdotbelow": "\u1EAC", + "/Acircumflexgrave": "\u1EA6", + "/Acircumflexhookabove": "\u1EA8", + "/Acircumflexsmall": "\uF7E2", + "/Acircumflextilde": "\u1EAA", + "/Acute": "\uF6C9", + "/Acutesmall": "\uF7B4", + "/Acyrillic": "\u0410", + "/Adblgrave": "\u0200", + "/Adieresis": "\u00C4", + "/Adieresiscyrillic": "\u04D2", + "/Adieresismacron": "\u01DE", + "/Adieresissmall": "\uF7E4", + "/Adotbelow": "\u1EA0", + "/Adotmacron": "\u01E0", + "/Agrave": "\u00C0", + "/Agravesmall": "\uF7E0", + "/Ahookabove": "\u1EA2", + "/Aiecyrillic": "\u04D4", + "/Ainvertedbreve": "\u0202", + "/Alpha": "\u0391", + "/Alphatonos": "\u0386", + "/Amacron": "\u0100", + "/Amonospace": "\uFF21", + "/Aogonek": "\u0104", + "/Aring": "\u00C5", + "/Aringacute": "\u01FA", + "/Aringbelow": "\u1E00", + "/Aringsmall": "\uF7E5", + "/Asmall": "\uF761", + "/Atilde": "\u00C3", + "/Atildesmall": "\uF7E3", + "/Aybarmenian": "\u0531", + "/B": "\u0042", + "/Bcircle": "\u24B7", + "/Bdotaccent": "\u1E02", + "/Bdotbelow": "\u1E04", + "/Becyrillic": "\u0411", + "/Benarmenian": "\u0532", + "/Beta": "\u0392", + "/Bhook": "\u0181", + "/Blinebelow": "\u1E06", + "/Bmonospace": "\uFF22", + "/Brevesmall": "\uF6F4", + "/Bsmall": "\uF762", + "/Btopbar": "\u0182", + "/C": "\u0043", + "/Caarmenian": "\u053E", + "/Cacute": "\u0106", + "/Caron": "\uF6CA", + "/Caronsmall": "\uF6F5", + "/Ccaron": "\u010C", + "/Ccedilla": "\u00C7", + "/Ccedillaacute": "\u1E08", + "/Ccedillasmall": "\uF7E7", + "/Ccircle": "\u24B8", + "/Ccircumflex": "\u0108", + "/Cdot": "\u010A", + "/Cdotaccent": "\u010A", + "/Cedillasmall": "\uF7B8", + "/Chaarmenian": "\u0549", + "/Cheabkhasiancyrillic": "\u04BC", + "/Checyrillic": "\u0427", + "/Chedescenderabkhasiancyrillic": "\u04BE", + "/Chedescendercyrillic": "\u04B6", + "/Chedieresiscyrillic": "\u04F4", + "/Cheharmenian": "\u0543", + "/Chekhakassiancyrillic": "\u04CB", + "/Cheverticalstrokecyrillic": "\u04B8", + "/Chi": "\u03A7", + "/Chook": "\u0187", + "/Circumflexsmall": "\uF6F6", + "/Cmonospace": "\uFF23", + "/Coarmenian": "\u0551", + "/Csmall": "\uF763", + "/D": "\u0044", + "/DZ": "\u01F1", + "/DZcaron": "\u01C4", + "/Daarmenian": "\u0534", + "/Dafrican": "\u0189", + "/Dcaron": "\u010E", + "/Dcedilla": "\u1E10", + "/Dcircle": "\u24B9", + "/Dcircumflexbelow": "\u1E12", + "/Dcroat": "\u0110", + "/Ddotaccent": "\u1E0A", + "/Ddotbelow": "\u1E0C", + "/Decyrillic": "\u0414", + "/Deicoptic": "\u03EE", + "/Delta": "\u2206", + "/Deltagreek": "\u0394", + "/Dhook": "\u018A", + "/Dieresis": "\uF6CB", + "/DieresisAcute": "\uF6CC", + "/DieresisGrave": "\uF6CD", + "/Dieresissmall": "\uF7A8", + "/Digammagreek": "\u03DC", + "/Djecyrillic": "\u0402", + "/Dlinebelow": "\u1E0E", + "/Dmonospace": "\uFF24", + "/Dotaccentsmall": "\uF6F7", + "/Dslash": "\u0110", + "/Dsmall": "\uF764", + "/Dtopbar": "\u018B", + "/Dz": "\u01F2", + "/Dzcaron": "\u01C5", + "/Dzeabkhasiancyrillic": "\u04E0", + "/Dzecyrillic": "\u0405", + "/Dzhecyrillic": "\u040F", + "/E": "\u0045", + "/Eacute": "\u00C9", + "/Eacutesmall": "\uF7E9", + "/Ebreve": "\u0114", + "/Ecaron": "\u011A", + "/Ecedillabreve": "\u1E1C", + "/Echarmenian": "\u0535", + "/Ecircle": "\u24BA", + "/Ecircumflex": "\u00CA", + "/Ecircumflexacute": "\u1EBE", + "/Ecircumflexbelow": "\u1E18", + "/Ecircumflexdotbelow": "\u1EC6", + "/Ecircumflexgrave": "\u1EC0", + "/Ecircumflexhookabove": "\u1EC2", + "/Ecircumflexsmall": "\uF7EA", + "/Ecircumflextilde": "\u1EC4", + "/Ecyrillic": "\u0404", + "/Edblgrave": "\u0204", + "/Edieresis": "\u00CB", + "/Edieresissmall": "\uF7EB", + "/Edot": "\u0116", + "/Edotaccent": "\u0116", + "/Edotbelow": "\u1EB8", + "/Efcyrillic": "\u0424", + "/Egrave": "\u00C8", + "/Egravesmall": "\uF7E8", + "/Eharmenian": "\u0537", + "/Ehookabove": "\u1EBA", + "/Eightroman": "\u2167", + "/Einvertedbreve": "\u0206", + "/Eiotifiedcyrillic": "\u0464", + "/Elcyrillic": "\u041B", + "/Elevenroman": "\u216A", + "/Emacron": "\u0112", + "/Emacronacute": "\u1E16", + "/Emacrongrave": "\u1E14", + "/Emcyrillic": "\u041C", + "/Emonospace": "\uFF25", + "/Encyrillic": "\u041D", + "/Endescendercyrillic": "\u04A2", + "/Eng": "\u014A", + "/Enghecyrillic": "\u04A4", + "/Enhookcyrillic": "\u04C7", + "/Eogonek": "\u0118", + "/Eopen": "\u0190", + "/Epsilon": "\u0395", + "/Epsilontonos": "\u0388", + "/Ercyrillic": "\u0420", + "/Ereversed": "\u018E", + "/Ereversedcyrillic": "\u042D", + "/Escyrillic": "\u0421", + "/Esdescendercyrillic": "\u04AA", + "/Esh": "\u01A9", + "/Esmall": "\uF765", + "/Eta": "\u0397", + "/Etarmenian": "\u0538", + "/Etatonos": "\u0389", + "/Eth": "\u00D0", + "/Ethsmall": "\uF7F0", + "/Etilde": "\u1EBC", + "/Etildebelow": "\u1E1A", + "/Euro": "\u20AC", + "/Ezh": "\u01B7", + "/Ezhcaron": "\u01EE", + "/Ezhreversed": "\u01B8", + "/F": "\u0046", + "/Fcircle": "\u24BB", + "/Fdotaccent": "\u1E1E", + "/Feharmenian": "\u0556", + "/Feicoptic": "\u03E4", + "/Fhook": "\u0191", + "/Fitacyrillic": "\u0472", + "/Fiveroman": "\u2164", + "/Fmonospace": "\uFF26", + "/Fourroman": "\u2163", + "/Fsmall": "\uF766", + "/G": "\u0047", + "/GBsquare": "\u3387", + "/Gacute": "\u01F4", + "/Gamma": "\u0393", + "/Gammaafrican": "\u0194", + "/Gangiacoptic": "\u03EA", + "/Gbreve": "\u011E", + "/Gcaron": "\u01E6", + "/Gcedilla": "\u0122", + "/Gcircle": "\u24BC", + "/Gcircumflex": "\u011C", + "/Gcommaaccent": "\u0122", + "/Gdot": "\u0120", + "/Gdotaccent": "\u0120", + "/Gecyrillic": "\u0413", + "/Ghadarmenian": "\u0542", + "/Ghemiddlehookcyrillic": "\u0494", + "/Ghestrokecyrillic": "\u0492", + "/Gheupturncyrillic": "\u0490", + "/Ghook": "\u0193", + "/Gimarmenian": "\u0533", + "/Gjecyrillic": "\u0403", + "/Gmacron": "\u1E20", + "/Gmonospace": "\uFF27", + "/Grave": "\uF6CE", + "/Gravesmall": "\uF760", + "/Gsmall": "\uF767", + "/Gsmallhook": "\u029B", + "/Gstroke": "\u01E4", + "/H": "\u0048", + "/H18533": "\u25CF", + "/H18543": "\u25AA", + "/H18551": "\u25AB", + "/H22073": "\u25A1", + "/HPsquare": "\u33CB", + "/Haabkhasiancyrillic": "\u04A8", + "/Hadescendercyrillic": "\u04B2", + "/Hardsigncyrillic": "\u042A", + "/Hbar": "\u0126", + "/Hbrevebelow": "\u1E2A", + "/Hcedilla": "\u1E28", + "/Hcircle": "\u24BD", + "/Hcircumflex": "\u0124", + "/Hdieresis": "\u1E26", + "/Hdotaccent": "\u1E22", + "/Hdotbelow": "\u1E24", + "/Hmonospace": "\uFF28", + "/Hoarmenian": "\u0540", + "/Horicoptic": "\u03E8", + "/Hsmall": "\uF768", + "/Hungarumlaut": "\uF6CF", + "/Hungarumlautsmall": "\uF6F8", + "/Hzsquare": "\u3390", + "/I": "\u0049", + "/IAcyrillic": "\u042F", + "/IJ": "\u0132", + "/IUcyrillic": "\u042E", + "/Iacute": "\u00CD", + "/Iacutesmall": "\uF7ED", + "/Ibreve": "\u012C", + "/Icaron": "\u01CF", + "/Icircle": "\u24BE", + "/Icircumflex": "\u00CE", + "/Icircumflexsmall": "\uF7EE", + "/Icyrillic": "\u0406", + "/Idblgrave": "\u0208", + "/Idieresis": "\u00CF", + "/Idieresisacute": "\u1E2E", + "/Idieresiscyrillic": "\u04E4", + "/Idieresissmall": "\uF7EF", + "/Idot": "\u0130", + "/Idotaccent": "\u0130", + "/Idotbelow": "\u1ECA", + "/Iebrevecyrillic": "\u04D6", + "/Iecyrillic": "\u0415", + "/Ifraktur": "\u2111", + "/Igrave": "\u00CC", + "/Igravesmall": "\uF7EC", + "/Ihookabove": "\u1EC8", + "/Iicyrillic": "\u0418", + "/Iinvertedbreve": "\u020A", + "/Iishortcyrillic": "\u0419", + "/Imacron": "\u012A", + "/Imacroncyrillic": "\u04E2", + "/Imonospace": "\uFF29", + "/Iniarmenian": "\u053B", + "/Iocyrillic": "\u0401", + "/Iogonek": "\u012E", + "/Iota": "\u0399", + "/Iotaafrican": "\u0196", + "/Iotadieresis": "\u03AA", + "/Iotatonos": "\u038A", + "/Ismall": "\uF769", + "/Istroke": "\u0197", + "/Itilde": "\u0128", + "/Itildebelow": "\u1E2C", + "/Izhitsacyrillic": "\u0474", + "/Izhitsadblgravecyrillic": "\u0476", + "/J": "\u004A", + "/Jaarmenian": "\u0541", + "/Jcircle": "\u24BF", + "/Jcircumflex": "\u0134", + "/Jecyrillic": "\u0408", + "/Jheharmenian": "\u054B", + "/Jmonospace": "\uFF2A", + "/Jsmall": "\uF76A", + "/K": "\u004B", + "/KBsquare": "\u3385", + "/KKsquare": "\u33CD", + "/Kabashkircyrillic": "\u04A0", + "/Kacute": "\u1E30", + "/Kacyrillic": "\u041A", + "/Kadescendercyrillic": "\u049A", + "/Kahookcyrillic": "\u04C3", + "/Kappa": "\u039A", + "/Kastrokecyrillic": "\u049E", + "/Kaverticalstrokecyrillic": "\u049C", + "/Kcaron": "\u01E8", + "/Kcedilla": "\u0136", + "/Kcircle": "\u24C0", + "/Kcommaaccent": "\u0136", + "/Kdotbelow": "\u1E32", + "/Keharmenian": "\u0554", + "/Kenarmenian": "\u053F", + "/Khacyrillic": "\u0425", + "/Kheicoptic": "\u03E6", + "/Khook": "\u0198", + "/Kjecyrillic": "\u040C", + "/Klinebelow": "\u1E34", + "/Kmonospace": "\uFF2B", + "/Koppacyrillic": "\u0480", + "/Koppagreek": "\u03DE", + "/Ksicyrillic": "\u046E", + "/Ksmall": "\uF76B", + "/L": "\u004C", + "/LJ": "\u01C7", + "/LL": "\uF6BF", + "/Lacute": "\u0139", + "/Lambda": "\u039B", + "/Lcaron": "\u013D", + "/Lcedilla": "\u013B", + "/Lcircle": "\u24C1", + "/Lcircumflexbelow": "\u1E3C", + "/Lcommaaccent": "\u013B", + "/Ldot": "\u013F", + "/Ldotaccent": "\u013F", + "/Ldotbelow": "\u1E36", + "/Ldotbelowmacron": "\u1E38", + "/Liwnarmenian": "\u053C", + "/Lj": "\u01C8", + "/Ljecyrillic": "\u0409", + "/Llinebelow": "\u1E3A", + "/Lmonospace": "\uFF2C", + "/Lslash": "\u0141", + "/Lslashsmall": "\uF6F9", + "/Lsmall": "\uF76C", + "/M": "\u004D", + "/MBsquare": "\u3386", + "/Macron": "\uF6D0", + "/Macronsmall": "\uF7AF", + "/Macute": "\u1E3E", + "/Mcircle": "\u24C2", + "/Mdotaccent": "\u1E40", + "/Mdotbelow": "\u1E42", + "/Menarmenian": "\u0544", + "/Mmonospace": "\uFF2D", + "/Msmall": "\uF76D", + "/Mturned": "\u019C", + "/Mu": "\u039C", + "/N": "\u004E", + "/NJ": "\u01CA", + "/Nacute": "\u0143", + "/Ncaron": "\u0147", + "/Ncedilla": "\u0145", + "/Ncircle": "\u24C3", + "/Ncircumflexbelow": "\u1E4A", + "/Ncommaaccent": "\u0145", + "/Ndotaccent": "\u1E44", + "/Ndotbelow": "\u1E46", + "/Nhookleft": "\u019D", + "/Nineroman": "\u2168", + "/Nj": "\u01CB", + "/Njecyrillic": "\u040A", + "/Nlinebelow": "\u1E48", + "/Nmonospace": "\uFF2E", + "/Nowarmenian": "\u0546", + "/Nsmall": "\uF76E", + "/Ntilde": "\u00D1", + "/Ntildesmall": "\uF7F1", + "/Nu": "\u039D", + "/O": "\u004F", + "/OE": "\u0152", + "/OEsmall": "\uF6FA", + "/Oacute": "\u00D3", + "/Oacutesmall": "\uF7F3", + "/Obarredcyrillic": "\u04E8", + "/Obarreddieresiscyrillic": "\u04EA", + "/Obreve": "\u014E", + "/Ocaron": "\u01D1", + "/Ocenteredtilde": "\u019F", + "/Ocircle": "\u24C4", + "/Ocircumflex": "\u00D4", + "/Ocircumflexacute": "\u1ED0", + "/Ocircumflexdotbelow": "\u1ED8", + "/Ocircumflexgrave": "\u1ED2", + "/Ocircumflexhookabove": "\u1ED4", + "/Ocircumflexsmall": "\uF7F4", + "/Ocircumflextilde": "\u1ED6", + "/Ocyrillic": "\u041E", + "/Odblacute": "\u0150", + "/Odblgrave": "\u020C", + "/Odieresis": "\u00D6", + "/Odieresiscyrillic": "\u04E6", + "/Odieresissmall": "\uF7F6", + "/Odotbelow": "\u1ECC", + "/Ogoneksmall": "\uF6FB", + "/Ograve": "\u00D2", + "/Ogravesmall": "\uF7F2", + "/Oharmenian": "\u0555", + "/Ohm": "\u2126", + "/Ohookabove": "\u1ECE", + "/Ohorn": "\u01A0", + "/Ohornacute": "\u1EDA", + "/Ohorndotbelow": "\u1EE2", + "/Ohorngrave": "\u1EDC", + "/Ohornhookabove": "\u1EDE", + "/Ohorntilde": "\u1EE0", + "/Ohungarumlaut": "\u0150", + "/Oi": "\u01A2", + "/Oinvertedbreve": "\u020E", + "/Omacron": "\u014C", + "/Omacronacute": "\u1E52", + "/Omacrongrave": "\u1E50", + "/Omega": "\u2126", + "/Omegacyrillic": "\u0460", + "/Omegagreek": "\u03A9", + "/Omegaroundcyrillic": "\u047A", + "/Omegatitlocyrillic": "\u047C", + "/Omegatonos": "\u038F", + "/Omicron": "\u039F", + "/Omicrontonos": "\u038C", + "/Omonospace": "\uFF2F", + "/Oneroman": "\u2160", + "/Oogonek": "\u01EA", + "/Oogonekmacron": "\u01EC", + "/Oopen": "\u0186", + "/Oslash": "\u00D8", + "/Oslashacute": "\u01FE", + "/Oslashsmall": "\uF7F8", + "/Osmall": "\uF76F", + "/Ostrokeacute": "\u01FE", + "/Otcyrillic": "\u047E", + "/Otilde": "\u00D5", + "/Otildeacute": "\u1E4C", + "/Otildedieresis": "\u1E4E", + "/Otildesmall": "\uF7F5", + "/P": "\u0050", + "/Pacute": "\u1E54", + "/Pcircle": "\u24C5", + "/Pdotaccent": "\u1E56", + "/Pecyrillic": "\u041F", + "/Peharmenian": "\u054A", + "/Pemiddlehookcyrillic": "\u04A6", + "/Phi": "\u03A6", + "/Phook": "\u01A4", + "/Pi": "\u03A0", + "/Piwrarmenian": "\u0553", + "/Pmonospace": "\uFF30", + "/Psi": "\u03A8", + "/Psicyrillic": "\u0470", + "/Psmall": "\uF770", + "/Q": "\u0051", + "/Qcircle": "\u24C6", + "/Qmonospace": "\uFF31", + "/Qsmall": "\uF771", + "/R": "\u0052", + "/Raarmenian": "\u054C", + "/Racute": "\u0154", + "/Rcaron": "\u0158", + "/Rcedilla": "\u0156", + "/Rcircle": "\u24C7", + "/Rcommaaccent": "\u0156", + "/Rdblgrave": "\u0210", + "/Rdotaccent": "\u1E58", + "/Rdotbelow": "\u1E5A", + "/Rdotbelowmacron": "\u1E5C", + "/Reharmenian": "\u0550", + "/Rfraktur": "\u211C", + "/Rho": "\u03A1", + "/Ringsmall": "\uF6FC", + "/Rinvertedbreve": "\u0212", + "/Rlinebelow": "\u1E5E", + "/Rmonospace": "\uFF32", + "/Rsmall": "\uF772", + "/Rsmallinverted": "\u0281", + "/Rsmallinvertedsuperior": "\u02B6", + "/S": "\u0053", + "/SF010000": "\u250C", + "/SF020000": "\u2514", + "/SF030000": "\u2510", + "/SF040000": "\u2518", + "/SF050000": "\u253C", + "/SF060000": "\u252C", + "/SF070000": "\u2534", + "/SF080000": "\u251C", + "/SF090000": "\u2524", + "/SF100000": "\u2500", + "/SF110000": "\u2502", + "/SF190000": "\u2561", + "/SF200000": "\u2562", + "/SF210000": "\u2556", + "/SF220000": "\u2555", + "/SF230000": "\u2563", + "/SF240000": "\u2551", + "/SF250000": "\u2557", + "/SF260000": "\u255D", + "/SF270000": "\u255C", + "/SF280000": "\u255B", + "/SF360000": "\u255E", + "/SF370000": "\u255F", + "/SF380000": "\u255A", + "/SF390000": "\u2554", + "/SF400000": "\u2569", + "/SF410000": "\u2566", + "/SF420000": "\u2560", + "/SF430000": "\u2550", + "/SF440000": "\u256C", + "/SF450000": "\u2567", + "/SF460000": "\u2568", + "/SF470000": "\u2564", + "/SF480000": "\u2565", + "/SF490000": "\u2559", + "/SF500000": "\u2558", + "/SF510000": "\u2552", + "/SF520000": "\u2553", + "/SF530000": "\u256B", + "/SF540000": "\u256A", + "/Sacute": "\u015A", + "/Sacutedotaccent": "\u1E64", + "/Sampigreek": "\u03E0", + "/Scaron": "\u0160", + "/Scarondotaccent": "\u1E66", + "/Scaronsmall": "\uF6FD", + "/Scedilla": "\u015E", + "/Schwa": "\u018F", + "/Schwacyrillic": "\u04D8", + "/Schwadieresiscyrillic": "\u04DA", + "/Scircle": "\u24C8", + "/Scircumflex": "\u015C", + "/Scommaaccent": "\u0218", + "/Sdotaccent": "\u1E60", + "/Sdotbelow": "\u1E62", + "/Sdotbelowdotaccent": "\u1E68", + "/Seharmenian": "\u054D", + "/Sevenroman": "\u2166", + "/Shaarmenian": "\u0547", + "/Shacyrillic": "\u0428", + "/Shchacyrillic": "\u0429", + "/Sheicoptic": "\u03E2", + "/Shhacyrillic": "\u04BA", + "/Shimacoptic": "\u03EC", + "/Sigma": "\u03A3", + "/Sixroman": "\u2165", + "/Smonospace": "\uFF33", + "/Softsigncyrillic": "\u042C", + "/Ssmall": "\uF773", + "/Stigmagreek": "\u03DA", + "/T": "\u0054", + "/Tau": "\u03A4", + "/Tbar": "\u0166", + "/Tcaron": "\u0164", + "/Tcedilla": "\u0162", + "/Tcircle": "\u24C9", + "/Tcircumflexbelow": "\u1E70", + "/Tcommaaccent": "\u0162", + "/Tdotaccent": "\u1E6A", + "/Tdotbelow": "\u1E6C", + "/Tecyrillic": "\u0422", + "/Tedescendercyrillic": "\u04AC", + "/Tenroman": "\u2169", + "/Tetsecyrillic": "\u04B4", + "/Theta": "\u0398", + "/Thook": "\u01AC", + "/Thorn": "\u00DE", + "/Thornsmall": "\uF7FE", + "/Threeroman": "\u2162", + "/Tildesmall": "\uF6FE", + "/Tiwnarmenian": "\u054F", + "/Tlinebelow": "\u1E6E", + "/Tmonospace": "\uFF34", + "/Toarmenian": "\u0539", + "/Tonefive": "\u01BC", + "/Tonesix": "\u0184", + "/Tonetwo": "\u01A7", + "/Tretroflexhook": "\u01AE", + "/Tsecyrillic": "\u0426", + "/Tshecyrillic": "\u040B", + "/Tsmall": "\uF774", + "/Twelveroman": "\u216B", + "/Tworoman": "\u2161", + "/U": "\u0055", + "/Uacute": "\u00DA", + "/Uacutesmall": "\uF7FA", + "/Ubreve": "\u016C", + "/Ucaron": "\u01D3", + "/Ucircle": "\u24CA", + "/Ucircumflex": "\u00DB", + "/Ucircumflexbelow": "\u1E76", + "/Ucircumflexsmall": "\uF7FB", + "/Ucyrillic": "\u0423", + "/Udblacute": "\u0170", + "/Udblgrave": "\u0214", + "/Udieresis": "\u00DC", + "/Udieresisacute": "\u01D7", + "/Udieresisbelow": "\u1E72", + "/Udieresiscaron": "\u01D9", + "/Udieresiscyrillic": "\u04F0", + "/Udieresisgrave": "\u01DB", + "/Udieresismacron": "\u01D5", + "/Udieresissmall": "\uF7FC", + "/Udotbelow": "\u1EE4", + "/Ugrave": "\u00D9", + "/Ugravesmall": "\uF7F9", + "/Uhookabove": "\u1EE6", + "/Uhorn": "\u01AF", + "/Uhornacute": "\u1EE8", + "/Uhorndotbelow": "\u1EF0", + "/Uhorngrave": "\u1EEA", + "/Uhornhookabove": "\u1EEC", + "/Uhorntilde": "\u1EEE", + "/Uhungarumlaut": "\u0170", + "/Uhungarumlautcyrillic": "\u04F2", + "/Uinvertedbreve": "\u0216", + "/Ukcyrillic": "\u0478", + "/Umacron": "\u016A", + "/Umacroncyrillic": "\u04EE", + "/Umacrondieresis": "\u1E7A", + "/Umonospace": "\uFF35", + "/Uogonek": "\u0172", + "/Upsilon": "\u03A5", + "/Upsilon1": "\u03D2", + "/Upsilonacutehooksymbolgreek": "\u03D3", + "/Upsilonafrican": "\u01B1", + "/Upsilondieresis": "\u03AB", + "/Upsilondieresishooksymbolgreek": "\u03D4", + "/Upsilonhooksymbol": "\u03D2", + "/Upsilontonos": "\u038E", + "/Uring": "\u016E", + "/Ushortcyrillic": "\u040E", + "/Usmall": "\uF775", + "/Ustraightcyrillic": "\u04AE", + "/Ustraightstrokecyrillic": "\u04B0", + "/Utilde": "\u0168", + "/Utildeacute": "\u1E78", + "/Utildebelow": "\u1E74", + "/V": "\u0056", + "/Vcircle": "\u24CB", + "/Vdotbelow": "\u1E7E", + "/Vecyrillic": "\u0412", + "/Vewarmenian": "\u054E", + "/Vhook": "\u01B2", + "/Vmonospace": "\uFF36", + "/Voarmenian": "\u0548", + "/Vsmall": "\uF776", + "/Vtilde": "\u1E7C", + "/W": "\u0057", + "/Wacute": "\u1E82", + "/Wcircle": "\u24CC", + "/Wcircumflex": "\u0174", + "/Wdieresis": "\u1E84", + "/Wdotaccent": "\u1E86", + "/Wdotbelow": "\u1E88", + "/Wgrave": "\u1E80", + "/Wmonospace": "\uFF37", + "/Wsmall": "\uF777", + "/X": "\u0058", + "/Xcircle": "\u24CD", + "/Xdieresis": "\u1E8C", + "/Xdotaccent": "\u1E8A", + "/Xeharmenian": "\u053D", + "/Xi": "\u039E", + "/Xmonospace": "\uFF38", + "/Xsmall": "\uF778", + "/Y": "\u0059", + "/Yacute": "\u00DD", + "/Yacutesmall": "\uF7FD", + "/Yatcyrillic": "\u0462", + "/Ycircle": "\u24CE", + "/Ycircumflex": "\u0176", + "/Ydieresis": "\u0178", + "/Ydieresissmall": "\uF7FF", + "/Ydotaccent": "\u1E8E", + "/Ydotbelow": "\u1EF4", + "/Yericyrillic": "\u042B", + "/Yerudieresiscyrillic": "\u04F8", + "/Ygrave": "\u1EF2", + "/Yhook": "\u01B3", + "/Yhookabove": "\u1EF6", + "/Yiarmenian": "\u0545", + "/Yicyrillic": "\u0407", + "/Yiwnarmenian": "\u0552", + "/Ymonospace": "\uFF39", + "/Ysmall": "\uF779", + "/Ytilde": "\u1EF8", + "/Yusbigcyrillic": "\u046A", + "/Yusbigiotifiedcyrillic": "\u046C", + "/Yuslittlecyrillic": "\u0466", + "/Yuslittleiotifiedcyrillic": "\u0468", + "/Z": "\u005A", + "/Zaarmenian": "\u0536", + "/Zacute": "\u0179", + "/Zcaron": "\u017D", + "/Zcaronsmall": "\uF6FF", + "/Zcircle": "\u24CF", + "/Zcircumflex": "\u1E90", + "/Zdot": "\u017B", + "/Zdotaccent": "\u017B", + "/Zdotbelow": "\u1E92", + "/Zecyrillic": "\u0417", + "/Zedescendercyrillic": "\u0498", + "/Zedieresiscyrillic": "\u04DE", + "/Zeta": "\u0396", + "/Zhearmenian": "\u053A", + "/Zhebrevecyrillic": "\u04C1", + "/Zhecyrillic": "\u0416", + "/Zhedescendercyrillic": "\u0496", + "/Zhedieresiscyrillic": "\u04DC", + "/Zlinebelow": "\u1E94", + "/Zmonospace": "\uFF3A", + "/Zsmall": "\uF77A", + "/Zstroke": "\u01B5", + "/a": "\u0061", + "/aabengali": "\u0986", + "/aacute": "\u00E1", + "/aadeva": "\u0906", + "/aagujarati": "\u0A86", + "/aagurmukhi": "\u0A06", + "/aamatragurmukhi": "\u0A3E", + "/aarusquare": "\u3303", + "/aavowelsignbengali": "\u09BE", + "/aavowelsigndeva": "\u093E", + "/aavowelsigngujarati": "\u0ABE", + "/abbreviationmarkarmenian": "\u055F", + "/abbreviationsigndeva": "\u0970", + "/abengali": "\u0985", + "/abopomofo": "\u311A", + "/abreve": "\u0103", + "/abreveacute": "\u1EAF", + "/abrevecyrillic": "\u04D1", + "/abrevedotbelow": "\u1EB7", + "/abrevegrave": "\u1EB1", + "/abrevehookabove": "\u1EB3", + "/abrevetilde": "\u1EB5", + "/acaron": "\u01CE", + "/acircle": "\u24D0", + "/acircumflex": "\u00E2", + "/acircumflexacute": "\u1EA5", + "/acircumflexdotbelow": "\u1EAD", + "/acircumflexgrave": "\u1EA7", + "/acircumflexhookabove": "\u1EA9", + "/acircumflextilde": "\u1EAB", + "/acute": "\u00B4", + "/acutebelowcmb": "\u0317", + "/acutecmb": "\u0301", + "/acutecomb": "\u0301", + "/acutedeva": "\u0954", + "/acutelowmod": "\u02CF", + "/acutetonecmb": "\u0341", + "/acyrillic": "\u0430", + "/adblgrave": "\u0201", + "/addakgurmukhi": "\u0A71", + "/adeva": "\u0905", + "/adieresis": "\u00E4", + "/adieresiscyrillic": "\u04D3", + "/adieresismacron": "\u01DF", + "/adotbelow": "\u1EA1", + "/adotmacron": "\u01E1", + "/ae": "\u00E6", + "/aeacute": "\u01FD", + "/aekorean": "\u3150", + "/aemacron": "\u01E3", + "/afii00208": "\u2015", + "/afii08941": "\u20A4", + "/afii10017": "\u0410", + "/afii10018": "\u0411", + "/afii10019": "\u0412", + "/afii10020": "\u0413", + "/afii10021": "\u0414", + "/afii10022": "\u0415", + "/afii10023": "\u0401", + "/afii10024": "\u0416", + "/afii10025": "\u0417", + "/afii10026": "\u0418", + "/afii10027": "\u0419", + "/afii10028": "\u041A", + "/afii10029": "\u041B", + "/afii10030": "\u041C", + "/afii10031": "\u041D", + "/afii10032": "\u041E", + "/afii10033": "\u041F", + "/afii10034": "\u0420", + "/afii10035": "\u0421", + "/afii10036": "\u0422", + "/afii10037": "\u0423", + "/afii10038": "\u0424", + "/afii10039": "\u0425", + "/afii10040": "\u0426", + "/afii10041": "\u0427", + "/afii10042": "\u0428", + "/afii10043": "\u0429", + "/afii10044": "\u042A", + "/afii10045": "\u042B", + "/afii10046": "\u042C", + "/afii10047": "\u042D", + "/afii10048": "\u042E", + "/afii10049": "\u042F", + "/afii10050": "\u0490", + "/afii10051": "\u0402", + "/afii10052": "\u0403", + "/afii10053": "\u0404", + "/afii10054": "\u0405", + "/afii10055": "\u0406", + "/afii10056": "\u0407", + "/afii10057": "\u0408", + "/afii10058": "\u0409", + "/afii10059": "\u040A", + "/afii10060": "\u040B", + "/afii10061": "\u040C", + "/afii10062": "\u040E", + "/afii10063": "\uF6C4", + "/afii10064": "\uF6C5", + "/afii10065": "\u0430", + "/afii10066": "\u0431", + "/afii10067": "\u0432", + "/afii10068": "\u0433", + "/afii10069": "\u0434", + "/afii10070": "\u0435", + "/afii10071": "\u0451", + "/afii10072": "\u0436", + "/afii10073": "\u0437", + "/afii10074": "\u0438", + "/afii10075": "\u0439", + "/afii10076": "\u043A", + "/afii10077": "\u043B", + "/afii10078": "\u043C", + "/afii10079": "\u043D", + "/afii10080": "\u043E", + "/afii10081": "\u043F", + "/afii10082": "\u0440", + "/afii10083": "\u0441", + "/afii10084": "\u0442", + "/afii10085": "\u0443", + "/afii10086": "\u0444", + "/afii10087": "\u0445", + "/afii10088": "\u0446", + "/afii10089": "\u0447", + "/afii10090": "\u0448", + "/afii10091": "\u0449", + "/afii10092": "\u044A", + "/afii10093": "\u044B", + "/afii10094": "\u044C", + "/afii10095": "\u044D", + "/afii10096": "\u044E", + "/afii10097": "\u044F", + "/afii10098": "\u0491", + "/afii10099": "\u0452", + "/afii10100": "\u0453", + "/afii10101": "\u0454", + "/afii10102": "\u0455", + "/afii10103": "\u0456", + "/afii10104": "\u0457", + "/afii10105": "\u0458", + "/afii10106": "\u0459", + "/afii10107": "\u045A", + "/afii10108": "\u045B", + "/afii10109": "\u045C", + "/afii10110": "\u045E", + "/afii10145": "\u040F", + "/afii10146": "\u0462", + "/afii10147": "\u0472", + "/afii10148": "\u0474", + "/afii10192": "\uF6C6", + "/afii10193": "\u045F", + "/afii10194": "\u0463", + "/afii10195": "\u0473", + "/afii10196": "\u0475", + "/afii10831": "\uF6C7", + "/afii10832": "\uF6C8", + "/afii10846": "\u04D9", + "/afii299": "\u200E", + "/afii300": "\u200F", + "/afii301": "\u200D", + "/afii57381": "\u066A", + "/afii57388": "\u060C", + "/afii57392": "\u0660", + "/afii57393": "\u0661", + "/afii57394": "\u0662", + "/afii57395": "\u0663", + "/afii57396": "\u0664", + "/afii57397": "\u0665", + "/afii57398": "\u0666", + "/afii57399": "\u0667", + "/afii57400": "\u0668", + "/afii57401": "\u0669", + "/afii57403": "\u061B", + "/afii57407": "\u061F", + "/afii57409": "\u0621", + "/afii57410": "\u0622", + "/afii57411": "\u0623", + "/afii57412": "\u0624", + "/afii57413": "\u0625", + "/afii57414": "\u0626", + "/afii57415": "\u0627", + "/afii57416": "\u0628", + "/afii57417": "\u0629", + "/afii57418": "\u062A", + "/afii57419": "\u062B", + "/afii57420": "\u062C", + "/afii57421": "\u062D", + "/afii57422": "\u062E", + "/afii57423": "\u062F", + "/afii57424": "\u0630", + "/afii57425": "\u0631", + "/afii57426": "\u0632", + "/afii57427": "\u0633", + "/afii57428": "\u0634", + "/afii57429": "\u0635", + "/afii57430": "\u0636", + "/afii57431": "\u0637", + "/afii57432": "\u0638", + "/afii57433": "\u0639", + "/afii57434": "\u063A", + "/afii57440": "\u0640", + "/afii57441": "\u0641", + "/afii57442": "\u0642", + "/afii57443": "\u0643", + "/afii57444": "\u0644", + "/afii57445": "\u0645", + "/afii57446": "\u0646", + "/afii57448": "\u0648", + "/afii57449": "\u0649", + "/afii57450": "\u064A", + "/afii57451": "\u064B", + "/afii57452": "\u064C", + "/afii57453": "\u064D", + "/afii57454": "\u064E", + "/afii57455": "\u064F", + "/afii57456": "\u0650", + "/afii57457": "\u0651", + "/afii57458": "\u0652", + "/afii57470": "\u0647", + "/afii57505": "\u06A4", + "/afii57506": "\u067E", + "/afii57507": "\u0686", + "/afii57508": "\u0698", + "/afii57509": "\u06AF", + "/afii57511": "\u0679", + "/afii57512": "\u0688", + "/afii57513": "\u0691", + "/afii57514": "\u06BA", + "/afii57519": "\u06D2", + "/afii57534": "\u06D5", + "/afii57636": "\u20AA", + "/afii57645": "\u05BE", + "/afii57658": "\u05C3", + "/afii57664": "\u05D0", + "/afii57665": "\u05D1", + "/afii57666": "\u05D2", + "/afii57667": "\u05D3", + "/afii57668": "\u05D4", + "/afii57669": "\u05D5", + "/afii57670": "\u05D6", + "/afii57671": "\u05D7", + "/afii57672": "\u05D8", + "/afii57673": "\u05D9", + "/afii57674": "\u05DA", + "/afii57675": "\u05DB", + "/afii57676": "\u05DC", + "/afii57677": "\u05DD", + "/afii57678": "\u05DE", + "/afii57679": "\u05DF", + "/afii57680": "\u05E0", + "/afii57681": "\u05E1", + "/afii57682": "\u05E2", + "/afii57683": "\u05E3", + "/afii57684": "\u05E4", + "/afii57685": "\u05E5", + "/afii57686": "\u05E6", + "/afii57687": "\u05E7", + "/afii57688": "\u05E8", + "/afii57689": "\u05E9", + "/afii57690": "\u05EA", + "/afii57694": "\uFB2A", + "/afii57695": "\uFB2B", + "/afii57700": "\uFB4B", + "/afii57705": "\uFB1F", + "/afii57716": "\u05F0", + "/afii57717": "\u05F1", + "/afii57718": "\u05F2", + "/afii57723": "\uFB35", + "/afii57793": "\u05B4", + "/afii57794": "\u05B5", + "/afii57795": "\u05B6", + "/afii57796": "\u05BB", + "/afii57797": "\u05B8", + "/afii57798": "\u05B7", + "/afii57799": "\u05B0", + "/afii57800": "\u05B2", + "/afii57801": "\u05B1", + "/afii57802": "\u05B3", + "/afii57803": "\u05C2", + "/afii57804": "\u05C1", + "/afii57806": "\u05B9", + "/afii57807": "\u05BC", + "/afii57839": "\u05BD", + "/afii57841": "\u05BF", + "/afii57842": "\u05C0", + "/afii57929": "\u02BC", + "/afii61248": "\u2105", + "/afii61289": "\u2113", + "/afii61352": "\u2116", + "/afii61573": "\u202C", + "/afii61574": "\u202D", + "/afii61575": "\u202E", + "/afii61664": "\u200C", + "/afii63167": "\u066D", + "/afii64937": "\u02BD", + "/agrave": "\u00E0", + "/agujarati": "\u0A85", + "/agurmukhi": "\u0A05", + "/ahiragana": "\u3042", + "/ahookabove": "\u1EA3", + "/aibengali": "\u0990", + "/aibopomofo": "\u311E", + "/aideva": "\u0910", + "/aiecyrillic": "\u04D5", + "/aigujarati": "\u0A90", + "/aigurmukhi": "\u0A10", + "/aimatragurmukhi": "\u0A48", + "/ainarabic": "\u0639", + "/ainfinalarabic": "\uFECA", + "/aininitialarabic": "\uFECB", + "/ainmedialarabic": "\uFECC", + "/ainvertedbreve": "\u0203", + "/aivowelsignbengali": "\u09C8", + "/aivowelsigndeva": "\u0948", + "/aivowelsigngujarati": "\u0AC8", + "/akatakana": "\u30A2", + "/akatakanahalfwidth": "\uFF71", + "/akorean": "\u314F", + "/alef": "\u05D0", + "/alefarabic": "\u0627", + "/alefdageshhebrew": "\uFB30", + "/aleffinalarabic": "\uFE8E", + "/alefhamzaabovearabic": "\u0623", + "/alefhamzaabovefinalarabic": "\uFE84", + "/alefhamzabelowarabic": "\u0625", + "/alefhamzabelowfinalarabic": "\uFE88", + "/alefhebrew": "\u05D0", + "/aleflamedhebrew": "\uFB4F", + "/alefmaddaabovearabic": "\u0622", + "/alefmaddaabovefinalarabic": "\uFE82", + "/alefmaksuraarabic": "\u0649", + "/alefmaksurafinalarabic": "\uFEF0", + "/alefmaksurainitialarabic": "\uFEF3", + "/alefmaksuramedialarabic": "\uFEF4", + "/alefpatahhebrew": "\uFB2E", + "/alefqamatshebrew": "\uFB2F", + "/aleph": "\u2135", + "/allequal": "\u224C", + "/alpha": "\u03B1", + "/alphatonos": "\u03AC", + "/amacron": "\u0101", + "/amonospace": "\uFF41", + "/ampersand": "\u0026", + "/ampersandmonospace": "\uFF06", + "/ampersandsmall": "\uF726", + "/amsquare": "\u33C2", + "/anbopomofo": "\u3122", + "/angbopomofo": "\u3124", + "/angkhankhuthai": "\u0E5A", + "/angle": "\u2220", + "/anglebracketleft": "\u3008", + "/anglebracketleftvertical": "\uFE3F", + "/anglebracketright": "\u3009", + "/anglebracketrightvertical": "\uFE40", + "/angleleft": "\u2329", + "/angleright": "\u232A", + "/angstrom": "\u212B", + "/anoteleia": "\u0387", + "/anudattadeva": "\u0952", + "/anusvarabengali": "\u0982", + "/anusvaradeva": "\u0902", + "/anusvaragujarati": "\u0A82", + "/aogonek": "\u0105", + "/apaatosquare": "\u3300", + "/aparen": "\u249C", + "/apostrophearmenian": "\u055A", + "/apostrophemod": "\u02BC", + "/apple": "\uF8FF", + "/approaches": "\u2250", + "/approxequal": "\u2248", + "/approxequalorimage": "\u2252", + "/approximatelyequal": "\u2245", + "/araeaekorean": "\u318E", + "/araeakorean": "\u318D", + "/arc": "\u2312", + "/arighthalfring": "\u1E9A", + "/aring": "\u00E5", + "/aringacute": "\u01FB", + "/aringbelow": "\u1E01", + "/arrowboth": "\u2194", + "/arrowdashdown": "\u21E3", + "/arrowdashleft": "\u21E0", + "/arrowdashright": "\u21E2", + "/arrowdashup": "\u21E1", + "/arrowdblboth": "\u21D4", + "/arrowdbldown": "\u21D3", + "/arrowdblleft": "\u21D0", + "/arrowdblright": "\u21D2", + "/arrowdblup": "\u21D1", + "/arrowdown": "\u2193", + "/arrowdownleft": "\u2199", + "/arrowdownright": "\u2198", + "/arrowdownwhite": "\u21E9", + "/arrowheaddownmod": "\u02C5", + "/arrowheadleftmod": "\u02C2", + "/arrowheadrightmod": "\u02C3", + "/arrowheadupmod": "\u02C4", + "/arrowhorizex": "\uF8E7", + "/arrowleft": "\u2190", + "/arrowleftdbl": "\u21D0", + "/arrowleftdblstroke": "\u21CD", + "/arrowleftoverright": "\u21C6", + "/arrowleftwhite": "\u21E6", + "/arrowright": "\u2192", + "/arrowrightdblstroke": "\u21CF", + "/arrowrightheavy": "\u279E", + "/arrowrightoverleft": "\u21C4", + "/arrowrightwhite": "\u21E8", + "/arrowtableft": "\u21E4", + "/arrowtabright": "\u21E5", + "/arrowup": "\u2191", + "/arrowupdn": "\u2195", + "/arrowupdnbse": "\u21A8", + "/arrowupdownbase": "\u21A8", + "/arrowupleft": "\u2196", + "/arrowupleftofdown": "\u21C5", + "/arrowupright": "\u2197", + "/arrowupwhite": "\u21E7", + "/arrowvertex": "\uF8E6", + "/asciicircum": "\u005E", + "/asciicircummonospace": "\uFF3E", + "/asciitilde": "\u007E", + "/asciitildemonospace": "\uFF5E", + "/ascript": "\u0251", + "/ascriptturned": "\u0252", + "/asmallhiragana": "\u3041", + "/asmallkatakana": "\u30A1", + "/asmallkatakanahalfwidth": "\uFF67", + "/asterisk": "\u002A", + "/asteriskaltonearabic": "\u066D", + "/asteriskarabic": "\u066D", + "/asteriskmath": "\u2217", + "/asteriskmonospace": "\uFF0A", + "/asterisksmall": "\uFE61", + "/asterism": "\u2042", + "/asuperior": "\uF6E9", + "/asymptoticallyequal": "\u2243", + "/at": "\u0040", + "/atilde": "\u00E3", + "/atmonospace": "\uFF20", + "/atsmall": "\uFE6B", + "/aturned": "\u0250", + "/aubengali": "\u0994", + "/aubopomofo": "\u3120", + "/audeva": "\u0914", + "/augujarati": "\u0A94", + "/augurmukhi": "\u0A14", + "/aulengthmarkbengali": "\u09D7", + "/aumatragurmukhi": "\u0A4C", + "/auvowelsignbengali": "\u09CC", + "/auvowelsigndeva": "\u094C", + "/auvowelsigngujarati": "\u0ACC", + "/avagrahadeva": "\u093D", + "/aybarmenian": "\u0561", + "/ayin": "\u05E2", + "/ayinaltonehebrew": "\uFB20", + "/ayinhebrew": "\u05E2", + "/b": "\u0062", + "/babengali": "\u09AC", + "/backslash": "\u005C", + "/backslashmonospace": "\uFF3C", + "/badeva": "\u092C", + "/bagujarati": "\u0AAC", + "/bagurmukhi": "\u0A2C", + "/bahiragana": "\u3070", + "/bahtthai": "\u0E3F", + "/bakatakana": "\u30D0", + "/bar": "\u007C", + "/barmonospace": "\uFF5C", + "/bbopomofo": "\u3105", + "/bcircle": "\u24D1", + "/bdotaccent": "\u1E03", + "/bdotbelow": "\u1E05", + "/beamedsixteenthnotes": "\u266C", + "/because": "\u2235", + "/becyrillic": "\u0431", + "/beharabic": "\u0628", + "/behfinalarabic": "\uFE90", + "/behinitialarabic": "\uFE91", + "/behiragana": "\u3079", + "/behmedialarabic": "\uFE92", + "/behmeeminitialarabic": "\uFC9F", + "/behmeemisolatedarabic": "\uFC08", + "/behnoonfinalarabic": "\uFC6D", + "/bekatakana": "\u30D9", + "/benarmenian": "\u0562", + "/bet": "\u05D1", + "/beta": "\u03B2", + "/betasymbolgreek": "\u03D0", + "/betdagesh": "\uFB31", + "/betdageshhebrew": "\uFB31", + "/bethebrew": "\u05D1", + "/betrafehebrew": "\uFB4C", + "/bhabengali": "\u09AD", + "/bhadeva": "\u092D", + "/bhagujarati": "\u0AAD", + "/bhagurmukhi": "\u0A2D", + "/bhook": "\u0253", + "/bihiragana": "\u3073", + "/bikatakana": "\u30D3", + "/bilabialclick": "\u0298", + "/bindigurmukhi": "\u0A02", + "/birusquare": "\u3331", + "/blackcircle": "\u25CF", + "/blackdiamond": "\u25C6", + "/blackdownpointingtriangle": "\u25BC", + "/blackleftpointingpointer": "\u25C4", + "/blackleftpointingtriangle": "\u25C0", + "/blacklenticularbracketleft": "\u3010", + "/blacklenticularbracketleftvertical": "\uFE3B", + "/blacklenticularbracketright": "\u3011", + "/blacklenticularbracketrightvertical": "\uFE3C", + "/blacklowerlefttriangle": "\u25E3", + "/blacklowerrighttriangle": "\u25E2", + "/blackrectangle": "\u25AC", + "/blackrightpointingpointer": "\u25BA", + "/blackrightpointingtriangle": "\u25B6", + "/blacksmallsquare": "\u25AA", + "/blacksmilingface": "\u263B", + "/blacksquare": "\u25A0", + "/blackstar": "\u2605", + "/blackupperlefttriangle": "\u25E4", + "/blackupperrighttriangle": "\u25E5", + "/blackuppointingsmalltriangle": "\u25B4", + "/blackuppointingtriangle": "\u25B2", + "/blank": "\u2423", + "/blinebelow": "\u1E07", + "/block": "\u2588", + "/bmonospace": "\uFF42", + "/bobaimaithai": "\u0E1A", + "/bohiragana": "\u307C", + "/bokatakana": "\u30DC", + "/bparen": "\u249D", + "/bqsquare": "\u33C3", + "/braceex": "\uF8F4", + "/braceleft": "\u007B", + "/braceleftbt": "\uF8F3", + "/braceleftmid": "\uF8F2", + "/braceleftmonospace": "\uFF5B", + "/braceleftsmall": "\uFE5B", + "/bracelefttp": "\uF8F1", + "/braceleftvertical": "\uFE37", + "/braceright": "\u007D", + "/bracerightbt": "\uF8FE", + "/bracerightmid": "\uF8FD", + "/bracerightmonospace": "\uFF5D", + "/bracerightsmall": "\uFE5C", + "/bracerighttp": "\uF8FC", + "/bracerightvertical": "\uFE38", + "/bracketleft": "\u005B", + "/bracketleftbt": "\uF8F0", + "/bracketleftex": "\uF8EF", + "/bracketleftmonospace": "\uFF3B", + "/bracketlefttp": "\uF8EE", + "/bracketright": "\u005D", + "/bracketrightbt": "\uF8FB", + "/bracketrightex": "\uF8FA", + "/bracketrightmonospace": "\uFF3D", + "/bracketrighttp": "\uF8F9", + "/breve": "\u02D8", + "/brevebelowcmb": "\u032E", + "/brevecmb": "\u0306", + "/breveinvertedbelowcmb": "\u032F", + "/breveinvertedcmb": "\u0311", + "/breveinverteddoublecmb": "\u0361", + "/bridgebelowcmb": "\u032A", + "/bridgeinvertedbelowcmb": "\u033A", + "/brokenbar": "\u00A6", + "/bstroke": "\u0180", + "/bsuperior": "\uF6EA", + "/btopbar": "\u0183", + "/buhiragana": "\u3076", + "/bukatakana": "\u30D6", + "/bullet": "\u2022", + "/bulletinverse": "\u25D8", + "/bulletoperator": "\u2219", + "/bullseye": "\u25CE", + "/c": "\u0063", + "/caarmenian": "\u056E", + "/cabengali": "\u099A", + "/cacute": "\u0107", + "/cadeva": "\u091A", + "/cagujarati": "\u0A9A", + "/cagurmukhi": "\u0A1A", + "/calsquare": "\u3388", + "/candrabindubengali": "\u0981", + "/candrabinducmb": "\u0310", + "/candrabindudeva": "\u0901", + "/candrabindugujarati": "\u0A81", + "/capslock": "\u21EA", + "/careof": "\u2105", + "/caron": "\u02C7", + "/caronbelowcmb": "\u032C", + "/caroncmb": "\u030C", + "/carriagereturn": "\u21B5", + "/cbopomofo": "\u3118", + "/ccaron": "\u010D", + "/ccedilla": "\u00E7", + "/ccedillaacute": "\u1E09", + "/ccircle": "\u24D2", + "/ccircumflex": "\u0109", + "/ccurl": "\u0255", + "/cdot": "\u010B", + "/cdotaccent": "\u010B", + "/cdsquare": "\u33C5", + "/cedilla": "\u00B8", + "/cedillacmb": "\u0327", + "/cent": "\u00A2", + "/centigrade": "\u2103", + "/centinferior": "\uF6DF", + "/centmonospace": "\uFFE0", + "/centoldstyle": "\uF7A2", + "/centsuperior": "\uF6E0", + "/chaarmenian": "\u0579", + "/chabengali": "\u099B", + "/chadeva": "\u091B", + "/chagujarati": "\u0A9B", + "/chagurmukhi": "\u0A1B", + "/chbopomofo": "\u3114", + "/cheabkhasiancyrillic": "\u04BD", + "/checkmark": "\u2713", + "/checyrillic": "\u0447", + "/chedescenderabkhasiancyrillic": "\u04BF", + "/chedescendercyrillic": "\u04B7", + "/chedieresiscyrillic": "\u04F5", + "/cheharmenian": "\u0573", + "/chekhakassiancyrillic": "\u04CC", + "/cheverticalstrokecyrillic": "\u04B9", + "/chi": "\u03C7", + "/chieuchacirclekorean": "\u3277", + "/chieuchaparenkorean": "\u3217", + "/chieuchcirclekorean": "\u3269", + "/chieuchkorean": "\u314A", + "/chieuchparenkorean": "\u3209", + "/chochangthai": "\u0E0A", + "/chochanthai": "\u0E08", + "/chochingthai": "\u0E09", + "/chochoethai": "\u0E0C", + "/chook": "\u0188", + "/cieucacirclekorean": "\u3276", + "/cieucaparenkorean": "\u3216", + "/cieuccirclekorean": "\u3268", + "/cieuckorean": "\u3148", + "/cieucparenkorean": "\u3208", + "/cieucuparenkorean": "\u321C", + "/circle": "\u25CB", + "/circlemultiply": "\u2297", + "/circleot": "\u2299", + "/circleplus": "\u2295", + "/circlepostalmark": "\u3036", + "/circlewithlefthalfblack": "\u25D0", + "/circlewithrighthalfblack": "\u25D1", + "/circumflex": "\u02C6", + "/circumflexbelowcmb": "\u032D", + "/circumflexcmb": "\u0302", + "/clear": "\u2327", + "/clickalveolar": "\u01C2", + "/clickdental": "\u01C0", + "/clicklateral": "\u01C1", + "/clickretroflex": "\u01C3", + "/club": "\u2663", + "/clubsuitblack": "\u2663", + "/clubsuitwhite": "\u2667", + "/cmcubedsquare": "\u33A4", + "/cmonospace": "\uFF43", + "/cmsquaredsquare": "\u33A0", + "/coarmenian": "\u0581", + "/colon": "\u003A", + "/colonmonetary": "\u20A1", + "/colonmonospace": "\uFF1A", + "/colonsign": "\u20A1", + "/colonsmall": "\uFE55", + "/colontriangularhalfmod": "\u02D1", + "/colontriangularmod": "\u02D0", + "/comma": "\u002C", + "/commaabovecmb": "\u0313", + "/commaaboverightcmb": "\u0315", + "/commaaccent": "\uF6C3", + "/commaarabic": "\u060C", + "/commaarmenian": "\u055D", + "/commainferior": "\uF6E1", + "/commamonospace": "\uFF0C", + "/commareversedabovecmb": "\u0314", + "/commareversedmod": "\u02BD", + "/commasmall": "\uFE50", + "/commasuperior": "\uF6E2", + "/commaturnedabovecmb": "\u0312", + "/commaturnedmod": "\u02BB", + "/compass": "\u263C", + "/congruent": "\u2245", + "/contourintegral": "\u222E", + "/control": "\u2303", + "/controlACK": "\u0006", + "/controlBEL": "\u0007", + "/controlBS": "\u0008", + "/controlCAN": "\u0018", + "/controlCR": "\u000D", + "/controlDC1": "\u0011", + "/controlDC2": "\u0012", + "/controlDC3": "\u0013", + "/controlDC4": "\u0014", + "/controlDEL": "\u007F", + "/controlDLE": "\u0010", + "/controlEM": "\u0019", + "/controlENQ": "\u0005", + "/controlEOT": "\u0004", + "/controlESC": "\u001B", + "/controlETB": "\u0017", + "/controlETX": "\u0003", + "/controlFF": "\u000C", + "/controlFS": "\u001C", + "/controlGS": "\u001D", + "/controlHT": "\u0009", + "/controlLF": "\u000A", + "/controlNAK": "\u0015", + "/controlRS": "\u001E", + "/controlSI": "\u000F", + "/controlSO": "\u000E", + "/controlSOT": "\u0002", + "/controlSTX": "\u0001", + "/controlSUB": "\u001A", + "/controlSYN": "\u0016", + "/controlUS": "\u001F", + "/controlVT": "\u000B", + "/copyright": "\u00A9", + "/copyrightsans": "\uF8E9", + "/copyrightserif": "\uF6D9", + "/cornerbracketleft": "\u300C", + "/cornerbracketlefthalfwidth": "\uFF62", + "/cornerbracketleftvertical": "\uFE41", + "/cornerbracketright": "\u300D", + "/cornerbracketrighthalfwidth": "\uFF63", + "/cornerbracketrightvertical": "\uFE42", + "/corporationsquare": "\u337F", + "/cosquare": "\u33C7", + "/coverkgsquare": "\u33C6", + "/cparen": "\u249E", + "/cruzeiro": "\u20A2", + "/cstretched": "\u0297", + "/curlyand": "\u22CF", + "/curlyor": "\u22CE", + "/currency": "\u00A4", + "/cyrBreve": "\uF6D1", + "/cyrFlex": "\uF6D2", + "/cyrbreve": "\uF6D4", + "/cyrflex": "\uF6D5", + "/d": "\u0064", + "/daarmenian": "\u0564", + "/dabengali": "\u09A6", + "/dadarabic": "\u0636", + "/dadeva": "\u0926", + "/dadfinalarabic": "\uFEBE", + "/dadinitialarabic": "\uFEBF", + "/dadmedialarabic": "\uFEC0", + "/dagesh": "\u05BC", + "/dageshhebrew": "\u05BC", + "/dagger": "\u2020", + "/daggerdbl": "\u2021", + "/dagujarati": "\u0AA6", + "/dagurmukhi": "\u0A26", + "/dahiragana": "\u3060", + "/dakatakana": "\u30C0", + "/dalarabic": "\u062F", + "/dalet": "\u05D3", + "/daletdagesh": "\uFB33", + "/daletdageshhebrew": "\uFB33", + "/dalethatafpatah": "\u05D3\u05B2", + "/dalethatafpatahhebrew": "\u05D3\u05B2", + "/dalethatafsegol": "\u05D3\u05B1", + "/dalethatafsegolhebrew": "\u05D3\u05B1", + "/dalethebrew": "\u05D3", + "/dalethiriq": "\u05D3\u05B4", + "/dalethiriqhebrew": "\u05D3\u05B4", + "/daletholam": "\u05D3\u05B9", + "/daletholamhebrew": "\u05D3\u05B9", + "/daletpatah": "\u05D3\u05B7", + "/daletpatahhebrew": "\u05D3\u05B7", + "/daletqamats": "\u05D3\u05B8", + "/daletqamatshebrew": "\u05D3\u05B8", + "/daletqubuts": "\u05D3\u05BB", + "/daletqubutshebrew": "\u05D3\u05BB", + "/daletsegol": "\u05D3\u05B6", + "/daletsegolhebrew": "\u05D3\u05B6", + "/daletsheva": "\u05D3\u05B0", + "/daletshevahebrew": "\u05D3\u05B0", + "/dalettsere": "\u05D3\u05B5", + "/dalettserehebrew": "\u05D3\u05B5", + "/dalfinalarabic": "\uFEAA", + "/dammaarabic": "\u064F", + "/dammalowarabic": "\u064F", + "/dammatanaltonearabic": "\u064C", + "/dammatanarabic": "\u064C", + "/danda": "\u0964", + "/dargahebrew": "\u05A7", + "/dargalefthebrew": "\u05A7", + "/dasiapneumatacyrilliccmb": "\u0485", + "/dblGrave": "\uF6D3", + "/dblanglebracketleft": "\u300A", + "/dblanglebracketleftvertical": "\uFE3D", + "/dblanglebracketright": "\u300B", + "/dblanglebracketrightvertical": "\uFE3E", + "/dblarchinvertedbelowcmb": "\u032B", + "/dblarrowleft": "\u21D4", + "/dblarrowright": "\u21D2", + "/dbldanda": "\u0965", + "/dblgrave": "\uF6D6", + "/dblgravecmb": "\u030F", + "/dblintegral": "\u222C", + "/dbllowline": "\u2017", + "/dbllowlinecmb": "\u0333", + "/dbloverlinecmb": "\u033F", + "/dblprimemod": "\u02BA", + "/dblverticalbar": "\u2016", + "/dblverticallineabovecmb": "\u030E", + "/dbopomofo": "\u3109", + "/dbsquare": "\u33C8", + "/dcaron": "\u010F", + "/dcedilla": "\u1E11", + "/dcircle": "\u24D3", + "/dcircumflexbelow": "\u1E13", + "/dcroat": "\u0111", + "/ddabengali": "\u09A1", + "/ddadeva": "\u0921", + "/ddagujarati": "\u0AA1", + "/ddagurmukhi": "\u0A21", + "/ddalarabic": "\u0688", + "/ddalfinalarabic": "\uFB89", + "/dddhadeva": "\u095C", + "/ddhabengali": "\u09A2", + "/ddhadeva": "\u0922", + "/ddhagujarati": "\u0AA2", + "/ddhagurmukhi": "\u0A22", + "/ddotaccent": "\u1E0B", + "/ddotbelow": "\u1E0D", + "/decimalseparatorarabic": "\u066B", + "/decimalseparatorpersian": "\u066B", + "/decyrillic": "\u0434", + "/degree": "\u00B0", + "/dehihebrew": "\u05AD", + "/dehiragana": "\u3067", + "/deicoptic": "\u03EF", + "/dekatakana": "\u30C7", + "/deleteleft": "\u232B", + "/deleteright": "\u2326", + "/delta": "\u03B4", + "/deltaturned": "\u018D", + "/denominatorminusonenumeratorbengali": "\u09F8", + "/dezh": "\u02A4", + "/dhabengali": "\u09A7", + "/dhadeva": "\u0927", + "/dhagujarati": "\u0AA7", + "/dhagurmukhi": "\u0A27", + "/dhook": "\u0257", + "/dialytikatonos": "\u0385", + "/dialytikatonoscmb": "\u0344", + "/diamond": "\u2666", + "/diamondsuitwhite": "\u2662", + "/dieresis": "\u00A8", + "/dieresisacute": "\uF6D7", + "/dieresisbelowcmb": "\u0324", + "/dieresiscmb": "\u0308", + "/dieresisgrave": "\uF6D8", + "/dieresistonos": "\u0385", + "/dihiragana": "\u3062", + "/dikatakana": "\u30C2", + "/dittomark": "\u3003", + "/divide": "\u00F7", + "/divides": "\u2223", + "/divisionslash": "\u2215", + "/djecyrillic": "\u0452", + "/dkshade": "\u2593", + "/dlinebelow": "\u1E0F", + "/dlsquare": "\u3397", + "/dmacron": "\u0111", + "/dmonospace": "\uFF44", + "/dnblock": "\u2584", + "/dochadathai": "\u0E0E", + "/dodekthai": "\u0E14", + "/dohiragana": "\u3069", + "/dokatakana": "\u30C9", + "/dollar": "\u0024", + "/dollarinferior": "\uF6E3", + "/dollarmonospace": "\uFF04", + "/dollaroldstyle": "\uF724", + "/dollarsmall": "\uFE69", + "/dollarsuperior": "\uF6E4", + "/dong": "\u20AB", + "/dorusquare": "\u3326", + "/dotaccent": "\u02D9", + "/dotaccentcmb": "\u0307", + "/dotbelowcmb": "\u0323", + "/dotbelowcomb": "\u0323", + "/dotkatakana": "\u30FB", + "/dotlessi": "\u0131", + "/dotlessj": "\uF6BE", + "/dotlessjstrokehook": "\u0284", + "/dotmath": "\u22C5", + "/dottedcircle": "\u25CC", + "/doubleyodpatah": "\uFB1F", + "/doubleyodpatahhebrew": "\uFB1F", + "/downtackbelowcmb": "\u031E", + "/downtackmod": "\u02D5", + "/dparen": "\u249F", + "/dsuperior": "\uF6EB", + "/dtail": "\u0256", + "/dtopbar": "\u018C", + "/duhiragana": "\u3065", + "/dukatakana": "\u30C5", + "/dz": "\u01F3", + "/dzaltone": "\u02A3", + "/dzcaron": "\u01C6", + "/dzcurl": "\u02A5", + "/dzeabkhasiancyrillic": "\u04E1", + "/dzecyrillic": "\u0455", + "/dzhecyrillic": "\u045F", + "/e": "\u0065", + "/eacute": "\u00E9", + "/earth": "\u2641", + "/ebengali": "\u098F", + "/ebopomofo": "\u311C", + "/ebreve": "\u0115", + "/ecandradeva": "\u090D", + "/ecandragujarati": "\u0A8D", + "/ecandravowelsigndeva": "\u0945", + "/ecandravowelsigngujarati": "\u0AC5", + "/ecaron": "\u011B", + "/ecedillabreve": "\u1E1D", + "/echarmenian": "\u0565", + "/echyiwnarmenian": "\u0587", + "/ecircle": "\u24D4", + "/ecircumflex": "\u00EA", + "/ecircumflexacute": "\u1EBF", + "/ecircumflexbelow": "\u1E19", + "/ecircumflexdotbelow": "\u1EC7", + "/ecircumflexgrave": "\u1EC1", + "/ecircumflexhookabove": "\u1EC3", + "/ecircumflextilde": "\u1EC5", + "/ecyrillic": "\u0454", + "/edblgrave": "\u0205", + "/edeva": "\u090F", + "/edieresis": "\u00EB", + "/edot": "\u0117", + "/edotaccent": "\u0117", + "/edotbelow": "\u1EB9", + "/eegurmukhi": "\u0A0F", + "/eematragurmukhi": "\u0A47", + "/efcyrillic": "\u0444", + "/egrave": "\u00E8", + "/egujarati": "\u0A8F", + "/eharmenian": "\u0567", + "/ehbopomofo": "\u311D", + "/ehiragana": "\u3048", + "/ehookabove": "\u1EBB", + "/eibopomofo": "\u311F", + "/eight": "\u0038", + "/eightarabic": "\u0668", + "/eightbengali": "\u09EE", + "/eightcircle": "\u2467", + "/eightcircleinversesansserif": "\u2791", + "/eightdeva": "\u096E", + "/eighteencircle": "\u2471", + "/eighteenparen": "\u2485", + "/eighteenperiod": "\u2499", + "/eightgujarati": "\u0AEE", + "/eightgurmukhi": "\u0A6E", + "/eighthackarabic": "\u0668", + "/eighthangzhou": "\u3028", + "/eighthnotebeamed": "\u266B", + "/eightideographicparen": "\u3227", + "/eightinferior": "\u2088", + "/eightmonospace": "\uFF18", + "/eightoldstyle": "\uF738", + "/eightparen": "\u247B", + "/eightperiod": "\u248F", + "/eightpersian": "\u06F8", + "/eightroman": "\u2177", + "/eightsuperior": "\u2078", + "/eightthai": "\u0E58", + "/einvertedbreve": "\u0207", + "/eiotifiedcyrillic": "\u0465", + "/ekatakana": "\u30A8", + "/ekatakanahalfwidth": "\uFF74", + "/ekonkargurmukhi": "\u0A74", + "/ekorean": "\u3154", + "/elcyrillic": "\u043B", + "/element": "\u2208", + "/elevencircle": "\u246A", + "/elevenparen": "\u247E", + "/elevenperiod": "\u2492", + "/elevenroman": "\u217A", + "/ellipsis": "\u2026", + "/ellipsisvertical": "\u22EE", + "/emacron": "\u0113", + "/emacronacute": "\u1E17", + "/emacrongrave": "\u1E15", + "/emcyrillic": "\u043C", + "/emdash": "\u2014", + "/emdashvertical": "\uFE31", + "/emonospace": "\uFF45", + "/emphasismarkarmenian": "\u055B", + "/emptyset": "\u2205", + "/enbopomofo": "\u3123", + "/encyrillic": "\u043D", + "/endash": "\u2013", + "/endashvertical": "\uFE32", + "/endescendercyrillic": "\u04A3", + "/eng": "\u014B", + "/engbopomofo": "\u3125", + "/enghecyrillic": "\u04A5", + "/enhookcyrillic": "\u04C8", + "/enspace": "\u2002", + "/eogonek": "\u0119", + "/eokorean": "\u3153", + "/eopen": "\u025B", + "/eopenclosed": "\u029A", + "/eopenreversed": "\u025C", + "/eopenreversedclosed": "\u025E", + "/eopenreversedhook": "\u025D", + "/eparen": "\u24A0", + "/epsilon": "\u03B5", + "/epsilontonos": "\u03AD", + "/equal": "\u003D", + "/equalmonospace": "\uFF1D", + "/equalsmall": "\uFE66", + "/equalsuperior": "\u207C", + "/equivalence": "\u2261", + "/erbopomofo": "\u3126", + "/ercyrillic": "\u0440", + "/ereversed": "\u0258", + "/ereversedcyrillic": "\u044D", + "/escyrillic": "\u0441", + "/esdescendercyrillic": "\u04AB", + "/esh": "\u0283", + "/eshcurl": "\u0286", + "/eshortdeva": "\u090E", + "/eshortvowelsigndeva": "\u0946", + "/eshreversedloop": "\u01AA", + "/eshsquatreversed": "\u0285", + "/esmallhiragana": "\u3047", + "/esmallkatakana": "\u30A7", + "/esmallkatakanahalfwidth": "\uFF6A", + "/estimated": "\u212E", + "/esuperior": "\uF6EC", + "/eta": "\u03B7", + "/etarmenian": "\u0568", + "/etatonos": "\u03AE", + "/eth": "\u00F0", + "/etilde": "\u1EBD", + "/etildebelow": "\u1E1B", + "/etnahtafoukhhebrew": "\u0591", + "/etnahtafoukhlefthebrew": "\u0591", + "/etnahtahebrew": "\u0591", + "/etnahtalefthebrew": "\u0591", + "/eturned": "\u01DD", + "/eukorean": "\u3161", + "/euro": "\u20AC", + "/evowelsignbengali": "\u09C7", + "/evowelsigndeva": "\u0947", + "/evowelsigngujarati": "\u0AC7", + "/exclam": "\u0021", + "/exclamarmenian": "\u055C", + "/exclamdbl": "\u203C", + "/exclamdown": "\u00A1", + "/exclamdownsmall": "\uF7A1", + "/exclammonospace": "\uFF01", + "/exclamsmall": "\uF721", + "/existential": "\u2203", + "/ezh": "\u0292", + "/ezhcaron": "\u01EF", + "/ezhcurl": "\u0293", + "/ezhreversed": "\u01B9", + "/ezhtail": "\u01BA", + "/f": "\u0066", + "/fadeva": "\u095E", + "/fagurmukhi": "\u0A5E", + "/fahrenheit": "\u2109", + "/fathaarabic": "\u064E", + "/fathalowarabic": "\u064E", + "/fathatanarabic": "\u064B", + "/fbopomofo": "\u3108", + "/fcircle": "\u24D5", + "/fdotaccent": "\u1E1F", + "/feharabic": "\u0641", + "/feharmenian": "\u0586", + "/fehfinalarabic": "\uFED2", + "/fehinitialarabic": "\uFED3", + "/fehmedialarabic": "\uFED4", + "/feicoptic": "\u03E5", + "/female": "\u2640", + "/ff": "\uFB00", + "/ffi": "\uFB03", + "/ffl": "\uFB04", + "/fi": "\uFB01", + "/fifteencircle": "\u246E", + "/fifteenparen": "\u2482", + "/fifteenperiod": "\u2496", + "/figuredash": "\u2012", + "/filledbox": "\u25A0", + "/filledrect": "\u25AC", + "/finalkaf": "\u05DA", + "/finalkafdagesh": "\uFB3A", + "/finalkafdageshhebrew": "\uFB3A", + "/finalkafhebrew": "\u05DA", + "/finalkafqamats": "\u05DA\u05B8", + "/finalkafqamatshebrew": "\u05DA\u05B8", + "/finalkafsheva": "\u05DA\u05B0", + "/finalkafshevahebrew": "\u05DA\u05B0", + "/finalmem": "\u05DD", + "/finalmemhebrew": "\u05DD", + "/finalnun": "\u05DF", + "/finalnunhebrew": "\u05DF", + "/finalpe": "\u05E3", + "/finalpehebrew": "\u05E3", + "/finaltsadi": "\u05E5", + "/finaltsadihebrew": "\u05E5", + "/firsttonechinese": "\u02C9", + "/fisheye": "\u25C9", + "/fitacyrillic": "\u0473", + "/five": "\u0035", + "/fivearabic": "\u0665", + "/fivebengali": "\u09EB", + "/fivecircle": "\u2464", + "/fivecircleinversesansserif": "\u278E", + "/fivedeva": "\u096B", + "/fiveeighths": "\u215D", + "/fivegujarati": "\u0AEB", + "/fivegurmukhi": "\u0A6B", + "/fivehackarabic": "\u0665", + "/fivehangzhou": "\u3025", + "/fiveideographicparen": "\u3224", + "/fiveinferior": "\u2085", + "/fivemonospace": "\uFF15", + "/fiveoldstyle": "\uF735", + "/fiveparen": "\u2478", + "/fiveperiod": "\u248C", + "/fivepersian": "\u06F5", + "/fiveroman": "\u2174", + "/fivesuperior": "\u2075", + "/fivethai": "\u0E55", + "/fl": "\uFB02", + "/florin": "\u0192", + "/fmonospace": "\uFF46", + "/fmsquare": "\u3399", + "/fofanthai": "\u0E1F", + "/fofathai": "\u0E1D", + "/fongmanthai": "\u0E4F", + "/forall": "\u2200", + "/four": "\u0034", + "/fourarabic": "\u0664", + "/fourbengali": "\u09EA", + "/fourcircle": "\u2463", + "/fourcircleinversesansserif": "\u278D", + "/fourdeva": "\u096A", + "/fourgujarati": "\u0AEA", + "/fourgurmukhi": "\u0A6A", + "/fourhackarabic": "\u0664", + "/fourhangzhou": "\u3024", + "/fourideographicparen": "\u3223", + "/fourinferior": "\u2084", + "/fourmonospace": "\uFF14", + "/fournumeratorbengali": "\u09F7", + "/fouroldstyle": "\uF734", + "/fourparen": "\u2477", + "/fourperiod": "\u248B", + "/fourpersian": "\u06F4", + "/fourroman": "\u2173", + "/foursuperior": "\u2074", + "/fourteencircle": "\u246D", + "/fourteenparen": "\u2481", + "/fourteenperiod": "\u2495", + "/fourthai": "\u0E54", + "/fourthtonechinese": "\u02CB", + "/fparen": "\u24A1", + "/fraction": "\u2044", + "/franc": "\u20A3", + "/g": "\u0067", + "/gabengali": "\u0997", + "/gacute": "\u01F5", + "/gadeva": "\u0917", + "/gafarabic": "\u06AF", + "/gaffinalarabic": "\uFB93", + "/gafinitialarabic": "\uFB94", + "/gafmedialarabic": "\uFB95", + "/gagujarati": "\u0A97", + "/gagurmukhi": "\u0A17", + "/gahiragana": "\u304C", + "/gakatakana": "\u30AC", + "/gamma": "\u03B3", + "/gammalatinsmall": "\u0263", + "/gammasuperior": "\u02E0", + "/gangiacoptic": "\u03EB", + "/gbopomofo": "\u310D", + "/gbreve": "\u011F", + "/gcaron": "\u01E7", + "/gcedilla": "\u0123", + "/gcircle": "\u24D6", + "/gcircumflex": "\u011D", + "/gcommaaccent": "\u0123", + "/gdot": "\u0121", + "/gdotaccent": "\u0121", + "/gecyrillic": "\u0433", + "/gehiragana": "\u3052", + "/gekatakana": "\u30B2", + "/geometricallyequal": "\u2251", + "/gereshaccenthebrew": "\u059C", + "/gereshhebrew": "\u05F3", + "/gereshmuqdamhebrew": "\u059D", + "/germandbls": "\u00DF", + "/gershayimaccenthebrew": "\u059E", + "/gershayimhebrew": "\u05F4", + "/getamark": "\u3013", + "/ghabengali": "\u0998", + "/ghadarmenian": "\u0572", + "/ghadeva": "\u0918", + "/ghagujarati": "\u0A98", + "/ghagurmukhi": "\u0A18", + "/ghainarabic": "\u063A", + "/ghainfinalarabic": "\uFECE", + "/ghaininitialarabic": "\uFECF", + "/ghainmedialarabic": "\uFED0", + "/ghemiddlehookcyrillic": "\u0495", + "/ghestrokecyrillic": "\u0493", + "/gheupturncyrillic": "\u0491", + "/ghhadeva": "\u095A", + "/ghhagurmukhi": "\u0A5A", + "/ghook": "\u0260", + "/ghzsquare": "\u3393", + "/gihiragana": "\u304E", + "/gikatakana": "\u30AE", + "/gimarmenian": "\u0563", + "/gimel": "\u05D2", + "/gimeldagesh": "\uFB32", + "/gimeldageshhebrew": "\uFB32", + "/gimelhebrew": "\u05D2", + "/gjecyrillic": "\u0453", + "/glottalinvertedstroke": "\u01BE", + "/glottalstop": "\u0294", + "/glottalstopinverted": "\u0296", + "/glottalstopmod": "\u02C0", + "/glottalstopreversed": "\u0295", + "/glottalstopreversedmod": "\u02C1", + "/glottalstopreversedsuperior": "\u02E4", + "/glottalstopstroke": "\u02A1", + "/glottalstopstrokereversed": "\u02A2", + "/gmacron": "\u1E21", + "/gmonospace": "\uFF47", + "/gohiragana": "\u3054", + "/gokatakana": "\u30B4", + "/gparen": "\u24A2", + "/gpasquare": "\u33AC", + "/gradient": "\u2207", + "/grave": "\u0060", + "/gravebelowcmb": "\u0316", + "/gravecmb": "\u0300", + "/gravecomb": "\u0300", + "/gravedeva": "\u0953", + "/gravelowmod": "\u02CE", + "/gravemonospace": "\uFF40", + "/gravetonecmb": "\u0340", + "/greater": "\u003E", + "/greaterequal": "\u2265", + "/greaterequalorless": "\u22DB", + "/greatermonospace": "\uFF1E", + "/greaterorequivalent": "\u2273", + "/greaterorless": "\u2277", + "/greateroverequal": "\u2267", + "/greatersmall": "\uFE65", + "/gscript": "\u0261", + "/gstroke": "\u01E5", + "/guhiragana": "\u3050", + "/guillemotleft": "\u00AB", + "/guillemotright": "\u00BB", + "/guilsinglleft": "\u2039", + "/guilsinglright": "\u203A", + "/gukatakana": "\u30B0", + "/guramusquare": "\u3318", + "/gysquare": "\u33C9", + "/h": "\u0068", + "/haabkhasiancyrillic": "\u04A9", + "/haaltonearabic": "\u06C1", + "/habengali": "\u09B9", + "/hadescendercyrillic": "\u04B3", + "/hadeva": "\u0939", + "/hagujarati": "\u0AB9", + "/hagurmukhi": "\u0A39", + "/haharabic": "\u062D", + "/hahfinalarabic": "\uFEA2", + "/hahinitialarabic": "\uFEA3", + "/hahiragana": "\u306F", + "/hahmedialarabic": "\uFEA4", + "/haitusquare": "\u332A", + "/hakatakana": "\u30CF", + "/hakatakanahalfwidth": "\uFF8A", + "/halantgurmukhi": "\u0A4D", + "/hamzaarabic": "\u0621", + "/hamzadammaarabic": "\u0621\u064F", + "/hamzadammatanarabic": "\u0621\u064C", + "/hamzafathaarabic": "\u0621\u064E", + "/hamzafathatanarabic": "\u0621\u064B", + "/hamzalowarabic": "\u0621", + "/hamzalowkasraarabic": "\u0621\u0650", + "/hamzalowkasratanarabic": "\u0621\u064D", + "/hamzasukunarabic": "\u0621\u0652", + "/hangulfiller": "\u3164", + "/hardsigncyrillic": "\u044A", + "/harpoonleftbarbup": "\u21BC", + "/harpoonrightbarbup": "\u21C0", + "/hasquare": "\u33CA", + "/hatafpatah": "\u05B2", + "/hatafpatah16": "\u05B2", + "/hatafpatah23": "\u05B2", + "/hatafpatah2f": "\u05B2", + "/hatafpatahhebrew": "\u05B2", + "/hatafpatahnarrowhebrew": "\u05B2", + "/hatafpatahquarterhebrew": "\u05B2", + "/hatafpatahwidehebrew": "\u05B2", + "/hatafqamats": "\u05B3", + "/hatafqamats1b": "\u05B3", + "/hatafqamats28": "\u05B3", + "/hatafqamats34": "\u05B3", + "/hatafqamatshebrew": "\u05B3", + "/hatafqamatsnarrowhebrew": "\u05B3", + "/hatafqamatsquarterhebrew": "\u05B3", + "/hatafqamatswidehebrew": "\u05B3", + "/hatafsegol": "\u05B1", + "/hatafsegol17": "\u05B1", + "/hatafsegol24": "\u05B1", + "/hatafsegol30": "\u05B1", + "/hatafsegolhebrew": "\u05B1", + "/hatafsegolnarrowhebrew": "\u05B1", + "/hatafsegolquarterhebrew": "\u05B1", + "/hatafsegolwidehebrew": "\u05B1", + "/hbar": "\u0127", + "/hbopomofo": "\u310F", + "/hbrevebelow": "\u1E2B", + "/hcedilla": "\u1E29", + "/hcircle": "\u24D7", + "/hcircumflex": "\u0125", + "/hdieresis": "\u1E27", + "/hdotaccent": "\u1E23", + "/hdotbelow": "\u1E25", + "/he": "\u05D4", + "/heart": "\u2665", + "/heartsuitblack": "\u2665", + "/heartsuitwhite": "\u2661", + "/hedagesh": "\uFB34", + "/hedageshhebrew": "\uFB34", + "/hehaltonearabic": "\u06C1", + "/heharabic": "\u0647", + "/hehebrew": "\u05D4", + "/hehfinalaltonearabic": "\uFBA7", + "/hehfinalalttwoarabic": "\uFEEA", + "/hehfinalarabic": "\uFEEA", + "/hehhamzaabovefinalarabic": "\uFBA5", + "/hehhamzaaboveisolatedarabic": "\uFBA4", + "/hehinitialaltonearabic": "\uFBA8", + "/hehinitialarabic": "\uFEEB", + "/hehiragana": "\u3078", + "/hehmedialaltonearabic": "\uFBA9", + "/hehmedialarabic": "\uFEEC", + "/heiseierasquare": "\u337B", + "/hekatakana": "\u30D8", + "/hekatakanahalfwidth": "\uFF8D", + "/hekutaarusquare": "\u3336", + "/henghook": "\u0267", + "/herutusquare": "\u3339", + "/het": "\u05D7", + "/hethebrew": "\u05D7", + "/hhook": "\u0266", + "/hhooksuperior": "\u02B1", + "/hieuhacirclekorean": "\u327B", + "/hieuhaparenkorean": "\u321B", + "/hieuhcirclekorean": "\u326D", + "/hieuhkorean": "\u314E", + "/hieuhparenkorean": "\u320D", + "/hihiragana": "\u3072", + "/hikatakana": "\u30D2", + "/hikatakanahalfwidth": "\uFF8B", + "/hiriq": "\u05B4", + "/hiriq14": "\u05B4", + "/hiriq21": "\u05B4", + "/hiriq2d": "\u05B4", + "/hiriqhebrew": "\u05B4", + "/hiriqnarrowhebrew": "\u05B4", + "/hiriqquarterhebrew": "\u05B4", + "/hiriqwidehebrew": "\u05B4", + "/hlinebelow": "\u1E96", + "/hmonospace": "\uFF48", + "/hoarmenian": "\u0570", + "/hohipthai": "\u0E2B", + "/hohiragana": "\u307B", + "/hokatakana": "\u30DB", + "/hokatakanahalfwidth": "\uFF8E", + "/holam": "\u05B9", + "/holam19": "\u05B9", + "/holam26": "\u05B9", + "/holam32": "\u05B9", + "/holamhebrew": "\u05B9", + "/holamnarrowhebrew": "\u05B9", + "/holamquarterhebrew": "\u05B9", + "/holamwidehebrew": "\u05B9", + "/honokhukthai": "\u0E2E", + "/hookabovecomb": "\u0309", + "/hookcmb": "\u0309", + "/hookpalatalizedbelowcmb": "\u0321", + "/hookretroflexbelowcmb": "\u0322", + "/hoonsquare": "\u3342", + "/horicoptic": "\u03E9", + "/horizontalbar": "\u2015", + "/horncmb": "\u031B", + "/hotsprings": "\u2668", + "/house": "\u2302", + "/hparen": "\u24A3", + "/hsuperior": "\u02B0", + "/hturned": "\u0265", + "/huhiragana": "\u3075", + "/huiitosquare": "\u3333", + "/hukatakana": "\u30D5", + "/hukatakanahalfwidth": "\uFF8C", + "/hungarumlaut": "\u02DD", + "/hungarumlautcmb": "\u030B", + "/hv": "\u0195", + "/hyphen": "\u002D", + "/hypheninferior": "\uF6E5", + "/hyphenmonospace": "\uFF0D", + "/hyphensmall": "\uFE63", + "/hyphensuperior": "\uF6E6", + "/hyphentwo": "\u2010", + "/i": "\u0069", + "/iacute": "\u00ED", + "/iacyrillic": "\u044F", + "/ibengali": "\u0987", + "/ibopomofo": "\u3127", + "/ibreve": "\u012D", + "/icaron": "\u01D0", + "/icircle": "\u24D8", + "/icircumflex": "\u00EE", + "/icyrillic": "\u0456", + "/idblgrave": "\u0209", + "/ideographearthcircle": "\u328F", + "/ideographfirecircle": "\u328B", + "/ideographicallianceparen": "\u323F", + "/ideographiccallparen": "\u323A", + "/ideographiccentrecircle": "\u32A5", + "/ideographicclose": "\u3006", + "/ideographiccomma": "\u3001", + "/ideographiccommaleft": "\uFF64", + "/ideographiccongratulationparen": "\u3237", + "/ideographiccorrectcircle": "\u32A3", + "/ideographicearthparen": "\u322F", + "/ideographicenterpriseparen": "\u323D", + "/ideographicexcellentcircle": "\u329D", + "/ideographicfestivalparen": "\u3240", + "/ideographicfinancialcircle": "\u3296", + "/ideographicfinancialparen": "\u3236", + "/ideographicfireparen": "\u322B", + "/ideographichaveparen": "\u3232", + "/ideographichighcircle": "\u32A4", + "/ideographiciterationmark": "\u3005", + "/ideographiclaborcircle": "\u3298", + "/ideographiclaborparen": "\u3238", + "/ideographicleftcircle": "\u32A7", + "/ideographiclowcircle": "\u32A6", + "/ideographicmedicinecircle": "\u32A9", + "/ideographicmetalparen": "\u322E", + "/ideographicmoonparen": "\u322A", + "/ideographicnameparen": "\u3234", + "/ideographicperiod": "\u3002", + "/ideographicprintcircle": "\u329E", + "/ideographicreachparen": "\u3243", + "/ideographicrepresentparen": "\u3239", + "/ideographicresourceparen": "\u323E", + "/ideographicrightcircle": "\u32A8", + "/ideographicsecretcircle": "\u3299", + "/ideographicselfparen": "\u3242", + "/ideographicsocietyparen": "\u3233", + "/ideographicspace": "\u3000", + "/ideographicspecialparen": "\u3235", + "/ideographicstockparen": "\u3231", + "/ideographicstudyparen": "\u323B", + "/ideographicsunparen": "\u3230", + "/ideographicsuperviseparen": "\u323C", + "/ideographicwaterparen": "\u322C", + "/ideographicwoodparen": "\u322D", + "/ideographiczero": "\u3007", + "/ideographmetalcircle": "\u328E", + "/ideographmooncircle": "\u328A", + "/ideographnamecircle": "\u3294", + "/ideographsuncircle": "\u3290", + "/ideographwatercircle": "\u328C", + "/ideographwoodcircle": "\u328D", + "/ideva": "\u0907", + "/idieresis": "\u00EF", + "/idieresisacute": "\u1E2F", + "/idieresiscyrillic": "\u04E5", + "/idotbelow": "\u1ECB", + "/iebrevecyrillic": "\u04D7", + "/iecyrillic": "\u0435", + "/ieungacirclekorean": "\u3275", + "/ieungaparenkorean": "\u3215", + "/ieungcirclekorean": "\u3267", + "/ieungkorean": "\u3147", + "/ieungparenkorean": "\u3207", + "/igrave": "\u00EC", + "/igujarati": "\u0A87", + "/igurmukhi": "\u0A07", + "/ihiragana": "\u3044", + "/ihookabove": "\u1EC9", + "/iibengali": "\u0988", + "/iicyrillic": "\u0438", + "/iideva": "\u0908", + "/iigujarati": "\u0A88", + "/iigurmukhi": "\u0A08", + "/iimatragurmukhi": "\u0A40", + "/iinvertedbreve": "\u020B", + "/iishortcyrillic": "\u0439", + "/iivowelsignbengali": "\u09C0", + "/iivowelsigndeva": "\u0940", + "/iivowelsigngujarati": "\u0AC0", + "/ij": "\u0133", + "/ikatakana": "\u30A4", + "/ikatakanahalfwidth": "\uFF72", + "/ikorean": "\u3163", + "/ilde": "\u02DC", + "/iluyhebrew": "\u05AC", + "/imacron": "\u012B", + "/imacroncyrillic": "\u04E3", + "/imageorapproximatelyequal": "\u2253", + "/imatragurmukhi": "\u0A3F", + "/imonospace": "\uFF49", + "/increment": "\u2206", + "/infinity": "\u221E", + "/iniarmenian": "\u056B", + "/integral": "\u222B", + "/integralbottom": "\u2321", + "/integralbt": "\u2321", + "/integralex": "\uF8F5", + "/integraltop": "\u2320", + "/integraltp": "\u2320", + "/intersection": "\u2229", + "/intisquare": "\u3305", + "/invbullet": "\u25D8", + "/invcircle": "\u25D9", + "/invsmileface": "\u263B", + "/iocyrillic": "\u0451", + "/iogonek": "\u012F", + "/iota": "\u03B9", + "/iotadieresis": "\u03CA", + "/iotadieresistonos": "\u0390", + "/iotalatin": "\u0269", + "/iotatonos": "\u03AF", + "/iparen": "\u24A4", + "/irigurmukhi": "\u0A72", + "/ismallhiragana": "\u3043", + "/ismallkatakana": "\u30A3", + "/ismallkatakanahalfwidth": "\uFF68", + "/issharbengali": "\u09FA", + "/istroke": "\u0268", + "/isuperior": "\uF6ED", + "/iterationhiragana": "\u309D", + "/iterationkatakana": "\u30FD", + "/itilde": "\u0129", + "/itildebelow": "\u1E2D", + "/iubopomofo": "\u3129", + "/iucyrillic": "\u044E", + "/ivowelsignbengali": "\u09BF", + "/ivowelsigndeva": "\u093F", + "/ivowelsigngujarati": "\u0ABF", + "/izhitsacyrillic": "\u0475", + "/izhitsadblgravecyrillic": "\u0477", + "/j": "\u006A", + "/jaarmenian": "\u0571", + "/jabengali": "\u099C", + "/jadeva": "\u091C", + "/jagujarati": "\u0A9C", + "/jagurmukhi": "\u0A1C", + "/jbopomofo": "\u3110", + "/jcaron": "\u01F0", + "/jcircle": "\u24D9", + "/jcircumflex": "\u0135", + "/jcrossedtail": "\u029D", + "/jdotlessstroke": "\u025F", + "/jecyrillic": "\u0458", + "/jeemarabic": "\u062C", + "/jeemfinalarabic": "\uFE9E", + "/jeeminitialarabic": "\uFE9F", + "/jeemmedialarabic": "\uFEA0", + "/jeharabic": "\u0698", + "/jehfinalarabic": "\uFB8B", + "/jhabengali": "\u099D", + "/jhadeva": "\u091D", + "/jhagujarati": "\u0A9D", + "/jhagurmukhi": "\u0A1D", + "/jheharmenian": "\u057B", + "/jis": "\u3004", + "/jmonospace": "\uFF4A", + "/jparen": "\u24A5", + "/jsuperior": "\u02B2", + "/k": "\u006B", + "/kabashkircyrillic": "\u04A1", + "/kabengali": "\u0995", + "/kacute": "\u1E31", + "/kacyrillic": "\u043A", + "/kadescendercyrillic": "\u049B", + "/kadeva": "\u0915", + "/kaf": "\u05DB", + "/kafarabic": "\u0643", + "/kafdagesh": "\uFB3B", + "/kafdageshhebrew": "\uFB3B", + "/kaffinalarabic": "\uFEDA", + "/kafhebrew": "\u05DB", + "/kafinitialarabic": "\uFEDB", + "/kafmedialarabic": "\uFEDC", + "/kafrafehebrew": "\uFB4D", + "/kagujarati": "\u0A95", + "/kagurmukhi": "\u0A15", + "/kahiragana": "\u304B", + "/kahookcyrillic": "\u04C4", + "/kakatakana": "\u30AB", + "/kakatakanahalfwidth": "\uFF76", + "/kappa": "\u03BA", + "/kappasymbolgreek": "\u03F0", + "/kapyeounmieumkorean": "\u3171", + "/kapyeounphieuphkorean": "\u3184", + "/kapyeounpieupkorean": "\u3178", + "/kapyeounssangpieupkorean": "\u3179", + "/karoriisquare": "\u330D", + "/kashidaautoarabic": "\u0640", + "/kashidaautonosidebearingarabic": "\u0640", + "/kasmallkatakana": "\u30F5", + "/kasquare": "\u3384", + "/kasraarabic": "\u0650", + "/kasratanarabic": "\u064D", + "/kastrokecyrillic": "\u049F", + "/katahiraprolongmarkhalfwidth": "\uFF70", + "/kaverticalstrokecyrillic": "\u049D", + "/kbopomofo": "\u310E", + "/kcalsquare": "\u3389", + "/kcaron": "\u01E9", + "/kcedilla": "\u0137", + "/kcircle": "\u24DA", + "/kcommaaccent": "\u0137", + "/kdotbelow": "\u1E33", + "/keharmenian": "\u0584", + "/kehiragana": "\u3051", + "/kekatakana": "\u30B1", + "/kekatakanahalfwidth": "\uFF79", + "/kenarmenian": "\u056F", + "/kesmallkatakana": "\u30F6", + "/kgreenlandic": "\u0138", + "/khabengali": "\u0996", + "/khacyrillic": "\u0445", + "/khadeva": "\u0916", + "/khagujarati": "\u0A96", + "/khagurmukhi": "\u0A16", + "/khaharabic": "\u062E", + "/khahfinalarabic": "\uFEA6", + "/khahinitialarabic": "\uFEA7", + "/khahmedialarabic": "\uFEA8", + "/kheicoptic": "\u03E7", + "/khhadeva": "\u0959", + "/khhagurmukhi": "\u0A59", + "/khieukhacirclekorean": "\u3278", + "/khieukhaparenkorean": "\u3218", + "/khieukhcirclekorean": "\u326A", + "/khieukhkorean": "\u314B", + "/khieukhparenkorean": "\u320A", + "/khokhaithai": "\u0E02", + "/khokhonthai": "\u0E05", + "/khokhuatthai": "\u0E03", + "/khokhwaithai": "\u0E04", + "/khomutthai": "\u0E5B", + "/khook": "\u0199", + "/khorakhangthai": "\u0E06", + "/khzsquare": "\u3391", + "/kihiragana": "\u304D", + "/kikatakana": "\u30AD", + "/kikatakanahalfwidth": "\uFF77", + "/kiroguramusquare": "\u3315", + "/kiromeetorusquare": "\u3316", + "/kirosquare": "\u3314", + "/kiyeokacirclekorean": "\u326E", + "/kiyeokaparenkorean": "\u320E", + "/kiyeokcirclekorean": "\u3260", + "/kiyeokkorean": "\u3131", + "/kiyeokparenkorean": "\u3200", + "/kiyeoksioskorean": "\u3133", + "/kjecyrillic": "\u045C", + "/klinebelow": "\u1E35", + "/klsquare": "\u3398", + "/kmcubedsquare": "\u33A6", + "/kmonospace": "\uFF4B", + "/kmsquaredsquare": "\u33A2", + "/kohiragana": "\u3053", + "/kohmsquare": "\u33C0", + "/kokaithai": "\u0E01", + "/kokatakana": "\u30B3", + "/kokatakanahalfwidth": "\uFF7A", + "/kooposquare": "\u331E", + "/koppacyrillic": "\u0481", + "/koreanstandardsymbol": "\u327F", + "/koroniscmb": "\u0343", + "/kparen": "\u24A6", + "/kpasquare": "\u33AA", + "/ksicyrillic": "\u046F", + "/ktsquare": "\u33CF", + "/kturned": "\u029E", + "/kuhiragana": "\u304F", + "/kukatakana": "\u30AF", + "/kukatakanahalfwidth": "\uFF78", + "/kvsquare": "\u33B8", + "/kwsquare": "\u33BE", + "/l": "\u006C", + "/labengali": "\u09B2", + "/lacute": "\u013A", + "/ladeva": "\u0932", + "/lagujarati": "\u0AB2", + "/lagurmukhi": "\u0A32", + "/lakkhangyaothai": "\u0E45", + "/lamaleffinalarabic": "\uFEFC", + "/lamalefhamzaabovefinalarabic": "\uFEF8", + "/lamalefhamzaaboveisolatedarabic": "\uFEF7", + "/lamalefhamzabelowfinalarabic": "\uFEFA", + "/lamalefhamzabelowisolatedarabic": "\uFEF9", + "/lamalefisolatedarabic": "\uFEFB", + "/lamalefmaddaabovefinalarabic": "\uFEF6", + "/lamalefmaddaaboveisolatedarabic": "\uFEF5", + "/lamarabic": "\u0644", + "/lambda": "\u03BB", + "/lambdastroke": "\u019B", + "/lamed": "\u05DC", + "/lameddagesh": "\uFB3C", + "/lameddageshhebrew": "\uFB3C", + "/lamedhebrew": "\u05DC", + "/lamedholam": "\u05DC\u05B9", + "/lamedholamdagesh": "\u05DC\u05B9\u05BC", + "/lamedholamdageshhebrew": "\u05DC\u05B9\u05BC", + "/lamedholamhebrew": "\u05DC\u05B9", + "/lamfinalarabic": "\uFEDE", + "/lamhahinitialarabic": "\uFCCA", + "/laminitialarabic": "\uFEDF", + "/lamjeeminitialarabic": "\uFCC9", + "/lamkhahinitialarabic": "\uFCCB", + "/lamlamhehisolatedarabic": "\uFDF2", + "/lammedialarabic": "\uFEE0", + "/lammeemhahinitialarabic": "\uFD88", + "/lammeeminitialarabic": "\uFCCC", + "/lammeemjeeminitialarabic": "\uFEDF\uFEE4\uFEA0", + "/lammeemkhahinitialarabic": "\uFEDF\uFEE4\uFEA8", + "/largecircle": "\u25EF", + "/lbar": "\u019A", + "/lbelt": "\u026C", + "/lbopomofo": "\u310C", + "/lcaron": "\u013E", + "/lcedilla": "\u013C", + "/lcircle": "\u24DB", + "/lcircumflexbelow": "\u1E3D", + "/lcommaaccent": "\u013C", + "/ldot": "\u0140", + "/ldotaccent": "\u0140", + "/ldotbelow": "\u1E37", + "/ldotbelowmacron": "\u1E39", + "/leftangleabovecmb": "\u031A", + "/lefttackbelowcmb": "\u0318", + "/less": "\u003C", + "/lessequal": "\u2264", + "/lessequalorgreater": "\u22DA", + "/lessmonospace": "\uFF1C", + "/lessorequivalent": "\u2272", + "/lessorgreater": "\u2276", + "/lessoverequal": "\u2266", + "/lesssmall": "\uFE64", + "/lezh": "\u026E", + "/lfblock": "\u258C", + "/lhookretroflex": "\u026D", + "/lira": "\u20A4", + "/liwnarmenian": "\u056C", + "/lj": "\u01C9", + "/ljecyrillic": "\u0459", + "/ll": "\uF6C0", + "/lladeva": "\u0933", + "/llagujarati": "\u0AB3", + "/llinebelow": "\u1E3B", + "/llladeva": "\u0934", + "/llvocalicbengali": "\u09E1", + "/llvocalicdeva": "\u0961", + "/llvocalicvowelsignbengali": "\u09E3", + "/llvocalicvowelsigndeva": "\u0963", + "/lmiddletilde": "\u026B", + "/lmonospace": "\uFF4C", + "/lmsquare": "\u33D0", + "/lochulathai": "\u0E2C", + "/logicaland": "\u2227", + "/logicalnot": "\u00AC", + "/logicalnotreversed": "\u2310", + "/logicalor": "\u2228", + "/lolingthai": "\u0E25", + "/longs": "\u017F", + "/lowlinecenterline": "\uFE4E", + "/lowlinecmb": "\u0332", + "/lowlinedashed": "\uFE4D", + "/lozenge": "\u25CA", + "/lparen": "\u24A7", + "/lslash": "\u0142", + "/lsquare": "\u2113", + "/lsuperior": "\uF6EE", + "/ltshade": "\u2591", + "/luthai": "\u0E26", + "/lvocalicbengali": "\u098C", + "/lvocalicdeva": "\u090C", + "/lvocalicvowelsignbengali": "\u09E2", + "/lvocalicvowelsigndeva": "\u0962", + "/lxsquare": "\u33D3", + "/m": "\u006D", + "/mabengali": "\u09AE", + "/macron": "\u00AF", + "/macronbelowcmb": "\u0331", + "/macroncmb": "\u0304", + "/macronlowmod": "\u02CD", + "/macronmonospace": "\uFFE3", + "/macute": "\u1E3F", + "/madeva": "\u092E", + "/magujarati": "\u0AAE", + "/magurmukhi": "\u0A2E", + "/mahapakhhebrew": "\u05A4", + "/mahapakhlefthebrew": "\u05A4", + "/mahiragana": "\u307E", + "/maichattawalowleftthai": "\uF895", + "/maichattawalowrightthai": "\uF894", + "/maichattawathai": "\u0E4B", + "/maichattawaupperleftthai": "\uF893", + "/maieklowleftthai": "\uF88C", + "/maieklowrightthai": "\uF88B", + "/maiekthai": "\u0E48", + "/maiekupperleftthai": "\uF88A", + "/maihanakatleftthai": "\uF884", + "/maihanakatthai": "\u0E31", + "/maitaikhuleftthai": "\uF889", + "/maitaikhuthai": "\u0E47", + "/maitholowleftthai": "\uF88F", + "/maitholowrightthai": "\uF88E", + "/maithothai": "\u0E49", + "/maithoupperleftthai": "\uF88D", + "/maitrilowleftthai": "\uF892", + "/maitrilowrightthai": "\uF891", + "/maitrithai": "\u0E4A", + "/maitriupperleftthai": "\uF890", + "/maiyamokthai": "\u0E46", + "/makatakana": "\u30DE", + "/makatakanahalfwidth": "\uFF8F", + "/male": "\u2642", + "/mansyonsquare": "\u3347", + "/maqafhebrew": "\u05BE", + "/mars": "\u2642", + "/masoracirclehebrew": "\u05AF", + "/masquare": "\u3383", + "/mbopomofo": "\u3107", + "/mbsquare": "\u33D4", + "/mcircle": "\u24DC", + "/mcubedsquare": "\u33A5", + "/mdotaccent": "\u1E41", + "/mdotbelow": "\u1E43", + "/meemarabic": "\u0645", + "/meemfinalarabic": "\uFEE2", + "/meeminitialarabic": "\uFEE3", + "/meemmedialarabic": "\uFEE4", + "/meemmeeminitialarabic": "\uFCD1", + "/meemmeemisolatedarabic": "\uFC48", + "/meetorusquare": "\u334D", + "/mehiragana": "\u3081", + "/meizierasquare": "\u337E", + "/mekatakana": "\u30E1", + "/mekatakanahalfwidth": "\uFF92", + "/mem": "\u05DE", + "/memdagesh": "\uFB3E", + "/memdageshhebrew": "\uFB3E", + "/memhebrew": "\u05DE", + "/menarmenian": "\u0574", + "/merkhahebrew": "\u05A5", + "/merkhakefulahebrew": "\u05A6", + "/merkhakefulalefthebrew": "\u05A6", + "/merkhalefthebrew": "\u05A5", + "/mhook": "\u0271", + "/mhzsquare": "\u3392", + "/middledotkatakanahalfwidth": "\uFF65", + "/middot": "\u00B7", + "/mieumacirclekorean": "\u3272", + "/mieumaparenkorean": "\u3212", + "/mieumcirclekorean": "\u3264", + "/mieumkorean": "\u3141", + "/mieumpansioskorean": "\u3170", + "/mieumparenkorean": "\u3204", + "/mieumpieupkorean": "\u316E", + "/mieumsioskorean": "\u316F", + "/mihiragana": "\u307F", + "/mikatakana": "\u30DF", + "/mikatakanahalfwidth": "\uFF90", + "/minus": "\u2212", + "/minusbelowcmb": "\u0320", + "/minuscircle": "\u2296", + "/minusmod": "\u02D7", + "/minusplus": "\u2213", + "/minute": "\u2032", + "/miribaarusquare": "\u334A", + "/mirisquare": "\u3349", + "/mlonglegturned": "\u0270", + "/mlsquare": "\u3396", + "/mmcubedsquare": "\u33A3", + "/mmonospace": "\uFF4D", + "/mmsquaredsquare": "\u339F", + "/mohiragana": "\u3082", + "/mohmsquare": "\u33C1", + "/mokatakana": "\u30E2", + "/mokatakanahalfwidth": "\uFF93", + "/molsquare": "\u33D6", + "/momathai": "\u0E21", + "/moverssquare": "\u33A7", + "/moverssquaredsquare": "\u33A8", + "/mparen": "\u24A8", + "/mpasquare": "\u33AB", + "/mssquare": "\u33B3", + "/msuperior": "\uF6EF", + "/mturned": "\u026F", + "/mu": "\u00B5", + "/mu1": "\u00B5", + "/muasquare": "\u3382", + "/muchgreater": "\u226B", + "/muchless": "\u226A", + "/mufsquare": "\u338C", + "/mugreek": "\u03BC", + "/mugsquare": "\u338D", + "/muhiragana": "\u3080", + "/mukatakana": "\u30E0", + "/mukatakanahalfwidth": "\uFF91", + "/mulsquare": "\u3395", + "/multiply": "\u00D7", + "/mumsquare": "\u339B", + "/munahhebrew": "\u05A3", + "/munahlefthebrew": "\u05A3", + "/musicalnote": "\u266A", + "/musicalnotedbl": "\u266B", + "/musicflatsign": "\u266D", + "/musicsharpsign": "\u266F", + "/mussquare": "\u33B2", + "/muvsquare": "\u33B6", + "/muwsquare": "\u33BC", + "/mvmegasquare": "\u33B9", + "/mvsquare": "\u33B7", + "/mwmegasquare": "\u33BF", + "/mwsquare": "\u33BD", + "/n": "\u006E", + "/nabengali": "\u09A8", + "/nabla": "\u2207", + "/nacute": "\u0144", + "/nadeva": "\u0928", + "/nagujarati": "\u0AA8", + "/nagurmukhi": "\u0A28", + "/nahiragana": "\u306A", + "/nakatakana": "\u30CA", + "/nakatakanahalfwidth": "\uFF85", + "/napostrophe": "\u0149", + "/nasquare": "\u3381", + "/nbopomofo": "\u310B", + "/nbspace": "\u00A0", + "/ncaron": "\u0148", + "/ncedilla": "\u0146", + "/ncircle": "\u24DD", + "/ncircumflexbelow": "\u1E4B", + "/ncommaaccent": "\u0146", + "/ndotaccent": "\u1E45", + "/ndotbelow": "\u1E47", + "/nehiragana": "\u306D", + "/nekatakana": "\u30CD", + "/nekatakanahalfwidth": "\uFF88", + "/newsheqelsign": "\u20AA", + "/nfsquare": "\u338B", + "/ngabengali": "\u0999", + "/ngadeva": "\u0919", + "/ngagujarati": "\u0A99", + "/ngagurmukhi": "\u0A19", + "/ngonguthai": "\u0E07", + "/nhiragana": "\u3093", + "/nhookleft": "\u0272", + "/nhookretroflex": "\u0273", + "/nieunacirclekorean": "\u326F", + "/nieunaparenkorean": "\u320F", + "/nieuncieuckorean": "\u3135", + "/nieuncirclekorean": "\u3261", + "/nieunhieuhkorean": "\u3136", + "/nieunkorean": "\u3134", + "/nieunpansioskorean": "\u3168", + "/nieunparenkorean": "\u3201", + "/nieunsioskorean": "\u3167", + "/nieuntikeutkorean": "\u3166", + "/nihiragana": "\u306B", + "/nikatakana": "\u30CB", + "/nikatakanahalfwidth": "\uFF86", + "/nikhahitleftthai": "\uF899", + "/nikhahitthai": "\u0E4D", + "/nine": "\u0039", + "/ninearabic": "\u0669", + "/ninebengali": "\u09EF", + "/ninecircle": "\u2468", + "/ninecircleinversesansserif": "\u2792", + "/ninedeva": "\u096F", + "/ninegujarati": "\u0AEF", + "/ninegurmukhi": "\u0A6F", + "/ninehackarabic": "\u0669", + "/ninehangzhou": "\u3029", + "/nineideographicparen": "\u3228", + "/nineinferior": "\u2089", + "/ninemonospace": "\uFF19", + "/nineoldstyle": "\uF739", + "/nineparen": "\u247C", + "/nineperiod": "\u2490", + "/ninepersian": "\u06F9", + "/nineroman": "\u2178", + "/ninesuperior": "\u2079", + "/nineteencircle": "\u2472", + "/nineteenparen": "\u2486", + "/nineteenperiod": "\u249A", + "/ninethai": "\u0E59", + "/nj": "\u01CC", + "/njecyrillic": "\u045A", + "/nkatakana": "\u30F3", + "/nkatakanahalfwidth": "\uFF9D", + "/nlegrightlong": "\u019E", + "/nlinebelow": "\u1E49", + "/nmonospace": "\uFF4E", + "/nmsquare": "\u339A", + "/nnabengali": "\u09A3", + "/nnadeva": "\u0923", + "/nnagujarati": "\u0AA3", + "/nnagurmukhi": "\u0A23", + "/nnnadeva": "\u0929", + "/nohiragana": "\u306E", + "/nokatakana": "\u30CE", + "/nokatakanahalfwidth": "\uFF89", + "/nonbreakingspace": "\u00A0", + "/nonenthai": "\u0E13", + "/nonuthai": "\u0E19", + "/noonarabic": "\u0646", + "/noonfinalarabic": "\uFEE6", + "/noonghunnaarabic": "\u06BA", + "/noonghunnafinalarabic": "\uFB9F", + "/noonhehinitialarabic": "\uFEE7\uFEEC", + "/nooninitialarabic": "\uFEE7", + "/noonjeeminitialarabic": "\uFCD2", + "/noonjeemisolatedarabic": "\uFC4B", + "/noonmedialarabic": "\uFEE8", + "/noonmeeminitialarabic": "\uFCD5", + "/noonmeemisolatedarabic": "\uFC4E", + "/noonnoonfinalarabic": "\uFC8D", + "/notcontains": "\u220C", + "/notelement": "\u2209", + "/notelementof": "\u2209", + "/notequal": "\u2260", + "/notgreater": "\u226F", + "/notgreaternorequal": "\u2271", + "/notgreaternorless": "\u2279", + "/notidentical": "\u2262", + "/notless": "\u226E", + "/notlessnorequal": "\u2270", + "/notparallel": "\u2226", + "/notprecedes": "\u2280", + "/notsubset": "\u2284", + "/notsucceeds": "\u2281", + "/notsuperset": "\u2285", + "/nowarmenian": "\u0576", + "/nparen": "\u24A9", + "/nssquare": "\u33B1", + "/nsuperior": "\u207F", + "/ntilde": "\u00F1", + "/nu": "\u03BD", + "/nuhiragana": "\u306C", + "/nukatakana": "\u30CC", + "/nukatakanahalfwidth": "\uFF87", + "/nuktabengali": "\u09BC", + "/nuktadeva": "\u093C", + "/nuktagujarati": "\u0ABC", + "/nuktagurmukhi": "\u0A3C", + "/numbersign": "\u0023", + "/numbersignmonospace": "\uFF03", + "/numbersignsmall": "\uFE5F", + "/numeralsigngreek": "\u0374", + "/numeralsignlowergreek": "\u0375", + "/numero": "\u2116", + "/nun": "\u05E0", + "/nundagesh": "\uFB40", + "/nundageshhebrew": "\uFB40", + "/nunhebrew": "\u05E0", + "/nvsquare": "\u33B5", + "/nwsquare": "\u33BB", + "/nyabengali": "\u099E", + "/nyadeva": "\u091E", + "/nyagujarati": "\u0A9E", + "/nyagurmukhi": "\u0A1E", + "/o": "\u006F", + "/oacute": "\u00F3", + "/oangthai": "\u0E2D", + "/obarred": "\u0275", + "/obarredcyrillic": "\u04E9", + "/obarreddieresiscyrillic": "\u04EB", + "/obengali": "\u0993", + "/obopomofo": "\u311B", + "/obreve": "\u014F", + "/ocandradeva": "\u0911", + "/ocandragujarati": "\u0A91", + "/ocandravowelsigndeva": "\u0949", + "/ocandravowelsigngujarati": "\u0AC9", + "/ocaron": "\u01D2", + "/ocircle": "\u24DE", + "/ocircumflex": "\u00F4", + "/ocircumflexacute": "\u1ED1", + "/ocircumflexdotbelow": "\u1ED9", + "/ocircumflexgrave": "\u1ED3", + "/ocircumflexhookabove": "\u1ED5", + "/ocircumflextilde": "\u1ED7", + "/ocyrillic": "\u043E", + "/odblacute": "\u0151", + "/odblgrave": "\u020D", + "/odeva": "\u0913", + "/odieresis": "\u00F6", + "/odieresiscyrillic": "\u04E7", + "/odotbelow": "\u1ECD", + "/oe": "\u0153", + "/oekorean": "\u315A", + "/ogonek": "\u02DB", + "/ogonekcmb": "\u0328", + "/ograve": "\u00F2", + "/ogujarati": "\u0A93", + "/oharmenian": "\u0585", + "/ohiragana": "\u304A", + "/ohookabove": "\u1ECF", + "/ohorn": "\u01A1", + "/ohornacute": "\u1EDB", + "/ohorndotbelow": "\u1EE3", + "/ohorngrave": "\u1EDD", + "/ohornhookabove": "\u1EDF", + "/ohorntilde": "\u1EE1", + "/ohungarumlaut": "\u0151", + "/oi": "\u01A3", + "/oinvertedbreve": "\u020F", + "/okatakana": "\u30AA", + "/okatakanahalfwidth": "\uFF75", + "/okorean": "\u3157", + "/olehebrew": "\u05AB", + "/omacron": "\u014D", + "/omacronacute": "\u1E53", + "/omacrongrave": "\u1E51", + "/omdeva": "\u0950", + "/omega": "\u03C9", + "/omega1": "\u03D6", + "/omegacyrillic": "\u0461", + "/omegalatinclosed": "\u0277", + "/omegaroundcyrillic": "\u047B", + "/omegatitlocyrillic": "\u047D", + "/omegatonos": "\u03CE", + "/omgujarati": "\u0AD0", + "/omicron": "\u03BF", + "/omicrontonos": "\u03CC", + "/omonospace": "\uFF4F", + "/one": "\u0031", + "/onearabic": "\u0661", + "/onebengali": "\u09E7", + "/onecircle": "\u2460", + "/onecircleinversesansserif": "\u278A", + "/onedeva": "\u0967", + "/onedotenleader": "\u2024", + "/oneeighth": "\u215B", + "/onefitted": "\uF6DC", + "/onegujarati": "\u0AE7", + "/onegurmukhi": "\u0A67", + "/onehackarabic": "\u0661", + "/onehalf": "\u00BD", + "/onehangzhou": "\u3021", + "/oneideographicparen": "\u3220", + "/oneinferior": "\u2081", + "/onemonospace": "\uFF11", + "/onenumeratorbengali": "\u09F4", + "/oneoldstyle": "\uF731", + "/oneparen": "\u2474", + "/oneperiod": "\u2488", + "/onepersian": "\u06F1", + "/onequarter": "\u00BC", + "/oneroman": "\u2170", + "/onesuperior": "\u00B9", + "/onethai": "\u0E51", + "/onethird": "\u2153", + "/oogonek": "\u01EB", + "/oogonekmacron": "\u01ED", + "/oogurmukhi": "\u0A13", + "/oomatragurmukhi": "\u0A4B", + "/oopen": "\u0254", + "/oparen": "\u24AA", + "/openbullet": "\u25E6", + "/option": "\u2325", + "/ordfeminine": "\u00AA", + "/ordmasculine": "\u00BA", + "/orthogonal": "\u221F", + "/oshortdeva": "\u0912", + "/oshortvowelsigndeva": "\u094A", + "/oslash": "\u00F8", + "/oslashacute": "\u01FF", + "/osmallhiragana": "\u3049", + "/osmallkatakana": "\u30A9", + "/osmallkatakanahalfwidth": "\uFF6B", + "/ostrokeacute": "\u01FF", + "/osuperior": "\uF6F0", + "/otcyrillic": "\u047F", + "/otilde": "\u00F5", + "/otildeacute": "\u1E4D", + "/otildedieresis": "\u1E4F", + "/oubopomofo": "\u3121", + "/overline": "\u203E", + "/overlinecenterline": "\uFE4A", + "/overlinecmb": "\u0305", + "/overlinedashed": "\uFE49", + "/overlinedblwavy": "\uFE4C", + "/overlinewavy": "\uFE4B", + "/overscore": "\u00AF", + "/ovowelsignbengali": "\u09CB", + "/ovowelsigndeva": "\u094B", + "/ovowelsigngujarati": "\u0ACB", + "/p": "\u0070", + "/paampssquare": "\u3380", + "/paasentosquare": "\u332B", + "/pabengali": "\u09AA", + "/pacute": "\u1E55", + "/padeva": "\u092A", + "/pagedown": "\u21DF", + "/pageup": "\u21DE", + "/pagujarati": "\u0AAA", + "/pagurmukhi": "\u0A2A", + "/pahiragana": "\u3071", + "/paiyannoithai": "\u0E2F", + "/pakatakana": "\u30D1", + "/palatalizationcyrilliccmb": "\u0484", + "/palochkacyrillic": "\u04C0", + "/pansioskorean": "\u317F", + "/paragraph": "\u00B6", + "/parallel": "\u2225", + "/parenleft": "\u0028", + "/parenleftaltonearabic": "\uFD3E", + "/parenleftbt": "\uF8ED", + "/parenleftex": "\uF8EC", + "/parenleftinferior": "\u208D", + "/parenleftmonospace": "\uFF08", + "/parenleftsmall": "\uFE59", + "/parenleftsuperior": "\u207D", + "/parenlefttp": "\uF8EB", + "/parenleftvertical": "\uFE35", + "/parenright": "\u0029", + "/parenrightaltonearabic": "\uFD3F", + "/parenrightbt": "\uF8F8", + "/parenrightex": "\uF8F7", + "/parenrightinferior": "\u208E", + "/parenrightmonospace": "\uFF09", + "/parenrightsmall": "\uFE5A", + "/parenrightsuperior": "\u207E", + "/parenrighttp": "\uF8F6", + "/parenrightvertical": "\uFE36", + "/partialdiff": "\u2202", + "/paseqhebrew": "\u05C0", + "/pashtahebrew": "\u0599", + "/pasquare": "\u33A9", + "/patah": "\u05B7", + "/patah11": "\u05B7", + "/patah1d": "\u05B7", + "/patah2a": "\u05B7", + "/patahhebrew": "\u05B7", + "/patahnarrowhebrew": "\u05B7", + "/patahquarterhebrew": "\u05B7", + "/patahwidehebrew": "\u05B7", + "/pazerhebrew": "\u05A1", + "/pbopomofo": "\u3106", + "/pcircle": "\u24DF", + "/pdotaccent": "\u1E57", + "/pe": "\u05E4", + "/pecyrillic": "\u043F", + "/pedagesh": "\uFB44", + "/pedageshhebrew": "\uFB44", + "/peezisquare": "\u333B", + "/pefinaldageshhebrew": "\uFB43", + "/peharabic": "\u067E", + "/peharmenian": "\u057A", + "/pehebrew": "\u05E4", + "/pehfinalarabic": "\uFB57", + "/pehinitialarabic": "\uFB58", + "/pehiragana": "\u307A", + "/pehmedialarabic": "\uFB59", + "/pekatakana": "\u30DA", + "/pemiddlehookcyrillic": "\u04A7", + "/perafehebrew": "\uFB4E", + "/percent": "\u0025", + "/percentarabic": "\u066A", + "/percentmonospace": "\uFF05", + "/percentsmall": "\uFE6A", + "/period": "\u002E", + "/periodarmenian": "\u0589", + "/periodcentered": "\u00B7", + "/periodhalfwidth": "\uFF61", + "/periodinferior": "\uF6E7", + "/periodmonospace": "\uFF0E", + "/periodsmall": "\uFE52", + "/periodsuperior": "\uF6E8", + "/perispomenigreekcmb": "\u0342", + "/perpendicular": "\u22A5", + "/perthousand": "\u2030", + "/peseta": "\u20A7", + "/pfsquare": "\u338A", + "/phabengali": "\u09AB", + "/phadeva": "\u092B", + "/phagujarati": "\u0AAB", + "/phagurmukhi": "\u0A2B", + "/phi": "\u03C6", + "/phi1": "\u03D5", + "/phieuphacirclekorean": "\u327A", + "/phieuphaparenkorean": "\u321A", + "/phieuphcirclekorean": "\u326C", + "/phieuphkorean": "\u314D", + "/phieuphparenkorean": "\u320C", + "/philatin": "\u0278", + "/phinthuthai": "\u0E3A", + "/phisymbolgreek": "\u03D5", + "/phook": "\u01A5", + "/phophanthai": "\u0E1E", + "/phophungthai": "\u0E1C", + "/phosamphaothai": "\u0E20", + "/pi": "\u03C0", + "/pieupacirclekorean": "\u3273", + "/pieupaparenkorean": "\u3213", + "/pieupcieuckorean": "\u3176", + "/pieupcirclekorean": "\u3265", + "/pieupkiyeokkorean": "\u3172", + "/pieupkorean": "\u3142", + "/pieupparenkorean": "\u3205", + "/pieupsioskiyeokkorean": "\u3174", + "/pieupsioskorean": "\u3144", + "/pieupsiostikeutkorean": "\u3175", + "/pieupthieuthkorean": "\u3177", + "/pieuptikeutkorean": "\u3173", + "/pihiragana": "\u3074", + "/pikatakana": "\u30D4", + "/pisymbolgreek": "\u03D6", + "/piwrarmenian": "\u0583", + "/plus": "\u002B", + "/plusbelowcmb": "\u031F", + "/pluscircle": "\u2295", + "/plusminus": "\u00B1", + "/plusmod": "\u02D6", + "/plusmonospace": "\uFF0B", + "/plussmall": "\uFE62", + "/plussuperior": "\u207A", + "/pmonospace": "\uFF50", + "/pmsquare": "\u33D8", + "/pohiragana": "\u307D", + "/pointingindexdownwhite": "\u261F", + "/pointingindexleftwhite": "\u261C", + "/pointingindexrightwhite": "\u261E", + "/pointingindexupwhite": "\u261D", + "/pokatakana": "\u30DD", + "/poplathai": "\u0E1B", + "/postalmark": "\u3012", + "/postalmarkface": "\u3020", + "/pparen": "\u24AB", + "/precedes": "\u227A", + "/prescription": "\u211E", + "/primemod": "\u02B9", + "/primereversed": "\u2035", + "/product": "\u220F", + "/projective": "\u2305", + "/prolongedkana": "\u30FC", + "/propellor": "\u2318", + "/propersubset": "\u2282", + "/propersuperset": "\u2283", + "/proportion": "\u2237", + "/proportional": "\u221D", + "/psi": "\u03C8", + "/psicyrillic": "\u0471", + "/psilipneumatacyrilliccmb": "\u0486", + "/pssquare": "\u33B0", + "/puhiragana": "\u3077", + "/pukatakana": "\u30D7", + "/pvsquare": "\u33B4", + "/pwsquare": "\u33BA", + "/q": "\u0071", + "/qadeva": "\u0958", + "/qadmahebrew": "\u05A8", + "/qafarabic": "\u0642", + "/qaffinalarabic": "\uFED6", + "/qafinitialarabic": "\uFED7", + "/qafmedialarabic": "\uFED8", + "/qamats": "\u05B8", + "/qamats10": "\u05B8", + "/qamats1a": "\u05B8", + "/qamats1c": "\u05B8", + "/qamats27": "\u05B8", + "/qamats29": "\u05B8", + "/qamats33": "\u05B8", + "/qamatsde": "\u05B8", + "/qamatshebrew": "\u05B8", + "/qamatsnarrowhebrew": "\u05B8", + "/qamatsqatanhebrew": "\u05B8", + "/qamatsqatannarrowhebrew": "\u05B8", + "/qamatsqatanquarterhebrew": "\u05B8", + "/qamatsqatanwidehebrew": "\u05B8", + "/qamatsquarterhebrew": "\u05B8", + "/qamatswidehebrew": "\u05B8", + "/qarneyparahebrew": "\u059F", + "/qbopomofo": "\u3111", + "/qcircle": "\u24E0", + "/qhook": "\u02A0", + "/qmonospace": "\uFF51", + "/qof": "\u05E7", + "/qofdagesh": "\uFB47", + "/qofdageshhebrew": "\uFB47", + "/qofhatafpatah": "\u05E7\u05B2", + "/qofhatafpatahhebrew": "\u05E7\u05B2", + "/qofhatafsegol": "\u05E7\u05B1", + "/qofhatafsegolhebrew": "\u05E7\u05B1", + "/qofhebrew": "\u05E7", + "/qofhiriq": "\u05E7\u05B4", + "/qofhiriqhebrew": "\u05E7\u05B4", + "/qofholam": "\u05E7\u05B9", + "/qofholamhebrew": "\u05E7\u05B9", + "/qofpatah": "\u05E7\u05B7", + "/qofpatahhebrew": "\u05E7\u05B7", + "/qofqamats": "\u05E7\u05B8", + "/qofqamatshebrew": "\u05E7\u05B8", + "/qofqubuts": "\u05E7\u05BB", + "/qofqubutshebrew": "\u05E7\u05BB", + "/qofsegol": "\u05E7\u05B6", + "/qofsegolhebrew": "\u05E7\u05B6", + "/qofsheva": "\u05E7\u05B0", + "/qofshevahebrew": "\u05E7\u05B0", + "/qoftsere": "\u05E7\u05B5", + "/qoftserehebrew": "\u05E7\u05B5", + "/qparen": "\u24AC", + "/quarternote": "\u2669", + "/qubuts": "\u05BB", + "/qubuts18": "\u05BB", + "/qubuts25": "\u05BB", + "/qubuts31": "\u05BB", + "/qubutshebrew": "\u05BB", + "/qubutsnarrowhebrew": "\u05BB", + "/qubutsquarterhebrew": "\u05BB", + "/qubutswidehebrew": "\u05BB", + "/question": "\u003F", + "/questionarabic": "\u061F", + "/questionarmenian": "\u055E", + "/questiondown": "\u00BF", + "/questiondownsmall": "\uF7BF", + "/questiongreek": "\u037E", + "/questionmonospace": "\uFF1F", + "/questionsmall": "\uF73F", + "/quotedbl": "\u0022", + "/quotedblbase": "\u201E", + "/quotedblleft": "\u201C", + "/quotedblmonospace": "\uFF02", + "/quotedblprime": "\u301E", + "/quotedblprimereversed": "\u301D", + "/quotedblright": "\u201D", + "/quoteleft": "\u2018", + "/quoteleftreversed": "\u201B", + "/quotereversed": "\u201B", + "/quoteright": "\u2019", + "/quoterightn": "\u0149", + "/quotesinglbase": "\u201A", + "/quotesingle": "\u0027", + "/quotesinglemonospace": "\uFF07", + "/r": "\u0072", + "/raarmenian": "\u057C", + "/rabengali": "\u09B0", + "/racute": "\u0155", + "/radeva": "\u0930", + "/radical": "\u221A", + "/radicalex": "\uF8E5", + "/radoverssquare": "\u33AE", + "/radoverssquaredsquare": "\u33AF", + "/radsquare": "\u33AD", + "/rafe": "\u05BF", + "/rafehebrew": "\u05BF", + "/ragujarati": "\u0AB0", + "/ragurmukhi": "\u0A30", + "/rahiragana": "\u3089", + "/rakatakana": "\u30E9", + "/rakatakanahalfwidth": "\uFF97", + "/ralowerdiagonalbengali": "\u09F1", + "/ramiddlediagonalbengali": "\u09F0", + "/ramshorn": "\u0264", + "/ratio": "\u2236", + "/rbopomofo": "\u3116", + "/rcaron": "\u0159", + "/rcedilla": "\u0157", + "/rcircle": "\u24E1", + "/rcommaaccent": "\u0157", + "/rdblgrave": "\u0211", + "/rdotaccent": "\u1E59", + "/rdotbelow": "\u1E5B", + "/rdotbelowmacron": "\u1E5D", + "/referencemark": "\u203B", + "/reflexsubset": "\u2286", + "/reflexsuperset": "\u2287", + "/registered": "\u00AE", + "/registersans": "\uF8E8", + "/registerserif": "\uF6DA", + "/reharabic": "\u0631", + "/reharmenian": "\u0580", + "/rehfinalarabic": "\uFEAE", + "/rehiragana": "\u308C", + "/rehyehaleflamarabic": "\u0631\uFEF3\uFE8E\u0644", + "/rekatakana": "\u30EC", + "/rekatakanahalfwidth": "\uFF9A", + "/resh": "\u05E8", + "/reshdageshhebrew": "\uFB48", + "/reshhatafpatah": "\u05E8\u05B2", + "/reshhatafpatahhebrew": "\u05E8\u05B2", + "/reshhatafsegol": "\u05E8\u05B1", + "/reshhatafsegolhebrew": "\u05E8\u05B1", + "/reshhebrew": "\u05E8", + "/reshhiriq": "\u05E8\u05B4", + "/reshhiriqhebrew": "\u05E8\u05B4", + "/reshholam": "\u05E8\u05B9", + "/reshholamhebrew": "\u05E8\u05B9", + "/reshpatah": "\u05E8\u05B7", + "/reshpatahhebrew": "\u05E8\u05B7", + "/reshqamats": "\u05E8\u05B8", + "/reshqamatshebrew": "\u05E8\u05B8", + "/reshqubuts": "\u05E8\u05BB", + "/reshqubutshebrew": "\u05E8\u05BB", + "/reshsegol": "\u05E8\u05B6", + "/reshsegolhebrew": "\u05E8\u05B6", + "/reshsheva": "\u05E8\u05B0", + "/reshshevahebrew": "\u05E8\u05B0", + "/reshtsere": "\u05E8\u05B5", + "/reshtserehebrew": "\u05E8\u05B5", + "/reversedtilde": "\u223D", + "/reviahebrew": "\u0597", + "/reviamugrashhebrew": "\u0597", + "/revlogicalnot": "\u2310", + "/rfishhook": "\u027E", + "/rfishhookreversed": "\u027F", + "/rhabengali": "\u09DD", + "/rhadeva": "\u095D", + "/rho": "\u03C1", + "/rhook": "\u027D", + "/rhookturned": "\u027B", + "/rhookturnedsuperior": "\u02B5", + "/rhosymbolgreek": "\u03F1", + "/rhotichookmod": "\u02DE", + "/rieulacirclekorean": "\u3271", + "/rieulaparenkorean": "\u3211", + "/rieulcirclekorean": "\u3263", + "/rieulhieuhkorean": "\u3140", + "/rieulkiyeokkorean": "\u313A", + "/rieulkiyeoksioskorean": "\u3169", + "/rieulkorean": "\u3139", + "/rieulmieumkorean": "\u313B", + "/rieulpansioskorean": "\u316C", + "/rieulparenkorean": "\u3203", + "/rieulphieuphkorean": "\u313F", + "/rieulpieupkorean": "\u313C", + "/rieulpieupsioskorean": "\u316B", + "/rieulsioskorean": "\u313D", + "/rieulthieuthkorean": "\u313E", + "/rieultikeutkorean": "\u316A", + "/rieulyeorinhieuhkorean": "\u316D", + "/rightangle": "\u221F", + "/righttackbelowcmb": "\u0319", + "/righttriangle": "\u22BF", + "/rihiragana": "\u308A", + "/rikatakana": "\u30EA", + "/rikatakanahalfwidth": "\uFF98", + "/ring": "\u02DA", + "/ringbelowcmb": "\u0325", + "/ringcmb": "\u030A", + "/ringhalfleft": "\u02BF", + "/ringhalfleftarmenian": "\u0559", + "/ringhalfleftbelowcmb": "\u031C", + "/ringhalfleftcentered": "\u02D3", + "/ringhalfright": "\u02BE", + "/ringhalfrightbelowcmb": "\u0339", + "/ringhalfrightcentered": "\u02D2", + "/rinvertedbreve": "\u0213", + "/rittorusquare": "\u3351", + "/rlinebelow": "\u1E5F", + "/rlongleg": "\u027C", + "/rlonglegturned": "\u027A", + "/rmonospace": "\uFF52", + "/rohiragana": "\u308D", + "/rokatakana": "\u30ED", + "/rokatakanahalfwidth": "\uFF9B", + "/roruathai": "\u0E23", + "/rparen": "\u24AD", + "/rrabengali": "\u09DC", + "/rradeva": "\u0931", + "/rragurmukhi": "\u0A5C", + "/rreharabic": "\u0691", + "/rrehfinalarabic": "\uFB8D", + "/rrvocalicbengali": "\u09E0", + "/rrvocalicdeva": "\u0960", + "/rrvocalicgujarati": "\u0AE0", + "/rrvocalicvowelsignbengali": "\u09C4", + "/rrvocalicvowelsigndeva": "\u0944", + "/rrvocalicvowelsigngujarati": "\u0AC4", + "/rsuperior": "\uF6F1", + "/rtblock": "\u2590", + "/rturned": "\u0279", + "/rturnedsuperior": "\u02B4", + "/ruhiragana": "\u308B", + "/rukatakana": "\u30EB", + "/rukatakanahalfwidth": "\uFF99", + "/rupeemarkbengali": "\u09F2", + "/rupeesignbengali": "\u09F3", + "/rupiah": "\uF6DD", + "/ruthai": "\u0E24", + "/rvocalicbengali": "\u098B", + "/rvocalicdeva": "\u090B", + "/rvocalicgujarati": "\u0A8B", + "/rvocalicvowelsignbengali": "\u09C3", + "/rvocalicvowelsigndeva": "\u0943", + "/rvocalicvowelsigngujarati": "\u0AC3", + "/s": "\u0073", + "/sabengali": "\u09B8", + "/sacute": "\u015B", + "/sacutedotaccent": "\u1E65", + "/sadarabic": "\u0635", + "/sadeva": "\u0938", + "/sadfinalarabic": "\uFEBA", + "/sadinitialarabic": "\uFEBB", + "/sadmedialarabic": "\uFEBC", + "/sagujarati": "\u0AB8", + "/sagurmukhi": "\u0A38", + "/sahiragana": "\u3055", + "/sakatakana": "\u30B5", + "/sakatakanahalfwidth": "\uFF7B", + "/sallallahoualayhewasallamarabic": "\uFDFA", + "/samekh": "\u05E1", + "/samekhdagesh": "\uFB41", + "/samekhdageshhebrew": "\uFB41", + "/samekhhebrew": "\u05E1", + "/saraaathai": "\u0E32", + "/saraaethai": "\u0E41", + "/saraaimaimalaithai": "\u0E44", + "/saraaimaimuanthai": "\u0E43", + "/saraamthai": "\u0E33", + "/saraathai": "\u0E30", + "/saraethai": "\u0E40", + "/saraiileftthai": "\uF886", + "/saraiithai": "\u0E35", + "/saraileftthai": "\uF885", + "/saraithai": "\u0E34", + "/saraothai": "\u0E42", + "/saraueeleftthai": "\uF888", + "/saraueethai": "\u0E37", + "/saraueleftthai": "\uF887", + "/sarauethai": "\u0E36", + "/sarauthai": "\u0E38", + "/sarauuthai": "\u0E39", + "/sbopomofo": "\u3119", + "/scaron": "\u0161", + "/scarondotaccent": "\u1E67", + "/scedilla": "\u015F", + "/schwa": "\u0259", + "/schwacyrillic": "\u04D9", + "/schwadieresiscyrillic": "\u04DB", + "/schwahook": "\u025A", + "/scircle": "\u24E2", + "/scircumflex": "\u015D", + "/scommaaccent": "\u0219", + "/sdotaccent": "\u1E61", + "/sdotbelow": "\u1E63", + "/sdotbelowdotaccent": "\u1E69", + "/seagullbelowcmb": "\u033C", + "/second": "\u2033", + "/secondtonechinese": "\u02CA", + "/section": "\u00A7", + "/seenarabic": "\u0633", + "/seenfinalarabic": "\uFEB2", + "/seeninitialarabic": "\uFEB3", + "/seenmedialarabic": "\uFEB4", + "/segol": "\u05B6", + "/segol13": "\u05B6", + "/segol1f": "\u05B6", + "/segol2c": "\u05B6", + "/segolhebrew": "\u05B6", + "/segolnarrowhebrew": "\u05B6", + "/segolquarterhebrew": "\u05B6", + "/segoltahebrew": "\u0592", + "/segolwidehebrew": "\u05B6", + "/seharmenian": "\u057D", + "/sehiragana": "\u305B", + "/sekatakana": "\u30BB", + "/sekatakanahalfwidth": "\uFF7E", + "/semicolon": "\u003B", + "/semicolonarabic": "\u061B", + "/semicolonmonospace": "\uFF1B", + "/semicolonsmall": "\uFE54", + "/semivoicedmarkkana": "\u309C", + "/semivoicedmarkkanahalfwidth": "\uFF9F", + "/sentisquare": "\u3322", + "/sentosquare": "\u3323", + "/seven": "\u0037", + "/sevenarabic": "\u0667", + "/sevenbengali": "\u09ED", + "/sevencircle": "\u2466", + "/sevencircleinversesansserif": "\u2790", + "/sevendeva": "\u096D", + "/seveneighths": "\u215E", + "/sevengujarati": "\u0AED", + "/sevengurmukhi": "\u0A6D", + "/sevenhackarabic": "\u0667", + "/sevenhangzhou": "\u3027", + "/sevenideographicparen": "\u3226", + "/seveninferior": "\u2087", + "/sevenmonospace": "\uFF17", + "/sevenoldstyle": "\uF737", + "/sevenparen": "\u247A", + "/sevenperiod": "\u248E", + "/sevenpersian": "\u06F7", + "/sevenroman": "\u2176", + "/sevensuperior": "\u2077", + "/seventeencircle": "\u2470", + "/seventeenparen": "\u2484", + "/seventeenperiod": "\u2498", + "/seventhai": "\u0E57", + "/sfthyphen": "\u00AD", + "/shaarmenian": "\u0577", + "/shabengali": "\u09B6", + "/shacyrillic": "\u0448", + "/shaddaarabic": "\u0651", + "/shaddadammaarabic": "\uFC61", + "/shaddadammatanarabic": "\uFC5E", + "/shaddafathaarabic": "\uFC60", + "/shaddafathatanarabic": "\u0651\u064B", + "/shaddakasraarabic": "\uFC62", + "/shaddakasratanarabic": "\uFC5F", + "/shade": "\u2592", + "/shadedark": "\u2593", + "/shadelight": "\u2591", + "/shademedium": "\u2592", + "/shadeva": "\u0936", + "/shagujarati": "\u0AB6", + "/shagurmukhi": "\u0A36", + "/shalshelethebrew": "\u0593", + "/shbopomofo": "\u3115", + "/shchacyrillic": "\u0449", + "/sheenarabic": "\u0634", + "/sheenfinalarabic": "\uFEB6", + "/sheeninitialarabic": "\uFEB7", + "/sheenmedialarabic": "\uFEB8", + "/sheicoptic": "\u03E3", + "/sheqel": "\u20AA", + "/sheqelhebrew": "\u20AA", + "/sheva": "\u05B0", + "/sheva115": "\u05B0", + "/sheva15": "\u05B0", + "/sheva22": "\u05B0", + "/sheva2e": "\u05B0", + "/shevahebrew": "\u05B0", + "/shevanarrowhebrew": "\u05B0", + "/shevaquarterhebrew": "\u05B0", + "/shevawidehebrew": "\u05B0", + "/shhacyrillic": "\u04BB", + "/shimacoptic": "\u03ED", + "/shin": "\u05E9", + "/shindagesh": "\uFB49", + "/shindageshhebrew": "\uFB49", + "/shindageshshindot": "\uFB2C", + "/shindageshshindothebrew": "\uFB2C", + "/shindageshsindot": "\uFB2D", + "/shindageshsindothebrew": "\uFB2D", + "/shindothebrew": "\u05C1", + "/shinhebrew": "\u05E9", + "/shinshindot": "\uFB2A", + "/shinshindothebrew": "\uFB2A", + "/shinsindot": "\uFB2B", + "/shinsindothebrew": "\uFB2B", + "/shook": "\u0282", + "/sigma": "\u03C3", + "/sigma1": "\u03C2", + "/sigmafinal": "\u03C2", + "/sigmalunatesymbolgreek": "\u03F2", + "/sihiragana": "\u3057", + "/sikatakana": "\u30B7", + "/sikatakanahalfwidth": "\uFF7C", + "/siluqhebrew": "\u05BD", + "/siluqlefthebrew": "\u05BD", + "/similar": "\u223C", + "/sindothebrew": "\u05C2", + "/siosacirclekorean": "\u3274", + "/siosaparenkorean": "\u3214", + "/sioscieuckorean": "\u317E", + "/sioscirclekorean": "\u3266", + "/sioskiyeokkorean": "\u317A", + "/sioskorean": "\u3145", + "/siosnieunkorean": "\u317B", + "/siosparenkorean": "\u3206", + "/siospieupkorean": "\u317D", + "/siostikeutkorean": "\u317C", + "/six": "\u0036", + "/sixarabic": "\u0666", + "/sixbengali": "\u09EC", + "/sixcircle": "\u2465", + "/sixcircleinversesansserif": "\u278F", + "/sixdeva": "\u096C", + "/sixgujarati": "\u0AEC", + "/sixgurmukhi": "\u0A6C", + "/sixhackarabic": "\u0666", + "/sixhangzhou": "\u3026", + "/sixideographicparen": "\u3225", + "/sixinferior": "\u2086", + "/sixmonospace": "\uFF16", + "/sixoldstyle": "\uF736", + "/sixparen": "\u2479", + "/sixperiod": "\u248D", + "/sixpersian": "\u06F6", + "/sixroman": "\u2175", + "/sixsuperior": "\u2076", + "/sixteencircle": "\u246F", + "/sixteencurrencydenominatorbengali": "\u09F9", + "/sixteenparen": "\u2483", + "/sixteenperiod": "\u2497", + "/sixthai": "\u0E56", + "/slash": "\u002F", + "/slashmonospace": "\uFF0F", + "/slong": "\u017F", + "/slongdotaccent": "\u1E9B", + "/smileface": "\u263A", + "/smonospace": "\uFF53", + "/sofpasuqhebrew": "\u05C3", + "/softhyphen": "\u00AD", + "/softsigncyrillic": "\u044C", + "/sohiragana": "\u305D", + "/sokatakana": "\u30BD", + "/sokatakanahalfwidth": "\uFF7F", + "/soliduslongoverlaycmb": "\u0338", + "/solidusshortoverlaycmb": "\u0337", + "/sorusithai": "\u0E29", + "/sosalathai": "\u0E28", + "/sosothai": "\u0E0B", + "/sosuathai": "\u0E2A", + "/space": "\u0020", + "/spacehackarabic": "\u0020", + "/spade": "\u2660", + "/spadesuitblack": "\u2660", + "/spadesuitwhite": "\u2664", + "/sparen": "\u24AE", + "/squarebelowcmb": "\u033B", + "/squarecc": "\u33C4", + "/squarecm": "\u339D", + "/squarediagonalcrosshatchfill": "\u25A9", + "/squarehorizontalfill": "\u25A4", + "/squarekg": "\u338F", + "/squarekm": "\u339E", + "/squarekmcapital": "\u33CE", + "/squareln": "\u33D1", + "/squarelog": "\u33D2", + "/squaremg": "\u338E", + "/squaremil": "\u33D5", + "/squaremm": "\u339C", + "/squaremsquared": "\u33A1", + "/squareorthogonalcrosshatchfill": "\u25A6", + "/squareupperlefttolowerrightfill": "\u25A7", + "/squareupperrighttolowerleftfill": "\u25A8", + "/squareverticalfill": "\u25A5", + "/squarewhitewithsmallblack": "\u25A3", + "/srsquare": "\u33DB", + "/ssabengali": "\u09B7", + "/ssadeva": "\u0937", + "/ssagujarati": "\u0AB7", + "/ssangcieuckorean": "\u3149", + "/ssanghieuhkorean": "\u3185", + "/ssangieungkorean": "\u3180", + "/ssangkiyeokkorean": "\u3132", + "/ssangnieunkorean": "\u3165", + "/ssangpieupkorean": "\u3143", + "/ssangsioskorean": "\u3146", + "/ssangtikeutkorean": "\u3138", + "/ssuperior": "\uF6F2", + "/sterling": "\u00A3", + "/sterlingmonospace": "\uFFE1", + "/strokelongoverlaycmb": "\u0336", + "/strokeshortoverlaycmb": "\u0335", + "/subset": "\u2282", + "/subsetnotequal": "\u228A", + "/subsetorequal": "\u2286", + "/succeeds": "\u227B", + "/suchthat": "\u220B", + "/suhiragana": "\u3059", + "/sukatakana": "\u30B9", + "/sukatakanahalfwidth": "\uFF7D", + "/sukunarabic": "\u0652", + "/summation": "\u2211", + "/sun": "\u263C", + "/superset": "\u2283", + "/supersetnotequal": "\u228B", + "/supersetorequal": "\u2287", + "/svsquare": "\u33DC", + "/syouwaerasquare": "\u337C", + "/t": "\u0074", + "/tabengali": "\u09A4", + "/tackdown": "\u22A4", + "/tackleft": "\u22A3", + "/tadeva": "\u0924", + "/tagujarati": "\u0AA4", + "/tagurmukhi": "\u0A24", + "/taharabic": "\u0637", + "/tahfinalarabic": "\uFEC2", + "/tahinitialarabic": "\uFEC3", + "/tahiragana": "\u305F", + "/tahmedialarabic": "\uFEC4", + "/taisyouerasquare": "\u337D", + "/takatakana": "\u30BF", + "/takatakanahalfwidth": "\uFF80", + "/tatweelarabic": "\u0640", + "/tau": "\u03C4", + "/tav": "\u05EA", + "/tavdages": "\uFB4A", + "/tavdagesh": "\uFB4A", + "/tavdageshhebrew": "\uFB4A", + "/tavhebrew": "\u05EA", + "/tbar": "\u0167", + "/tbopomofo": "\u310A", + "/tcaron": "\u0165", + "/tccurl": "\u02A8", + "/tcedilla": "\u0163", + "/tcheharabic": "\u0686", + "/tchehfinalarabic": "\uFB7B", + "/tchehinitialarabic": "\uFB7C", + "/tchehmedialarabic": "\uFB7D", + "/tchehmeeminitialarabic": "\uFB7C\uFEE4", + "/tcircle": "\u24E3", + "/tcircumflexbelow": "\u1E71", + "/tcommaaccent": "\u0163", + "/tdieresis": "\u1E97", + "/tdotaccent": "\u1E6B", + "/tdotbelow": "\u1E6D", + "/tecyrillic": "\u0442", + "/tedescendercyrillic": "\u04AD", + "/teharabic": "\u062A", + "/tehfinalarabic": "\uFE96", + "/tehhahinitialarabic": "\uFCA2", + "/tehhahisolatedarabic": "\uFC0C", + "/tehinitialarabic": "\uFE97", + "/tehiragana": "\u3066", + "/tehjeeminitialarabic": "\uFCA1", + "/tehjeemisolatedarabic": "\uFC0B", + "/tehmarbutaarabic": "\u0629", + "/tehmarbutafinalarabic": "\uFE94", + "/tehmedialarabic": "\uFE98", + "/tehmeeminitialarabic": "\uFCA4", + "/tehmeemisolatedarabic": "\uFC0E", + "/tehnoonfinalarabic": "\uFC73", + "/tekatakana": "\u30C6", + "/tekatakanahalfwidth": "\uFF83", + "/telephone": "\u2121", + "/telephoneblack": "\u260E", + "/telishagedolahebrew": "\u05A0", + "/telishaqetanahebrew": "\u05A9", + "/tencircle": "\u2469", + "/tenideographicparen": "\u3229", + "/tenparen": "\u247D", + "/tenperiod": "\u2491", + "/tenroman": "\u2179", + "/tesh": "\u02A7", + "/tet": "\u05D8", + "/tetdagesh": "\uFB38", + "/tetdageshhebrew": "\uFB38", + "/tethebrew": "\u05D8", + "/tetsecyrillic": "\u04B5", + "/tevirhebrew": "\u059B", + "/tevirlefthebrew": "\u059B", + "/thabengali": "\u09A5", + "/thadeva": "\u0925", + "/thagujarati": "\u0AA5", + "/thagurmukhi": "\u0A25", + "/thalarabic": "\u0630", + "/thalfinalarabic": "\uFEAC", + "/thanthakhatlowleftthai": "\uF898", + "/thanthakhatlowrightthai": "\uF897", + "/thanthakhatthai": "\u0E4C", + "/thanthakhatupperleftthai": "\uF896", + "/theharabic": "\u062B", + "/thehfinalarabic": "\uFE9A", + "/thehinitialarabic": "\uFE9B", + "/thehmedialarabic": "\uFE9C", + "/thereexists": "\u2203", + "/therefore": "\u2234", + "/theta": "\u03B8", + "/theta1": "\u03D1", + "/thetasymbolgreek": "\u03D1", + "/thieuthacirclekorean": "\u3279", + "/thieuthaparenkorean": "\u3219", + "/thieuthcirclekorean": "\u326B", + "/thieuthkorean": "\u314C", + "/thieuthparenkorean": "\u320B", + "/thirteencircle": "\u246C", + "/thirteenparen": "\u2480", + "/thirteenperiod": "\u2494", + "/thonangmonthothai": "\u0E11", + "/thook": "\u01AD", + "/thophuthaothai": "\u0E12", + "/thorn": "\u00FE", + "/thothahanthai": "\u0E17", + "/thothanthai": "\u0E10", + "/thothongthai": "\u0E18", + "/thothungthai": "\u0E16", + "/thousandcyrillic": "\u0482", + "/thousandsseparatorarabic": "\u066C", + "/thousandsseparatorpersian": "\u066C", + "/three": "\u0033", + "/threearabic": "\u0663", + "/threebengali": "\u09E9", + "/threecircle": "\u2462", + "/threecircleinversesansserif": "\u278C", + "/threedeva": "\u0969", + "/threeeighths": "\u215C", + "/threegujarati": "\u0AE9", + "/threegurmukhi": "\u0A69", + "/threehackarabic": "\u0663", + "/threehangzhou": "\u3023", + "/threeideographicparen": "\u3222", + "/threeinferior": "\u2083", + "/threemonospace": "\uFF13", + "/threenumeratorbengali": "\u09F6", + "/threeoldstyle": "\uF733", + "/threeparen": "\u2476", + "/threeperiod": "\u248A", + "/threepersian": "\u06F3", + "/threequarters": "\u00BE", + "/threequartersemdash": "\uF6DE", + "/threeroman": "\u2172", + "/threesuperior": "\u00B3", + "/threethai": "\u0E53", + "/thzsquare": "\u3394", + "/tihiragana": "\u3061", + "/tikatakana": "\u30C1", + "/tikatakanahalfwidth": "\uFF81", + "/tikeutacirclekorean": "\u3270", + "/tikeutaparenkorean": "\u3210", + "/tikeutcirclekorean": "\u3262", + "/tikeutkorean": "\u3137", + "/tikeutparenkorean": "\u3202", + "/tilde": "\u02DC", + "/tildebelowcmb": "\u0330", + "/tildecmb": "\u0303", + "/tildecomb": "\u0303", + "/tildedoublecmb": "\u0360", + "/tildeoperator": "\u223C", + "/tildeoverlaycmb": "\u0334", + "/tildeverticalcmb": "\u033E", + "/timescircle": "\u2297", + "/tipehahebrew": "\u0596", + "/tipehalefthebrew": "\u0596", + "/tippigurmukhi": "\u0A70", + "/titlocyrilliccmb": "\u0483", + "/tiwnarmenian": "\u057F", + "/tlinebelow": "\u1E6F", + "/tmonospace": "\uFF54", + "/toarmenian": "\u0569", + "/tohiragana": "\u3068", + "/tokatakana": "\u30C8", + "/tokatakanahalfwidth": "\uFF84", + "/tonebarextrahighmod": "\u02E5", + "/tonebarextralowmod": "\u02E9", + "/tonebarhighmod": "\u02E6", + "/tonebarlowmod": "\u02E8", + "/tonebarmidmod": "\u02E7", + "/tonefive": "\u01BD", + "/tonesix": "\u0185", + "/tonetwo": "\u01A8", + "/tonos": "\u0384", + "/tonsquare": "\u3327", + "/topatakthai": "\u0E0F", + "/tortoiseshellbracketleft": "\u3014", + "/tortoiseshellbracketleftsmall": "\uFE5D", + "/tortoiseshellbracketleftvertical": "\uFE39", + "/tortoiseshellbracketright": "\u3015", + "/tortoiseshellbracketrightsmall": "\uFE5E", + "/tortoiseshellbracketrightvertical": "\uFE3A", + "/totaothai": "\u0E15", + "/tpalatalhook": "\u01AB", + "/tparen": "\u24AF", + "/trademark": "\u2122", + "/trademarksans": "\uF8EA", + "/trademarkserif": "\uF6DB", + "/tretroflexhook": "\u0288", + "/triagdn": "\u25BC", + "/triaglf": "\u25C4", + "/triagrt": "\u25BA", + "/triagup": "\u25B2", + "/ts": "\u02A6", + "/tsadi": "\u05E6", + "/tsadidagesh": "\uFB46", + "/tsadidageshhebrew": "\uFB46", + "/tsadihebrew": "\u05E6", + "/tsecyrillic": "\u0446", + "/tsere": "\u05B5", + "/tsere12": "\u05B5", + "/tsere1e": "\u05B5", + "/tsere2b": "\u05B5", + "/tserehebrew": "\u05B5", + "/tserenarrowhebrew": "\u05B5", + "/tserequarterhebrew": "\u05B5", + "/tserewidehebrew": "\u05B5", + "/tshecyrillic": "\u045B", + "/tsuperior": "\uF6F3", + "/ttabengali": "\u099F", + "/ttadeva": "\u091F", + "/ttagujarati": "\u0A9F", + "/ttagurmukhi": "\u0A1F", + "/tteharabic": "\u0679", + "/ttehfinalarabic": "\uFB67", + "/ttehinitialarabic": "\uFB68", + "/ttehmedialarabic": "\uFB69", + "/tthabengali": "\u09A0", + "/tthadeva": "\u0920", + "/tthagujarati": "\u0AA0", + "/tthagurmukhi": "\u0A20", + "/tturned": "\u0287", + "/tuhiragana": "\u3064", + "/tukatakana": "\u30C4", + "/tukatakanahalfwidth": "\uFF82", + "/tusmallhiragana": "\u3063", + "/tusmallkatakana": "\u30C3", + "/tusmallkatakanahalfwidth": "\uFF6F", + "/twelvecircle": "\u246B", + "/twelveparen": "\u247F", + "/twelveperiod": "\u2493", + "/twelveroman": "\u217B", + "/twentycircle": "\u2473", + "/twentyhangzhou": "\u5344", + "/twentyparen": "\u2487", + "/twentyperiod": "\u249B", + "/two": "\u0032", + "/twoarabic": "\u0662", + "/twobengali": "\u09E8", + "/twocircle": "\u2461", + "/twocircleinversesansserif": "\u278B", + "/twodeva": "\u0968", + "/twodotenleader": "\u2025", + "/twodotleader": "\u2025", + "/twodotleadervertical": "\uFE30", + "/twogujarati": "\u0AE8", + "/twogurmukhi": "\u0A68", + "/twohackarabic": "\u0662", + "/twohangzhou": "\u3022", + "/twoideographicparen": "\u3221", + "/twoinferior": "\u2082", + "/twomonospace": "\uFF12", + "/twonumeratorbengali": "\u09F5", + "/twooldstyle": "\uF732", + "/twoparen": "\u2475", + "/twoperiod": "\u2489", + "/twopersian": "\u06F2", + "/tworoman": "\u2171", + "/twostroke": "\u01BB", + "/twosuperior": "\u00B2", + "/twothai": "\u0E52", + "/twothirds": "\u2154", + "/u": "\u0075", + "/uacute": "\u00FA", + "/ubar": "\u0289", + "/ubengali": "\u0989", + "/ubopomofo": "\u3128", + "/ubreve": "\u016D", + "/ucaron": "\u01D4", + "/ucircle": "\u24E4", + "/ucircumflex": "\u00FB", + "/ucircumflexbelow": "\u1E77", + "/ucyrillic": "\u0443", + "/udattadeva": "\u0951", + "/udblacute": "\u0171", + "/udblgrave": "\u0215", + "/udeva": "\u0909", + "/udieresis": "\u00FC", + "/udieresisacute": "\u01D8", + "/udieresisbelow": "\u1E73", + "/udieresiscaron": "\u01DA", + "/udieresiscyrillic": "\u04F1", + "/udieresisgrave": "\u01DC", + "/udieresismacron": "\u01D6", + "/udotbelow": "\u1EE5", + "/ugrave": "\u00F9", + "/ugujarati": "\u0A89", + "/ugurmukhi": "\u0A09", + "/uhiragana": "\u3046", + "/uhookabove": "\u1EE7", + "/uhorn": "\u01B0", + "/uhornacute": "\u1EE9", + "/uhorndotbelow": "\u1EF1", + "/uhorngrave": "\u1EEB", + "/uhornhookabove": "\u1EED", + "/uhorntilde": "\u1EEF", + "/uhungarumlaut": "\u0171", + "/uhungarumlautcyrillic": "\u04F3", + "/uinvertedbreve": "\u0217", + "/ukatakana": "\u30A6", + "/ukatakanahalfwidth": "\uFF73", + "/ukcyrillic": "\u0479", + "/ukorean": "\u315C", + "/umacron": "\u016B", + "/umacroncyrillic": "\u04EF", + "/umacrondieresis": "\u1E7B", + "/umatragurmukhi": "\u0A41", + "/umonospace": "\uFF55", + "/underscore": "\u005F", + "/underscoredbl": "\u2017", + "/underscoremonospace": "\uFF3F", + "/underscorevertical": "\uFE33", + "/underscorewavy": "\uFE4F", + "/union": "\u222A", + "/universal": "\u2200", + "/uogonek": "\u0173", + "/uparen": "\u24B0", + "/upblock": "\u2580", + "/upperdothebrew": "\u05C4", + "/upsilon": "\u03C5", + "/upsilondieresis": "\u03CB", + "/upsilondieresistonos": "\u03B0", + "/upsilonlatin": "\u028A", + "/upsilontonos": "\u03CD", + "/uptackbelowcmb": "\u031D", + "/uptackmod": "\u02D4", + "/uragurmukhi": "\u0A73", + "/uring": "\u016F", + "/ushortcyrillic": "\u045E", + "/usmallhiragana": "\u3045", + "/usmallkatakana": "\u30A5", + "/usmallkatakanahalfwidth": "\uFF69", + "/ustraightcyrillic": "\u04AF", + "/ustraightstrokecyrillic": "\u04B1", + "/utilde": "\u0169", + "/utildeacute": "\u1E79", + "/utildebelow": "\u1E75", + "/uubengali": "\u098A", + "/uudeva": "\u090A", + "/uugujarati": "\u0A8A", + "/uugurmukhi": "\u0A0A", + "/uumatragurmukhi": "\u0A42", + "/uuvowelsignbengali": "\u09C2", + "/uuvowelsigndeva": "\u0942", + "/uuvowelsigngujarati": "\u0AC2", + "/uvowelsignbengali": "\u09C1", + "/uvowelsigndeva": "\u0941", + "/uvowelsigngujarati": "\u0AC1", + "/v": "\u0076", + "/vadeva": "\u0935", + "/vagujarati": "\u0AB5", + "/vagurmukhi": "\u0A35", + "/vakatakana": "\u30F7", + "/vav": "\u05D5", + "/vavdagesh": "\uFB35", + "/vavdagesh65": "\uFB35", + "/vavdageshhebrew": "\uFB35", + "/vavhebrew": "\u05D5", + "/vavholam": "\uFB4B", + "/vavholamhebrew": "\uFB4B", + "/vavvavhebrew": "\u05F0", + "/vavyodhebrew": "\u05F1", + "/vcircle": "\u24E5", + "/vdotbelow": "\u1E7F", + "/vecyrillic": "\u0432", + "/veharabic": "\u06A4", + "/vehfinalarabic": "\uFB6B", + "/vehinitialarabic": "\uFB6C", + "/vehmedialarabic": "\uFB6D", + "/vekatakana": "\u30F9", + "/venus": "\u2640", + "/verticalbar": "\u007C", + "/verticallineabovecmb": "\u030D", + "/verticallinebelowcmb": "\u0329", + "/verticallinelowmod": "\u02CC", + "/verticallinemod": "\u02C8", + "/vewarmenian": "\u057E", + "/vhook": "\u028B", + "/vikatakana": "\u30F8", + "/viramabengali": "\u09CD", + "/viramadeva": "\u094D", + "/viramagujarati": "\u0ACD", + "/visargabengali": "\u0983", + "/visargadeva": "\u0903", + "/visargagujarati": "\u0A83", + "/vmonospace": "\uFF56", + "/voarmenian": "\u0578", + "/voicediterationhiragana": "\u309E", + "/voicediterationkatakana": "\u30FE", + "/voicedmarkkana": "\u309B", + "/voicedmarkkanahalfwidth": "\uFF9E", + "/vokatakana": "\u30FA", + "/vparen": "\u24B1", + "/vtilde": "\u1E7D", + "/vturned": "\u028C", + "/vuhiragana": "\u3094", + "/vukatakana": "\u30F4", + "/w": "\u0077", + "/wacute": "\u1E83", + "/waekorean": "\u3159", + "/wahiragana": "\u308F", + "/wakatakana": "\u30EF", + "/wakatakanahalfwidth": "\uFF9C", + "/wakorean": "\u3158", + "/wasmallhiragana": "\u308E", + "/wasmallkatakana": "\u30EE", + "/wattosquare": "\u3357", + "/wavedash": "\u301C", + "/wavyunderscorevertical": "\uFE34", + "/wawarabic": "\u0648", + "/wawfinalarabic": "\uFEEE", + "/wawhamzaabovearabic": "\u0624", + "/wawhamzaabovefinalarabic": "\uFE86", + "/wbsquare": "\u33DD", + "/wcircle": "\u24E6", + "/wcircumflex": "\u0175", + "/wdieresis": "\u1E85", + "/wdotaccent": "\u1E87", + "/wdotbelow": "\u1E89", + "/wehiragana": "\u3091", + "/weierstrass": "\u2118", + "/wekatakana": "\u30F1", + "/wekorean": "\u315E", + "/weokorean": "\u315D", + "/wgrave": "\u1E81", + "/whitebullet": "\u25E6", + "/whitecircle": "\u25CB", + "/whitecircleinverse": "\u25D9", + "/whitecornerbracketleft": "\u300E", + "/whitecornerbracketleftvertical": "\uFE43", + "/whitecornerbracketright": "\u300F", + "/whitecornerbracketrightvertical": "\uFE44", + "/whitediamond": "\u25C7", + "/whitediamondcontainingblacksmalldiamond": "\u25C8", + "/whitedownpointingsmalltriangle": "\u25BF", + "/whitedownpointingtriangle": "\u25BD", + "/whiteleftpointingsmalltriangle": "\u25C3", + "/whiteleftpointingtriangle": "\u25C1", + "/whitelenticularbracketleft": "\u3016", + "/whitelenticularbracketright": "\u3017", + "/whiterightpointingsmalltriangle": "\u25B9", + "/whiterightpointingtriangle": "\u25B7", + "/whitesmallsquare": "\u25AB", + "/whitesmilingface": "\u263A", + "/whitesquare": "\u25A1", + "/whitestar": "\u2606", + "/whitetelephone": "\u260F", + "/whitetortoiseshellbracketleft": "\u3018", + "/whitetortoiseshellbracketright": "\u3019", + "/whiteuppointingsmalltriangle": "\u25B5", + "/whiteuppointingtriangle": "\u25B3", + "/wihiragana": "\u3090", + "/wikatakana": "\u30F0", + "/wikorean": "\u315F", + "/wmonospace": "\uFF57", + "/wohiragana": "\u3092", + "/wokatakana": "\u30F2", + "/wokatakanahalfwidth": "\uFF66", + "/won": "\u20A9", + "/wonmonospace": "\uFFE6", + "/wowaenthai": "\u0E27", + "/wparen": "\u24B2", + "/wring": "\u1E98", + "/wsuperior": "\u02B7", + "/wturned": "\u028D", + "/wynn": "\u01BF", + "/x": "\u0078", + "/xabovecmb": "\u033D", + "/xbopomofo": "\u3112", + "/xcircle": "\u24E7", + "/xdieresis": "\u1E8D", + "/xdotaccent": "\u1E8B", + "/xeharmenian": "\u056D", + "/xi": "\u03BE", + "/xmonospace": "\uFF58", + "/xparen": "\u24B3", + "/xsuperior": "\u02E3", + "/y": "\u0079", + "/yaadosquare": "\u334E", + "/yabengali": "\u09AF", + "/yacute": "\u00FD", + "/yadeva": "\u092F", + "/yaekorean": "\u3152", + "/yagujarati": "\u0AAF", + "/yagurmukhi": "\u0A2F", + "/yahiragana": "\u3084", + "/yakatakana": "\u30E4", + "/yakatakanahalfwidth": "\uFF94", + "/yakorean": "\u3151", + "/yamakkanthai": "\u0E4E", + "/yasmallhiragana": "\u3083", + "/yasmallkatakana": "\u30E3", + "/yasmallkatakanahalfwidth": "\uFF6C", + "/yatcyrillic": "\u0463", + "/ycircle": "\u24E8", + "/ycircumflex": "\u0177", + "/ydieresis": "\u00FF", + "/ydotaccent": "\u1E8F", + "/ydotbelow": "\u1EF5", + "/yeharabic": "\u064A", + "/yehbarreearabic": "\u06D2", + "/yehbarreefinalarabic": "\uFBAF", + "/yehfinalarabic": "\uFEF2", + "/yehhamzaabovearabic": "\u0626", + "/yehhamzaabovefinalarabic": "\uFE8A", + "/yehhamzaaboveinitialarabic": "\uFE8B", + "/yehhamzaabovemedialarabic": "\uFE8C", + "/yehinitialarabic": "\uFEF3", + "/yehmedialarabic": "\uFEF4", + "/yehmeeminitialarabic": "\uFCDD", + "/yehmeemisolatedarabic": "\uFC58", + "/yehnoonfinalarabic": "\uFC94", + "/yehthreedotsbelowarabic": "\u06D1", + "/yekorean": "\u3156", + "/yen": "\u00A5", + "/yenmonospace": "\uFFE5", + "/yeokorean": "\u3155", + "/yeorinhieuhkorean": "\u3186", + "/yerahbenyomohebrew": "\u05AA", + "/yerahbenyomolefthebrew": "\u05AA", + "/yericyrillic": "\u044B", + "/yerudieresiscyrillic": "\u04F9", + "/yesieungkorean": "\u3181", + "/yesieungpansioskorean": "\u3183", + "/yesieungsioskorean": "\u3182", + "/yetivhebrew": "\u059A", + "/ygrave": "\u1EF3", + "/yhook": "\u01B4", + "/yhookabove": "\u1EF7", + "/yiarmenian": "\u0575", + "/yicyrillic": "\u0457", + "/yikorean": "\u3162", + "/yinyang": "\u262F", + "/yiwnarmenian": "\u0582", + "/ymonospace": "\uFF59", + "/yod": "\u05D9", + "/yoddagesh": "\uFB39", + "/yoddageshhebrew": "\uFB39", + "/yodhebrew": "\u05D9", + "/yodyodhebrew": "\u05F2", + "/yodyodpatahhebrew": "\uFB1F", + "/yohiragana": "\u3088", + "/yoikorean": "\u3189", + "/yokatakana": "\u30E8", + "/yokatakanahalfwidth": "\uFF96", + "/yokorean": "\u315B", + "/yosmallhiragana": "\u3087", + "/yosmallkatakana": "\u30E7", + "/yosmallkatakanahalfwidth": "\uFF6E", + "/yotgreek": "\u03F3", + "/yoyaekorean": "\u3188", + "/yoyakorean": "\u3187", + "/yoyakthai": "\u0E22", + "/yoyingthai": "\u0E0D", + "/yparen": "\u24B4", + "/ypogegrammeni": "\u037A", + "/ypogegrammenigreekcmb": "\u0345", + "/yr": "\u01A6", + "/yring": "\u1E99", + "/ysuperior": "\u02B8", + "/ytilde": "\u1EF9", + "/yturned": "\u028E", + "/yuhiragana": "\u3086", + "/yuikorean": "\u318C", + "/yukatakana": "\u30E6", + "/yukatakanahalfwidth": "\uFF95", + "/yukorean": "\u3160", + "/yusbigcyrillic": "\u046B", + "/yusbigiotifiedcyrillic": "\u046D", + "/yuslittlecyrillic": "\u0467", + "/yuslittleiotifiedcyrillic": "\u0469", + "/yusmallhiragana": "\u3085", + "/yusmallkatakana": "\u30E5", + "/yusmallkatakanahalfwidth": "\uFF6D", + "/yuyekorean": "\u318B", + "/yuyeokorean": "\u318A", + "/yyabengali": "\u09DF", + "/yyadeva": "\u095F", + "/z": "\u007A", + "/zaarmenian": "\u0566", + "/zacute": "\u017A", + "/zadeva": "\u095B", + "/zagurmukhi": "\u0A5B", + "/zaharabic": "\u0638", + "/zahfinalarabic": "\uFEC6", + "/zahinitialarabic": "\uFEC7", + "/zahiragana": "\u3056", + "/zahmedialarabic": "\uFEC8", + "/zainarabic": "\u0632", + "/zainfinalarabic": "\uFEB0", + "/zakatakana": "\u30B6", + "/zaqefgadolhebrew": "\u0595", + "/zaqefqatanhebrew": "\u0594", + "/zarqahebrew": "\u0598", + "/zayin": "\u05D6", + "/zayindagesh": "\uFB36", + "/zayindageshhebrew": "\uFB36", + "/zayinhebrew": "\u05D6", + "/zbopomofo": "\u3117", + "/zcaron": "\u017E", + "/zcircle": "\u24E9", + "/zcircumflex": "\u1E91", + "/zcurl": "\u0291", + "/zdot": "\u017C", + "/zdotaccent": "\u017C", + "/zdotbelow": "\u1E93", + "/zecyrillic": "\u0437", + "/zedescendercyrillic": "\u0499", + "/zedieresiscyrillic": "\u04DF", + "/zehiragana": "\u305C", + "/zekatakana": "\u30BC", + "/zero": "\u0030", + "/zeroarabic": "\u0660", + "/zerobengali": "\u09E6", + "/zerodeva": "\u0966", + "/zerogujarati": "\u0AE6", + "/zerogurmukhi": "\u0A66", + "/zerohackarabic": "\u0660", + "/zeroinferior": "\u2080", + "/zeromonospace": "\uFF10", + "/zerooldstyle": "\uF730", + "/zeropersian": "\u06F0", + "/zerosuperior": "\u2070", + "/zerothai": "\u0E50", + "/zerowidthjoiner": "\uFEFF", + "/zerowidthnonjoiner": "\u200C", + "/zerowidthspace": "\u200B", + "/zeta": "\u03B6", + "/zhbopomofo": "\u3113", + "/zhearmenian": "\u056A", + "/zhebrevecyrillic": "\u04C2", + "/zhecyrillic": "\u0436", + "/zhedescendercyrillic": "\u0497", + "/zhedieresiscyrillic": "\u04DD", + "/zihiragana": "\u3058", + "/zikatakana": "\u30B8", + "/zinorhebrew": "\u05AE", + "/zlinebelow": "\u1E95", + "/zmonospace": "\uFF5A", + "/zohiragana": "\u305E", + "/zokatakana": "\u30BE", + "/zparen": "\u24B5", + "/zretroflexhook": "\u0290", + "/zstroke": "\u01B6", + "/zuhiragana": "\u305A", + "/zukatakana": "\u30BA", + # glyphNamesToUnicode.txt + "/guillemetleft": "\u00AB", + "/hyphensoft": "\u00AD", + "/two.superior": "\u00B2", + "/three.superior": "\u00B3", + "/mu.math": "\u00B5", + "/one.superior": "\u00B9", + "/guillemetright": "\u00BB", + "/kra": "\u0138", + "/fhook": "\u0192", + "/lt:Gamma": "\u0194", + "/lt:Iota": "\u0196", + "/nlongrightleg": "\u019E", + "/Obar": "\u019F", + "/lt:Upsilon": "\u01B1", + "/Hwair": "\u01F6", + "/Wynn": "\u01F7", + "/Ngrave": "\u01F8", + "/ngrave": "\u01F9", + "/Agravedbl": "\u0200", + "/agravedbl": "\u0201", + "/Egravedbl": "\u0204", + "/egravedbl": "\u0205", + "/Igravedbl": "\u0208", + "/igravedbl": "\u0209", + "/Ogravedbl": "\u020C", + "/ogravedbl": "\u020D", + "/Rgravedbl": "\u0210", + "/rgravedbl": "\u0211", + "/Ugravedbl": "\u0214", + "/ugravedbl": "\u0215", + "/Yogh": "\u021C", + "/yogh": "\u021D", + "/Hcaron": "\u021E", + "/hcaron": "\u021F", + "/Nlongrightleg": "\u0220", + "/dcurl": "\u0221", + "/Ou": "\u0222", + "/ou": "\u0223", + "/Zhook": "\u0224", + "/zhook": "\u0225", + "/Adot": "\u0226", + "/adot": "\u0227", + "/Ecedilla": "\u0228", + "/ecedilla": "\u0229", + "/Odieresismacron": "\u022A", + "/odieresismacron": "\u022B", + "/Otildemacron": "\u022C", + "/otildemacron": "\u022D", + "/Odot": "\u022E", + "/odot": "\u022F", + "/Odotmacron": "\u0230", + "/odotmacron": "\u0231", + "/Ymacron": "\u0232", + "/ymacron": "\u0233", + "/lcurl": "\u0234", + "/ncurl": "\u0235", + "/tcurl": "\u0236", + "/dbdigraph": "\u0238", + "/qpdigraph": "\u0239", + "/Astroke": "\u023A", + "/Cstroke": "\u023B", + "/cstroke": "\u023C", + "/Lbar": "\u023D", + "/Twithdiagonalstroke": "\u023E", + "/sswashtail": "\u023F", + "/zswashtail": "\u0240", + "/Glottalstop": "\u0241", + "/Bstroke": "\u0243", + "/Ubar": "\u0244", + "/Vturned": "\u0245", + "/Estroke": "\u0246", + "/estroke": "\u0247", + "/Jstroke": "\u0248", + "/jstroke": "\u0249", + "/Qsmallhooktail": "\u024A", + "/qhooktail": "\u024B", + "/Rstroke": "\u024C", + "/rstroke": "\u024D", + "/Ystroke": "\u024E", + "/ystroke": "\u024F", + "/ltgr:alpha": "\u0251", + "/alphaturned": "\u0252", + "/ipa:ereversed": "\u0258", + "/dotlessjstroke": "\u025F", + "/ipa:gscript": "\u0261", + "/ltgr:gamma": "\u0263", + "/ltgr:iota": "\u0269", + "/ipa:Ismall": "\u026A", + "/lretroflex": "\u026D", + "/nretroflex": "\u0273", + "/omegaclosed": "\u0277", + "/ltgr:phi": "\u0278", + "/ipa:rtail": "\u027D", + "/Rinvertedsmall": "\u0281", + "/tretroflex": "\u0288", + "/ipa:upsilon": "\u028A", + "/zretroflex": "\u0290", + "/ipa:glottalstop": "\u0294", + "/pharyngealvoicedfricative": "\u0295", + "/Cstretched": "\u0297", + "/clickbilabial": "\u0298", + "/Ghooksmall": "\u029B", + "/dzed": "\u02A3", + "/dzedcurl": "\u02A5", + "/feng": "\u02A9", + "/ls": "\u02AA", + "/lzed": "\u02AB", + "/percussivebilabial": "\u02AC", + "/percussivebidental": "\u02AD", + "/hfishhookturned": "\u02AE", + "/handtailfishhookturned": "\u02AF", + "/hsupmod": "\u02B0", + "/hhooksupmod": "\u02B1", + "/jsupmod": "\u02B2", + "/rsupmod": "\u02B3", + "/rturnedsupmod": "\u02B4", + "/rhookturnedsupmod": "\u02B5", + "/Rsupinvertedmod": "\u02B6", + "/wsupmod": "\u02B7", + "/ysupmod": "\u02B8", + "/primedblmod": "\u02BA", + "/ringhalfrightmod": "\u02BE", + "/ringhalfleftmod": "\u02BF", + "/macronmod": "\u02C9", + "/acutemod": "\u02CA", + "/gravemod": "\u02CB", + "/ringhalfrightcentredmod": "\u02D2", + "/ringhalfleftcentredmod": "\u02D3", + "/tackupmod": "\u02D4", + "/tackdownmod": "\u02D5", + "/plussignmod": "\u02D6", + "/minussignmod": "\u02D7", + "/crossmod": "\u02DF", + "/gammasupmod": "\u02E0", + "/lsupmod": "\u02E1", + "/ssupmod": "\u02E2", + "/xsupmod": "\u02E3", + "/glottalstopsupreversedmod": "\u02E4", + "/yintonemod": "\u02EA", + "/yangtonemod": "\u02EB", + "/voicingmod": "\u02EC", + "/unaspiratedmod": "\u02ED", + "/apostrophedblmod": "\u02EE", + "/arrowheaddownlowmod": "\u02EF", + "/arrowheaduplowmod": "\u02F0", + "/arrowheadleftlowmod": "\u02F1", + "/arrowheadrightlowmod": "\u02F2", + "/ringlowmod": "\u02F3", + "/gravemiddlemod": "\u02F4", + "/gravedblmiddlemod": "\u02F5", + "/acutedblmiddlemod": "\u02F6", + "/tildelowmod": "\u02F7", + "/colonraisedmod": "\u02F8", + "/tonehighbeginmod": "\u02F9", + "/tonehighendmod": "\u02FA", + "/tonelowbeginmod": "\u02FB", + "/tonelowendmod": "\u02FC", + "/shelfmod": "\u02FD", + "/shelfopenmod": "\u02FE", + "/arrowleftlowmod": "\u02FF", + "/hookabovecmb": "\u0309", + "/ringabovecmb": "\u030A", + "/gravedoublecmb": "\u030F", + "/invertedbrevecmb": "\u0311", + "/turnedabovecmb": "\u0313", + "/reversedcommaabovecmb": "\u0314", + "/turnedcommaabovecmb": "\u0315", + "/halfleftringbelowcmb": "\u031C", + "/palatalizedhookbelowcmb": "\u0321", + "/retroflexhookbelowcmb": "\u0322", + "/commaaccentbelowcmb": "\u0326", + "/belowbrevecmb": "\u032E", + "/invertedbelowbrevecmb": "\u032F", + "/lowlinedoublecmb": "\u0333", + "/overlaystrokeshortcmb": "\u0335", + "/overlaystrokelongcmb": "\u0336", + "/halfrightringbelowcmb": "\u0339", + "/invertedbridgebelowcmb": "\u033A", + "/perispomenicmb": "\u0342", + "/iotasubcmb": "\u0345", + "/bridgeabovecmb": "\u0346", + "/equalbelowcmb": "\u0347", + "/dblverticallinebelowcmb": "\u0348", + "/leftanglebelowcmb": "\u0349", + "/nottildeabovecmb": "\u034A", + "/homotheticabovecmb": "\u034B", + "/almostequalabovecmb": "\u034C", + "/arrowleftrightbelowcmb": "\u034D", + "/arrowupbelowcmb": "\u034E", + "/graphemejoinercmb": "\u034F", + "/arrowheadrightabovecmb": "\u0350", + "/halfleftringabovecmb": "\u0351", + "/fermatacmb": "\u0352", + "/xbelowcmb": "\u0353", + "/arrowheadleftbelowcmb": "\u0354", + "/arrowheadrightbelowcmb": "\u0355", + "/arrowheadrightarrowheadupbelowcmb": "\u0356", + "/halfrightringabovecmb": "\u0357", + "/dotrightabovecmb": "\u0358", + "/asteriskbelowcmb": "\u0359", + "/doubleringbelowcmb": "\u035A", + "/zigzagabovecmb": "\u035B", + "/doublebelowbrevecmb": "\u035C", + "/doublebrevecmb": "\u035D", + "/macrondoublecmb": "\u035E", + "/macrondoublebelowcmb": "\u035F", + "/inverteddoublebrevecmb": "\u0361", + "/arrowrightdoublebelowcmb": "\u0362", + "/acmb": "\u0363", + "/ecmb": "\u0364", + "/icmb": "\u0365", + "/ocmb": "\u0366", + "/ucmb": "\u0367", + "/ccmb": "\u0368", + "/dcmb": "\u0369", + "/hcmb": "\u036A", + "/mcmb": "\u036B", + "/rcmb": "\u036C", + "/tcmb": "\u036D", + "/vcmb": "\u036E", + "/xcmb": "\u036F", + "/Heta": "\u0370", + "/heta": "\u0371", + "/Sampiarchaic": "\u0372", + "/sampiarchaic": "\u0373", + "/numeralsign": "\u0374", + "/lownumeralsign": "\u0375", + "/Digammapamphylian": "\u0376", + "/digammapamphylian": "\u0377", + "/iotasub": "\u037A", + "/sigmalunatereversedsymbol": "\u037B", + "/sigmalunatedottedsymbol": "\u037C", + "/sigmalunatedottedreversedsymbol": "\u037D", + "/gr:question": "\u037E", + "/Yot": "\u037F", + "/finalsigma": "\u03C2", + "/Kaisymbol": "\u03CF", + "/betasymbol": "\u03D0", + "/theta.math": "\u03D1", + "/Upsilonacutehooksymbol": "\u03D3", + "/Upsilonadieresishooksymbol": "\u03D4", + "/phi.math": "\u03D5", + "/pi.math": "\u03D6", + "/kaisymbol": "\u03D7", + "/Koppaarchaic": "\u03D8", + "/koppaarchaic": "\u03D9", + "/Stigma": "\u03DA", + "/stigma": "\u03DB", + "/Digamma": "\u03DC", + "/digamma": "\u03DD", + "/Koppa": "\u03DE", + "/koppa": "\u03DF", + "/Sampi": "\u03E0", + "/sampi": "\u03E1", + "/kappa.math": "\u03F0", + "/rhosymbol": "\u03F1", + "/sigmalunatesymbol": "\u03F2", + "/yot": "\u03F3", + "/Thetasymbol": "\u03F4", + "/epsilonlunatesymbol": "\u03F5", + "/epsilonreversedlunatesymbol": "\u03F6", + "/Sho": "\u03F7", + "/sho": "\u03F8", + "/Sigmalunatesymbol": "\u03F9", + "/San": "\u03FA", + "/san": "\u03FB", + "/rhostrokesymbol": "\u03FC", + "/Sigmareversedlunatesymbol": "\u03FD", + "/Sigmalunatesymboldotted": "\u03FE", + "/Reverseddottedsigmalunatesymbol": "\u03FF", + "/Iegravecyr": "\u0400", + "/Iocyr": "\u0401", + "/Djecyr": "\u0402", + "/Gjecyr": "\u0403", + "/Eukrcyr": "\u0404", + "/Dzecyr": "\u0405", + "/Iukrcyr": "\u0406", + "/Yukrcyr": "\u0407", + "/Jecyr": "\u0408", + "/Ljecyr": "\u0409", + "/Njecyr": "\u040A", + "/Tshecyr": "\u040B", + "/Kjecyr": "\u040C", + "/Igravecyr": "\u040D", + "/Ushortcyr": "\u040E", + "/Dzhecyr": "\u040F", + "/Acyr": "\u0410", + "/Becyr": "\u0411", + "/Vecyr": "\u0412", + "/Gecyr": "\u0413", + "/Decyr": "\u0414", + "/Iecyr": "\u0415", + "/Zhecyr": "\u0416", + "/Zecyr": "\u0417", + "/Icyr": "\u0418", + "/Ishortcyr": "\u0419", + "/Kacyr": "\u041A", + "/Elcyr": "\u041B", + "/Emcyr": "\u041C", + "/Encyr": "\u041D", + "/Ocyr": "\u041E", + "/Pecyr": "\u041F", + "/Ercyr": "\u0420", + "/Escyr": "\u0421", + "/Tecyr": "\u0422", + "/Ucyr": "\u0423", + "/Efcyr": "\u0424", + "/Hacyr": "\u0425", + "/Tsecyr": "\u0426", + "/Checyr": "\u0427", + "/Shacyr": "\u0428", + "/Shchacyr": "\u0429", + "/Hardcyr": "\u042A", + "/Ylongcyr": "\u042B", + "/Softcyr": "\u042C", + "/Ereversedcyr": "\u042D", + "/Yucyr": "\u042E", + "/Yacyr": "\u042F", + "/acyr": "\u0430", + "/becyr": "\u0431", + "/vecyr": "\u0432", + "/gecyr": "\u0433", + "/decyr": "\u0434", + "/iecyr": "\u0435", + "/zhecyr": "\u0436", + "/zecyr": "\u0437", + "/icyr": "\u0438", + "/ishortcyr": "\u0439", + "/kacyr": "\u043A", + "/elcyr": "\u043B", + "/emcyr": "\u043C", + "/encyr": "\u043D", + "/ocyr": "\u043E", + "/pecyr": "\u043F", + "/ercyr": "\u0440", + "/escyr": "\u0441", + "/tecyr": "\u0442", + "/ucyr": "\u0443", + "/efcyr": "\u0444", + "/hacyr": "\u0445", + "/tsecyr": "\u0446", + "/checyr": "\u0447", + "/shacyr": "\u0448", + "/shchacyr": "\u0449", + "/hardcyr": "\u044A", + "/ylongcyr": "\u044B", + "/softcyr": "\u044C", + "/ereversedcyr": "\u044D", + "/yucyr": "\u044E", + "/yacyr": "\u044F", + "/iegravecyr": "\u0450", + "/iocyr": "\u0451", + "/djecyr": "\u0452", + "/gjecyr": "\u0453", + "/eukrcyr": "\u0454", + "/dzecyr": "\u0455", + "/iukrcyr": "\u0456", + "/yukrcyr": "\u0457", + "/jecyr": "\u0458", + "/ljecyr": "\u0459", + "/njecyr": "\u045A", + "/tshecyr": "\u045B", + "/kjecyr": "\u045C", + "/igravecyr": "\u045D", + "/ushortcyr": "\u045E", + "/dzhecyr": "\u045F", + "/Omegacyr": "\u0460", + "/omegacyr": "\u0461", + "/Yatcyr": "\u0462", + "/yatcyr": "\u0463", + "/Eiotifiedcyr": "\u0464", + "/eiotifiedcyr": "\u0465", + "/Yuslittlecyr": "\u0466", + "/yuslittlecyr": "\u0467", + "/Yuslittleiotifiedcyr": "\u0468", + "/yuslittleiotifiedcyr": "\u0469", + "/Yusbigcyr": "\u046A", + "/yusbigcyr": "\u046B", + "/Yusbigiotifiedcyr": "\u046C", + "/yusbigiotifiedcyr": "\u046D", + "/Ksicyr": "\u046E", + "/ksicyr": "\u046F", + "/Psicyr": "\u0470", + "/psicyr": "\u0471", + "/Fitacyr": "\u0472", + "/fitacyr": "\u0473", + "/Izhitsacyr": "\u0474", + "/izhitsacyr": "\u0475", + "/Izhitsagravedblcyr": "\u0476", + "/izhitsagravedblcyr": "\u0477", + "/Ukcyr": "\u0478", + "/ukcyr": "\u0479", + "/Omegaroundcyr": "\u047A", + "/omegaroundcyr": "\u047B", + "/Omegatitlocyr": "\u047C", + "/omegatitlocyr": "\u047D", + "/Otcyr": "\u047E", + "/otcyr": "\u047F", + "/Koppacyr": "\u0480", + "/koppacyr": "\u0481", + "/thousandscyr": "\u0482", + "/titlocmbcyr": "\u0483", + "/palatcmbcyr": "\u0484", + "/dasiacmbcyr": "\u0485", + "/psilicmbcyr": "\u0486", + "/pokrytiecmbcyr": "\u0487", + "/hundredthousandscmbcyr": "\u0488", + "/millionscmbcyr": "\u0489", + "/Ishortsharptailcyr": "\u048A", + "/ishortsharptailcyr": "\u048B", + "/Semisoftcyr": "\u048C", + "/semisoftcyr": "\u048D", + "/Ertickcyr": "\u048E", + "/ertickcyr": "\u048F", + "/Geupcyr": "\u0490", + "/geupcyr": "\u0491", + "/Gestrokecyr": "\u0492", + "/gestrokecyr": "\u0493", + "/Gehookcyr": "\u0494", + "/gehookcyr": "\u0495", + "/Zhetailcyr": "\u0496", + "/zhetailcyr": "\u0497", + "/Zetailcyr": "\u0498", + "/zetailcyr": "\u0499", + "/Katailcyr": "\u049A", + "/katailcyr": "\u049B", + "/Kaverticalstrokecyr": "\u049C", + "/kaverticalstrokecyr": "\u049D", + "/Kastrokecyr": "\u049E", + "/kastrokecyr": "\u049F", + "/Kabashkcyr": "\u04A0", + "/kabashkcyr": "\u04A1", + "/Entailcyr": "\u04A2", + "/entailcyr": "\u04A3", + "/Engecyr": "\u04A4", + "/engecyr": "\u04A5", + "/Pehookcyr": "\u04A6", + "/pehookcyr": "\u04A7", + "/Haabkhcyr": "\u04A8", + "/haabkhcyr": "\u04A9", + "/Estailcyr": "\u04AA", + "/estailcyr": "\u04AB", + "/Tetailcyr": "\u04AC", + "/tetailcyr": "\u04AD", + "/Ustraightcyr": "\u04AE", + "/ustraightcyr": "\u04AF", + "/Ustraightstrokecyr": "\u04B0", + "/ustraightstrokecyr": "\u04B1", + "/Xatailcyr": "\u04B2", + "/xatailcyr": "\u04B3", + "/Tetsecyr": "\u04B4", + "/tetsecyr": "\u04B5", + "/Chetailcyr": "\u04B6", + "/chetailcyr": "\u04B7", + "/Chevertcyr": "\u04B8", + "/chevertcyr": "\u04B9", + "/Shhacyr": "\u04BA", + "/shhacyr": "\u04BB", + "/Cheabkhcyr": "\u04BC", + "/cheabkhcyr": "\u04BD", + "/Cheabkhtailcyr": "\u04BE", + "/cheabkhtailcyr": "\u04BF", + "/Palochkacyr": "\u04C0", + "/Zhebrevecyr": "\u04C1", + "/zhebrevecyr": "\u04C2", + "/Kahookcyr": "\u04C3", + "/kahookcyr": "\u04C4", + "/Elsharptailcyr": "\u04C5", + "/elsharptailcyr": "\u04C6", + "/Enhookcyr": "\u04C7", + "/enhookcyr": "\u04C8", + "/Ensharptailcyr": "\u04C9", + "/ensharptailcyr": "\u04CA", + "/Chekhakascyr": "\u04CB", + "/chekhakascyr": "\u04CC", + "/Emsharptailcyr": "\u04CD", + "/emsharptailcyr": "\u04CE", + "/palochkacyr": "\u04CF", + "/Abrevecyr": "\u04D0", + "/abrevecyr": "\u04D1", + "/Adieresiscyr": "\u04D2", + "/adieresiscyr": "\u04D3", + "/Aiecyr": "\u04D4", + "/aiecyr": "\u04D5", + "/Iebrevecyr": "\u04D6", + "/iebrevecyr": "\u04D7", + "/Schwacyr": "\u04D8", + "/schwacyr": "\u04D9", + "/Schwadieresiscyr": "\u04DA", + "/schwadieresiscyr": "\u04DB", + "/Zhedieresiscyr": "\u04DC", + "/zhedieresiscyr": "\u04DD", + "/Zedieresiscyr": "\u04DE", + "/zedieresiscyr": "\u04DF", + "/Dzeabkhcyr": "\u04E0", + "/dzeabkhcyr": "\u04E1", + "/Imacroncyr": "\u04E2", + "/imacroncyr": "\u04E3", + "/Idieresiscyr": "\u04E4", + "/idieresiscyr": "\u04E5", + "/Odieresiscyr": "\u04E6", + "/odieresiscyr": "\u04E7", + "/Obarcyr": "\u04E8", + "/obarcyr": "\u04E9", + "/Obardieresiscyr": "\u04EA", + "/obardieresiscyr": "\u04EB", + "/Ereverseddieresiscyr": "\u04EC", + "/ereverseddieresiscyr": "\u04ED", + "/Umacroncyr": "\u04EE", + "/umacroncyr": "\u04EF", + "/Udieresiscyr": "\u04F0", + "/udieresiscyr": "\u04F1", + "/Uacutedblcyr": "\u04F2", + "/uacutedblcyr": "\u04F3", + "/Chedieresiscyr": "\u04F4", + "/chedieresiscyr": "\u04F5", + "/Getailcyr": "\u04F6", + "/getailcyr": "\u04F7", + "/Ylongdieresiscyr": "\u04F8", + "/ylongdieresiscyr": "\u04F9", + "/Gehookstrokecyr": "\u04FA", + "/gehookstrokecyr": "\u04FB", + "/Hahookcyr": "\u04FC", + "/hahookcyr": "\u04FD", + "/Hastrokecyr": "\u04FE", + "/hastrokecyr": "\u04FF", + "/Dekomicyr": "\u0500", + "/dekomicyr": "\u0501", + "/Djekomicyr": "\u0502", + "/djekomicyr": "\u0503", + "/Zjekomicyr": "\u0504", + "/zjekomicyr": "\u0505", + "/Dzjekomicyr": "\u0506", + "/dzjekomicyr": "\u0507", + "/Ljekomicyr": "\u0508", + "/ljekomicyr": "\u0509", + "/Njekomicyr": "\u050A", + "/njekomicyr": "\u050B", + "/Sjekomicyr": "\u050C", + "/sjekomicyr": "\u050D", + "/Tjekomicyr": "\u050E", + "/tjekomicyr": "\u050F", + "/Reversedzecyr": "\u0510", + "/reversedzecyr": "\u0511", + "/Elhookcyr": "\u0512", + "/elhookcyr": "\u0513", + "/Lhacyr": "\u0514", + "/lhacyr": "\u0515", + "/Rhacyr": "\u0516", + "/rhacyr": "\u0517", + "/Yaecyr": "\u0518", + "/yaecyr": "\u0519", + "/Qacyr": "\u051A", + "/qacyr": "\u051B", + "/Wecyr": "\u051C", + "/wecyr": "\u051D", + "/Kaaleutcyr": "\u051E", + "/kaaleutcyr": "\u051F", + "/Elmiddlehookcyr": "\u0520", + "/elmiddlehookcyr": "\u0521", + "/Enmiddlehookcyr": "\u0522", + "/enmiddlehookcyr": "\u0523", + "/Petailcyr": "\u0524", + "/petailcyr": "\u0525", + "/Shhatailcyr": "\u0526", + "/shhatailcyr": "\u0527", + "/Enhookleftcyr": "\u0528", + "/enhookleftcyr": "\u0529", + "/Dzzhecyr": "\u052A", + "/dzzhecyr": "\u052B", + "/Dchecyr": "\u052C", + "/dchecyr": "\u052D", + "/Eltailcyr": "\u052E", + "/eltailcyr": "\u052F", + "/Aybarmn": "\u0531", + "/Benarmn": "\u0532", + "/Gimarmn": "\u0533", + "/Daarmn": "\u0534", + "/Echarmn": "\u0535", + "/Zaarmn": "\u0536", + "/Eharmn": "\u0537", + "/Etarmn": "\u0538", + "/Toarmn": "\u0539", + "/Zhearmn": "\u053A", + "/Iniarmn": "\u053B", + "/Liwnarmn": "\u053C", + "/Xeharmn": "\u053D", + "/Caarmn": "\u053E", + "/Kenarmn": "\u053F", + "/Hoarmn": "\u0540", + "/Jaarmn": "\u0541", + "/Ghadarmn": "\u0542", + "/Cheharmn": "\u0543", + "/Menarmn": "\u0544", + "/Yiarmn": "\u0545", + "/Nowarmn": "\u0546", + "/Shaarmn": "\u0547", + "/Voarmn": "\u0548", + "/Chaarmn": "\u0549", + "/Peharmn": "\u054A", + "/Jheharmn": "\u054B", + "/Raarmn": "\u054C", + "/Seharmn": "\u054D", + "/Vewarmn": "\u054E", + "/Tiwnarmn": "\u054F", + "/Reharmn": "\u0550", + "/Coarmn": "\u0551", + "/Yiwnarmn": "\u0552", + "/Piwrarmn": "\u0553", + "/Keharmn": "\u0554", + "/Oharmn": "\u0555", + "/Feharmn": "\u0556", + "/ringhalfleftarmn": "\u0559", + "/apostrophearmn": "\u055A", + "/emphasismarkarmn": "\u055B", + "/exclamarmn": "\u055C", + "/commaarmn": "\u055D", + "/questionarmn": "\u055E", + "/abbreviationmarkarmn": "\u055F", + "/turnedaybarmn": "\u0560", + "/aybarmn": "\u0561", + "/benarmn": "\u0562", + "/gimarmn": "\u0563", + "/daarmn": "\u0564", + "/echarmn": "\u0565", + "/zaarmn": "\u0566", + "/eharmn": "\u0567", + "/etarmn": "\u0568", + "/toarmn": "\u0569", + "/zhearmn": "\u056A", + "/iniarmn": "\u056B", + "/liwnarmn": "\u056C", + "/xeharmn": "\u056D", + "/caarmn": "\u056E", + "/kenarmn": "\u056F", + "/hoarmn": "\u0570", + "/jaarmn": "\u0571", + "/ghadarmn": "\u0572", + "/cheharmn": "\u0573", + "/menarmn": "\u0574", + "/yiarmn": "\u0575", + "/nowarmn": "\u0576", + "/shaarmn": "\u0577", + "/voarmn": "\u0578", + "/chaarmn": "\u0579", + "/peharmn": "\u057A", + "/jheharmn": "\u057B", + "/raarmn": "\u057C", + "/seharmn": "\u057D", + "/vewarmn": "\u057E", + "/tiwnarmn": "\u057F", + "/reharmn": "\u0580", + "/coarmn": "\u0581", + "/yiwnarmn": "\u0582", + "/piwrarmn": "\u0583", + "/keharmn": "\u0584", + "/oharmn": "\u0585", + "/feharmn": "\u0586", + "/ech_yiwnarmn": "\u0587", + "/yiwithstrokearmn": "\u0588", + "/periodarmn": "\u0589", + "/hyphenarmn": "\u058A", + "/rightfacingeternitysignarmn": "\u058D", + "/leftfacingeternitysignarmn": "\u058E", + "/dramsignarmn": "\u058F", + "/etnahta:hb": "\u0591", + "/segolta:hb": "\u0592", + "/shalshelet:hb": "\u0593", + "/zaqefQatan:hb": "\u0594", + "/zaqefGadol:hb": "\u0595", + "/tifcha:hb": "\u0596", + "/revia:hb": "\u0597", + "/zarqa:hb": "\u0598", + "/pashta:hb": "\u0599", + "/yetiv:hb": "\u059A", + "/tevir:hb": "\u059B", + "/azla:hb": "\u059C", + "/gereshMuqdam:hb": "\u059D", + "/SheneGerishin:hb": "\u059E", + "/qarneFarah:hb": "\u059F", + "/telishaGedolah:hb": "\u05A0", + "/pazer:hb": "\u05A1", + "/atnachHafukh:hb": "\u05A2", + "/munach:hb": "\u05A3", + "/mahpach:hb": "\u05A4", + "/mercha:hb": "\u05A5", + "/merchaKefulah:hb": "\u05A6", + "/darga:hb": "\u05A7", + "/qadma:hb": "\u05A8", + "/telishaQetannah:hb": "\u05A9", + "/yerachBenYomo:hb": "\u05AA", + "/ole:hb": "\u05AB", + "/iluy:hb": "\u05AC", + "/dehi:hb": "\u05AD", + "/tsinnorit:hb": "\u05AE", + "/masoraCircle:hb": "\u05AF", + "/sheva:hb": "\u05B0", + "/hatafSegol:hb": "\u05B1", + "/hatafPatah:hb": "\u05B2", + "/hatafQamats:hb": "\u05B3", + "/hiriq:hb": "\u05B4", + "/tsere:hb": "\u05B5", + "/segol:hb": "\u05B6", + "/patah:hb": "\u05B7", + "/qamats:hb": "\u05B8", + "/holam:hb": "\u05B9", + "/holamHaser:hb": "\u05BA", + "/qubuts:hb": "\u05BB", + "/dagesh:hb": "\u05BC", + "/meteg:hb": "\u05BD", + "/maqaf:hb": "\u05BE", + "/rafe:hb": "\u05BF", + "/paseq:hb": "\u05C0", + "/shinDot:hb": "\u05C1", + "/sinDot:hb": "\u05C2", + "/sofPasuq:hb": "\u05C3", + "/dotupper:hb": "\u05C4", + "/dotlower:hb": "\u05C5", + "/nunHafukha:hb": "\u05C6", + "/qamatsQatan:hb": "\u05C7", + "/alef:hb": "\u05D0", + "/bet:hb": "\u05D1", + "/gimel:hb": "\u05D2", + "/dalet:hb": "\u05D3", + "/he:hb": "\u05D4", + "/vav:hb": "\u05D5", + "/zayin:hb": "\u05D6", + "/het:hb": "\u05D7", + "/tet:hb": "\u05D8", + "/yod:hb": "\u05D9", + "/finalkaf:hb": "\u05DA", + "/kaf:hb": "\u05DB", + "/lamed:hb": "\u05DC", + "/finalmem:hb": "\u05DD", + "/mem:hb": "\u05DE", + "/finalnun:hb": "\u05DF", + "/nun:hb": "\u05E0", + "/samekh:hb": "\u05E1", + "/ayin:hb": "\u05E2", + "/finalpe:hb": "\u05E3", + "/pe:hb": "\u05E4", + "/finaltsadi:hb": "\u05E5", + "/tsadi:hb": "\u05E6", + "/qof:hb": "\u05E7", + "/resh:hb": "\u05E8", + "/shin:hb": "\u05E9", + "/tav:hb": "\u05EA", + "/yodtriangle:hb": "\u05EF", + "/vav_vav:hb": "\u05F0", + "/vav_yod:hb": "\u05F1", + "/yod_yod:hb": "\u05F2", + "/geresh:hb": "\u05F3", + "/gershayim:hb": "\u05F4", + "/arnumbersign": "\u0600", + "/Sanah": "\u0601", + "/footnote": "\u0602", + "/Safha": "\u0603", + "/samvat": "\u0604", + "/numbermarkabove": "\u0605", + "/arcuberoot": "\u0606", + "/arfourthroot": "\u0607", + "/ray": "\u0608", + "/permille": "\u0609", + "/arperthousand": "\u060A", + "/afghani": "\u060B", + "/arcomma": "\u060C", + "/dateseparator": "\u060D", + "/poeticverse": "\u060E", + "/misra": "\u060F", + "/HonSAW": "\u0610", + "/HonAA": "\u0611", + "/HonRA": "\u0612", + "/aHonRAA": "\u0613", + "/takhallus": "\u0614", + "/tahabove": "\u0615", + "/alefLamYehabove": "\u0616", + "/zainabove": "\u0617", + "/fathasmall": "\u0618", + "/dammasmall": "\u0619", + "/kasrasmall": "\u061A", + "/arsemicolon": "\u061B", + "/mark": "\u061C", + "/endoftextmark": "\u061D", + "/tripledot": "\u061E", + "/arquestion": "\u061F", + "/kashmiriyeh": "\u0620", + "/hamza": "\u0621", + "/alefmadda": "\u0622", + "/alefhamza": "\u0623", + "/wawhamza": "\u0624", + "/alefhamzabelow": "\u0625", + "/yehhamza": "\u0626", + "/beh": "\u0628", + "/tehmarbuta": "\u0629", + "/teh": "\u062A", + "/theh": "\u062B", + "/jeem": "\u062C", + "/hah": "\u062D", + "/khah": "\u062E", + "/dal": "\u062F", + "/thal": "\u0630", + "/reh": "\u0631", + "/zain": "\u0632", + "/seen": "\u0633", + "/sheen": "\u0634", + "/sad": "\u0635", + "/dad": "\u0636", + "/tah": "\u0637", + "/zah": "\u0638", + "/ain": "\u0639", + "/ghain": "\u063A", + "/kehehtwodotsabove": "\u063B", + "/kehehthreedotsbelow": "\u063C", + "/yehfarsiinvertedV": "\u063D", + "/yehfarsitwodotsabove": "\u063E", + "/yehfarsithreedotsabove": "\u063F", + "/kashida": "\u0640", + "/feh": "\u0641", + "/qaf": "\u0642", + "/lam": "\u0644", + "/meem": "\u0645", + "/noon": "\u0646", + "/heh": "\u0647", + "/waw": "\u0648", + "/alefmaksura": "\u0649", + "/yeh": "\u064A", + "/fathatan": "\u064B", + "/dammatan": "\u064C", + "/kasratan": "\u064D", + "/fatha": "\u064E", + "/damma": "\u064F", + "/kasra": "\u0650", + "/shadda": "\u0651", + "/sukun": "\u0652", + "/madda": "\u0653", + "/hamzaabove": "\u0654", + "/hamzabelow": "\u0655", + "/subscriptalef": "\u0656", + "/inverteddamma": "\u0657", + "/marknoonghunna": "\u0658", + "/zwarakay": "\u0659", + "/vowelVabove": "\u065A", + "/vowelinvertedVabove": "\u065B", + "/voweldotbelow": "\u065C", + "/dammareversed": "\u065D", + "/fathatwodotsdots": "\u065E", + "/wavyhamzabelow": "\u065F", + "/arzero": "\u0660", + "/arone": "\u0661", + "/artwo": "\u0662", + "/arthree": "\u0663", + "/arfour": "\u0664", + "/arfive": "\u0665", + "/arsix": "\u0666", + "/arseven": "\u0667", + "/areight": "\u0668", + "/arnine": "\u0669", + "/arpercent": "\u066A", + "/ardecimalseparator": "\u066B", + "/thousandsseparator": "\u066C", + "/fivepointedstar": "\u066D", + "/dotlessbeh": "\u066E", + "/dotlessqaf": "\u066F", + "/alefabove": "\u0670", + "/alefwasla": "\u0671", + "/alefwavyhamza": "\u0672", + "/alefwavyhamzabelow": "\u0673", + "/highhamza": "\u0674", + "/alefhighhamza": "\u0675", + "/wawhighhamza": "\u0676", + "/uhamza": "\u0677", + "/yehhighhamza": "\u0678", + "/tteh": "\u0679", + "/tteheh": "\u067A", + "/beeh": "\u067B", + "/tehring": "\u067C", + "/tehdownthreedotsabove": "\u067D", + "/peh": "\u067E", + "/teheh": "\u067F", + "/beheh": "\u0680", + "/hahhamza": "\u0681", + "/hahtwodotsvertical": "\u0682", + "/nyeh": "\u0683", + "/dyeh": "\u0684", + "/hahthreedotsabove": "\u0685", + "/tcheh": "\u0686", + "/tcheheh": "\u0687", + "/ddal": "\u0688", + "/dalring": "\u0689", + "/daldotbelow": "\u068A", + "/daldotbelowtahsmall": "\u068B", + "/dahal": "\u068C", + "/ddahal": "\u068D", + "/dul": "\u068E", + "/daldownthreedotsabove": "\u068F", + "/dalfourdotsabove": "\u0690", + "/rreh": "\u0691", + "/rehVabove": "\u0692", + "/rehring": "\u0693", + "/rehdotbelow": "\u0694", + "/rehVbelow": "\u0695", + "/rehdotbelowdotabove": "\u0696", + "/rehtwodotsabove": "\u0697", + "/jeh": "\u0698", + "/rehfourdotsabove": "\u0699", + "/seendotbelowdotabove": "\u069A", + "/seenthreedotsbelow": "\u069B", + "/seenthreedotsbelowthreedotsabove": "\u069C", + "/sadtwodotsbelow": "\u069D", + "/sadthreedotsabove": "\u069E", + "/tahthreedotsabove": "\u069F", + "/ainthreedotsabove": "\u06A0", + "/dotlessfeh": "\u06A1", + "/fehdotbelowright": "\u06A2", + "/fehdotbelow": "\u06A3", + "/veh": "\u06A4", + "/fehthreedotsbelow": "\u06A5", + "/peheh": "\u06A6", + "/qafdotabove": "\u06A7", + "/qafthreedotsabove": "\u06A8", + "/keheh": "\u06A9", + "/kafswash": "\u06AA", + "/kafring": "\u06AB", + "/kafdotabove": "\u06AC", + "/ng": "\u06AD", + "/kafthreedotsbelow": "\u06AE", + "/gaf": "\u06AF", + "/gafring": "\u06B0", + "/ngoeh": "\u06B1", + "/gaftwodotsbelow": "\u06B2", + "/gueh": "\u06B3", + "/gafthreedotsabove": "\u06B4", + "/lamVabove": "\u06B5", + "/lamdotabove": "\u06B6", + "/lamthreedotsabove": "\u06B7", + "/lamthreedotsbelow": "\u06B8", + "/noondotbelow": "\u06B9", + "/noonghunna": "\u06BA", + "/rnoon": "\u06BB", + "/noonring": "\u06BC", + "/noonthreedotsabove": "\u06BD", + "/hehdoachashmee": "\u06BE", + "/tchehdotabove": "\u06BF", + "/hehyeh": "\u06C0", + "/hehgoal": "\u06C1", + "/hehgoalhamza": "\u06C2", + "/tehmarbutagoal": "\u06C3", + "/wawring": "\u06C4", + "/oekirghiz": "\u06C5", + "/aroe": "\u06C6", + "/aru": "\u06C7", + "/yu": "\u06C8", + "/yukirghiz": "\u06C9", + "/wawtwodotsabove": "\u06CA", + "/ve": "\u06CB", + "/yehfarsi": "\u06CC", + "/yehtail": "\u06CD", + "/yehVabove": "\u06CE", + "/wawdotabove": "\u06CF", + "/are": "\u06D0", + "/yehthreedotsbelow": "\u06D1", + "/yehbarree": "\u06D2", + "/yehbarreehamza": "\u06D3", + "/periodurdu": "\u06D4", + "/arae": "\u06D5", + "/sad_lam_alefmaksuraabove": "\u06D6", + "/qaf_lam_alefmaksuraabove": "\u06D7", + "/meemabove.init": "\u06D8", + "/lamalefabove": "\u06D9", + "/jeemabove": "\u06DA", + "/threedotsaboveabove": "\u06DB", + "/seenabove": "\u06DC", + "/Ayahend": "\u06DD", + "/RubElHizbstart": "\u06DE", + "/roundedzeroabove": "\u06DF", + "/zerosquareabove": "\u06E0", + "/dotlesskhahabove": "\u06E1", + "/meemabove": "\u06E2", + "/seenlow": "\u06E3", + "/maddaabove": "\u06E4", + "/wawsmall": "\u06E5", + "/yehsmall": "\u06E6", + "/yehabove": "\u06E7", + "/noonabove": "\u06E8", + "/Sajdah": "\u06E9", + "/stopbelow": "\u06EA", + "/stopabove": "\u06EB", + "/filledstopabove": "\u06EC", + "/meembelow": "\u06ED", + "/dalinvertedV": "\u06EE", + "/rehinvertedV": "\u06EF", + "/perzero": "\u06F0", + "/perone": "\u06F1", + "/pertwo": "\u06F2", + "/perthree": "\u06F3", + "/perfour": "\u06F4", + "/perfive": "\u06F5", + "/persix": "\u06F6", + "/perseven": "\u06F7", + "/pereight": "\u06F8", + "/pernine": "\u06F9", + "/sheendotbelow": "\u06FA", + "/daddotbelow": "\u06FB", + "/ghaindotbelow": "\u06FC", + "/ampersandSindhi": "\u06FD", + "/menpostSindhi": "\u06FE", + "/hehinvertedV": "\u06FF", + "/behThreeDotsHorizontallyBelow": "\u0750", + "/behDotBelowThreeDotsAbove": "\u0751", + "/behThreeDotsUpBelow": "\u0752", + "/behThreeDotsUpBelowTwoDotsAbove": "\u0753", + "/behTwoDotsBelowDotAbove": "\u0754", + "/behInvertedSmallVBelow": "\u0755", + "/behSmallV": "\u0756", + "/hahTwoDotsAbove": "\u0757", + "/hahThreeDotsUpBelow": "\u0758", + "/dalTwoDotsVerticallyBelowSmallTah": "\u0759", + "/dalInvertedSmallVBelow": "\u075A", + "/rehStroke": "\u075B", + "/seenFourDotsAbove": "\u075C", + "/ainTwoDotsAbove": "\u075D", + "/ainThreeDotsDownAbove": "\u075E", + "/ainTwoDotsVerticallyAbove": "\u075F", + "/fehTwoDotsBelow": "\u0760", + "/fehThreeDotsUpBelow": "\u0761", + "/kehehDotAbove": "\u0762", + "/kehehThreeDotsAbove": "\u0763", + "/kehehThreeDotsUpBelow": "\u0764", + "/meemDotAbove": "\u0765", + "/meemDotBelow": "\u0766", + "/noonTwoDotsBelow": "\u0767", + "/noonSmallTah": "\u0768", + "/noonSmallV": "\u0769", + "/lamBar": "\u076A", + "/rehTwoDotsVerticallyAbove": "\u076B", + "/rehHamzaAbove": "\u076C", + "/seenTwoDotsVerticallyAbove": "\u076D", + "/hahSmallTahBelow": "\u076E", + "/hahSmallTahTwoDots": "\u076F", + "/seenSmallTahTwoDots": "\u0770", + "/rehSmallTahTwoDots": "\u0771", + "/hahSmallTahAbove": "\u0772", + "/alefDigitTwoAbove": "\u0773", + "/alefDigitThreeAbove": "\u0774", + "/farsiYehDigitTwoAbove": "\u0775", + "/farsiYehDigitThreeAbove": "\u0776", + "/farsiYehDigitFourBelow": "\u0777", + "/wawDigitTwoAbove": "\u0778", + "/wawDigitThreeAbove": "\u0779", + "/yehBarreeDigitTwoAbove": "\u077A", + "/yehBarreeDigitThreeAbove": "\u077B", + "/hahDigitFourBelow": "\u077C", + "/seenDigitFourAbove": "\u077D", + "/seenInvertedV": "\u077E", + "/kafTwoDotsAbove": "\u077F", + "/deva:candrabinduinverted": "\u0900", + "/deva:candrabindu": "\u0901", + "/deva:anusvara": "\u0902", + "/deva:visarga": "\u0903", + "/deva:ashort": "\u0904", + "/deva:a": "\u0905", + "/deva:aa": "\u0906", + "/deva:i": "\u0907", + "/deva:ii": "\u0908", + "/deva:u": "\u0909", + "/deva:uu": "\u090A", + "/deva:vocalicr": "\u090B", + "/deva:vocalicl": "\u090C", + "/deva:ecandra": "\u090D", + "/deva:eshort": "\u090E", + "/deva:e": "\u090F", + "/deva:ai": "\u0910", + "/deva:ocandra": "\u0911", + "/deva:oshort": "\u0912", + "/deva:o": "\u0913", + "/deva:au": "\u0914", + "/deva:ka": "\u0915", + "/deva:kha": "\u0916", + "/deva:ga": "\u0917", + "/deva:gha": "\u0918", + "/deva:nga": "\u0919", + "/deva:ca": "\u091A", + "/deva:cha": "\u091B", + "/deva:ja": "\u091C", + "/deva:jha": "\u091D", + "/deva:nya": "\u091E", + "/deva:tta": "\u091F", + "/deva:ttha": "\u0920", + "/deva:dda": "\u0921", + "/deva:ddha": "\u0922", + "/deva:nna": "\u0923", + "/deva:ta": "\u0924", + "/deva:tha": "\u0925", + "/deva:da": "\u0926", + "/deva:dha": "\u0927", + "/deva:na": "\u0928", + "/deva:nnna": "\u0929", + "/deva:pa": "\u092A", + "/deva:pha": "\u092B", + "/deva:ba": "\u092C", + "/deva:bha": "\u092D", + "/deva:ma": "\u092E", + "/deva:ya": "\u092F", + "/deva:ra": "\u0930", + "/deva:rra": "\u0931", + "/deva:la": "\u0932", + "/deva:lla": "\u0933", + "/deva:llla": "\u0934", + "/deva:va": "\u0935", + "/deva:sha": "\u0936", + "/deva:ssa": "\u0937", + "/deva:sa": "\u0938", + "/deva:ha": "\u0939", + "/deva:oesign": "\u093A", + "/deva:ooesign": "\u093B", + "/deva:nukta": "\u093C", + "/deva:avagraha": "\u093D", + "/deva:aasign": "\u093E", + "/deva:isign": "\u093F", + "/deva:iisign": "\u0940", + "/deva:usign": "\u0941", + "/deva:uusign": "\u0942", + "/deva:vocalicrsign": "\u0943", + "/deva:vocalicrrsign": "\u0944", + "/deva:esigncandra": "\u0945", + "/deva:esignshort": "\u0946", + "/deva:esign": "\u0947", + "/deva:aisign": "\u0948", + "/deva:osigncandra": "\u0949", + "/deva:osignshort": "\u094A", + "/deva:osign": "\u094B", + "/deva:ausign": "\u094C", + "/deva:virama": "\u094D", + "/deva:esignprishthamatra": "\u094E", + "/deva:awsign": "\u094F", + "/deva:om": "\u0950", + "/deva:udatta": "\u0951", + "/deva:anudatta": "\u0952", + "/deva:grave": "\u0953", + "/deva:acute": "\u0954", + "/deva:esigncandralong": "\u0955", + "/deva:uesign": "\u0956", + "/deva:uuesign": "\u0957", + "/deva:qa": "\u0958", + "/deva:khha": "\u0959", + "/deva:ghha": "\u095A", + "/deva:za": "\u095B", + "/deva:dddha": "\u095C", + "/deva:rha": "\u095D", + "/deva:fa": "\u095E", + "/deva:yya": "\u095F", + "/deva:vocalicrr": "\u0960", + "/deva:vocalicll": "\u0961", + "/deva:vocaliclsign": "\u0962", + "/deva:vocalicllsign": "\u0963", + "/deva:danda": "\u0964", + "/deva:dbldanda": "\u0965", + "/deva:zero": "\u0966", + "/deva:one": "\u0967", + "/deva:two": "\u0968", + "/deva:three": "\u0969", + "/deva:four": "\u096A", + "/deva:five": "\u096B", + "/deva:six": "\u096C", + "/deva:seven": "\u096D", + "/deva:eight": "\u096E", + "/deva:nine": "\u096F", + "/deva:abbreviation": "\u0970", + "/deva:dothigh": "\u0971", + "/deva:acandra": "\u0972", + "/deva:oe": "\u0973", + "/deva:ooe": "\u0974", + "/deva:aw": "\u0975", + "/deva:ue": "\u0976", + "/deva:uue": "\u0977", + "/deva:marwaridda": "\u0978", + "/deva:zha": "\u0979", + "/deva:yaheavy": "\u097A", + "/deva:gga": "\u097B", + "/deva:jja": "\u097C", + "/deva:glottalstop": "\u097D", + "/deva:ddda": "\u097E", + "/deva:bba": "\u097F", + "/beng:anji": "\u0980", + "/beng:candrabindu": "\u0981", + "/beng:anusvara": "\u0982", + "/beng:visarga": "\u0983", + "/beng:a": "\u0985", + "/beng:aa": "\u0986", + "/beng:i": "\u0987", + "/beng:ii": "\u0988", + "/beng:u": "\u0989", + "/beng:uu": "\u098A", + "/beng:vocalicr": "\u098B", + "/beng:vocalicl": "\u098C", + "/beng:e": "\u098F", + "/beng:ai": "\u0990", + "/beng:o": "\u0993", + "/beng:au": "\u0994", + "/beng:ka": "\u0995", + "/beng:kha": "\u0996", + "/beng:ga": "\u0997", + "/beng:gha": "\u0998", + "/beng:nga": "\u0999", + "/beng:ca": "\u099A", + "/beng:cha": "\u099B", + "/beng:ja": "\u099C", + "/beng:jha": "\u099D", + "/beng:nya": "\u099E", + "/beng:tta": "\u099F", + "/beng:ttha": "\u09A0", + "/beng:dda": "\u09A1", + "/beng:ddha": "\u09A2", + "/beng:nna": "\u09A3", + "/beng:ta": "\u09A4", + "/beng:tha": "\u09A5", + "/beng:da": "\u09A6", + "/beng:dha": "\u09A7", + "/beng:na": "\u09A8", + "/beng:pa": "\u09AA", + "/beng:pha": "\u09AB", + "/beng:ba": "\u09AC", + "/beng:bha": "\u09AD", + "/beng:ma": "\u09AE", + "/beng:ya": "\u09AF", + "/beng:ra": "\u09B0", + "/beng:la": "\u09B2", + "/beng:sha": "\u09B6", + "/beng:ssa": "\u09B7", + "/beng:sa": "\u09B8", + "/beng:ha": "\u09B9", + "/beng:nukta": "\u09BC", + "/beng:avagraha": "\u09BD", + "/beng:aasign": "\u09BE", + "/beng:isign": "\u09BF", + "/beng:iisign": "\u09C0", + "/beng:usign": "\u09C1", + "/beng:uusign": "\u09C2", + "/beng:vocalicrsign": "\u09C3", + "/beng:vocalicrrsign": "\u09C4", + "/beng:esign": "\u09C7", + "/beng:aisign": "\u09C8", + "/beng:osign": "\u09CB", + "/beng:ausign": "\u09CC", + "/beng:virama": "\u09CD", + "/beng:khandata": "\u09CE", + "/beng:aulengthmark": "\u09D7", + "/beng:rra": "\u09DC", + "/beng:rha": "\u09DD", + "/beng:yya": "\u09DF", + "/beng:vocalicrr": "\u09E0", + "/beng:vocalicll": "\u09E1", + "/beng:vocaliclsign": "\u09E2", + "/beng:vocalicllsign": "\u09E3", + "/beng:zero": "\u09E6", + "/beng:one": "\u09E7", + "/beng:two": "\u09E8", + "/beng:three": "\u09E9", + "/beng:four": "\u09EA", + "/beng:five": "\u09EB", + "/beng:six": "\u09EC", + "/beng:seven": "\u09ED", + "/beng:eight": "\u09EE", + "/beng:nine": "\u09EF", + "/beng:ramiddiagonal": "\u09F0", + "/beng:ralowdiagonal": "\u09F1", + "/beng:rupeemark": "\u09F2", + "/beng:rupee": "\u09F3", + "/beng:onecurrencynumerator": "\u09F4", + "/beng:twocurrencynumerator": "\u09F5", + "/beng:threecurrencynumerator": "\u09F6", + "/beng:fourcurrencynumerator": "\u09F7", + "/beng:currencyoneless": "\u09F8", + "/beng:sixteencurrencydenominator": "\u09F9", + "/beng:isshar": "\u09FA", + "/beng:gandamark": "\u09FB", + "/beng:vedicanusvara": "\u09FC", + "/beng:abbreviationsign": "\u09FD", + "/beng:sandhimark": "\u09FE", + "/guru:adakbindisign": "\u0A01", + "/guru:bindisign": "\u0A02", + "/guru:visarga": "\u0A03", + "/guru:a": "\u0A05", + "/guru:aa": "\u0A06", + "/guru:i": "\u0A07", + "/guru:ii": "\u0A08", + "/guru:u": "\u0A09", + "/guru:uu": "\u0A0A", + "/guru:ee": "\u0A0F", + "/guru:ai": "\u0A10", + "/guru:oo": "\u0A13", + "/guru:au": "\u0A14", + "/guru:ka": "\u0A15", + "/guru:kha": "\u0A16", + "/guru:ga": "\u0A17", + "/guru:gha": "\u0A18", + "/guru:nga": "\u0A19", + "/guru:ca": "\u0A1A", + "/guru:cha": "\u0A1B", + "/guru:ja": "\u0A1C", + "/guru:jha": "\u0A1D", + "/guru:nya": "\u0A1E", + "/guru:tta": "\u0A1F", + "/guru:ttha": "\u0A20", + "/guru:dda": "\u0A21", + "/guru:ddha": "\u0A22", + "/guru:nna": "\u0A23", + "/guru:ta": "\u0A24", + "/guru:tha": "\u0A25", + "/guru:da": "\u0A26", + "/guru:dha": "\u0A27", + "/guru:na": "\u0A28", + "/guru:pa": "\u0A2A", + "/guru:pha": "\u0A2B", + "/guru:ba": "\u0A2C", + "/guru:bha": "\u0A2D", + "/guru:ma": "\u0A2E", + "/guru:ya": "\u0A2F", + "/guru:ra": "\u0A30", + "/guru:la": "\u0A32", + "/guru:lla": "\u0A33", + "/guru:va": "\u0A35", + "/guru:sha": "\u0A36", + "/guru:sa": "\u0A38", + "/guru:ha": "\u0A39", + "/guru:nukta": "\u0A3C", + "/guru:aasign": "\u0A3E", + "/guru:isign": "\u0A3F", + "/guru:iisign": "\u0A40", + "/guru:usign": "\u0A41", + "/guru:uusign": "\u0A42", + "/guru:eesign": "\u0A47", + "/guru:aisign": "\u0A48", + "/guru:oosign": "\u0A4B", + "/guru:ausign": "\u0A4C", + "/guru:virama": "\u0A4D", + "/guru:udaatsign": "\u0A51", + "/guru:khha": "\u0A59", + "/guru:ghha": "\u0A5A", + "/guru:za": "\u0A5B", + "/guru:rra": "\u0A5C", + "/guru:fa": "\u0A5E", + "/guru:zero": "\u0A66", + "/guru:one": "\u0A67", + "/guru:two": "\u0A68", + "/guru:three": "\u0A69", + "/guru:four": "\u0A6A", + "/guru:five": "\u0A6B", + "/guru:six": "\u0A6C", + "/guru:seven": "\u0A6D", + "/guru:eight": "\u0A6E", + "/guru:nine": "\u0A6F", + "/guru:tippi": "\u0A70", + "/guru:addak": "\u0A71", + "/guru:iri": "\u0A72", + "/guru:ura": "\u0A73", + "/guru:ekonkar": "\u0A74", + "/guru:yakashsign": "\u0A75", + "/guru:abbreviationsign": "\u0A76", + "/gujr:candrabindu": "\u0A81", + "/gujr:anusvara": "\u0A82", + "/gujr:visarga": "\u0A83", + "/gujr:a": "\u0A85", + "/gujr:aa": "\u0A86", + "/gujr:i": "\u0A87", + "/gujr:ii": "\u0A88", + "/gujr:u": "\u0A89", + "/gujr:uu": "\u0A8A", + "/gujr:vocalicr": "\u0A8B", + "/gujr:vocalicl": "\u0A8C", + "/gujr:ecandra": "\u0A8D", + "/gujr:e": "\u0A8F", + "/gujr:ai": "\u0A90", + "/gujr:ocandra": "\u0A91", + "/gujr:o": "\u0A93", + "/gujr:au": "\u0A94", + "/gujr:ka": "\u0A95", + "/gujr:kha": "\u0A96", + "/gujr:ga": "\u0A97", + "/gujr:gha": "\u0A98", + "/gujr:nga": "\u0A99", + "/gujr:ca": "\u0A9A", + "/gujr:cha": "\u0A9B", + "/gujr:ja": "\u0A9C", + "/gujr:jha": "\u0A9D", + "/gujr:nya": "\u0A9E", + "/gujr:tta": "\u0A9F", + "/gujr:ttha": "\u0AA0", + "/gujr:dda": "\u0AA1", + "/gujr:ddha": "\u0AA2", + "/gujr:nna": "\u0AA3", + "/gujr:ta": "\u0AA4", + "/gujr:tha": "\u0AA5", + "/gujr:da": "\u0AA6", + "/gujr:dha": "\u0AA7", + "/gujr:na": "\u0AA8", + "/gujr:pa": "\u0AAA", + "/gujr:pha": "\u0AAB", + "/gujr:ba": "\u0AAC", + "/gujr:bha": "\u0AAD", + "/gujr:ma": "\u0AAE", + "/gujr:ya": "\u0AAF", + "/gujr:ra": "\u0AB0", + "/gujr:la": "\u0AB2", + "/gujr:lla": "\u0AB3", + "/gujr:va": "\u0AB5", + "/gujr:sha": "\u0AB6", + "/gujr:ssa": "\u0AB7", + "/gujr:sa": "\u0AB8", + "/gujr:ha": "\u0AB9", + "/gujr:nukta": "\u0ABC", + "/gujr:avagraha": "\u0ABD", + "/gujr:aasign": "\u0ABE", + "/gujr:isign": "\u0ABF", + "/gujr:iisign": "\u0AC0", + "/gujr:usign": "\u0AC1", + "/gujr:uusign": "\u0AC2", + "/gujr:vocalicrsign": "\u0AC3", + "/gujr:vocalicrrsign": "\u0AC4", + "/gujr:esigncandra": "\u0AC5", + "/gujr:esign": "\u0AC7", + "/gujr:aisign": "\u0AC8", + "/gujr:osigncandra": "\u0AC9", + "/gujr:osign": "\u0ACB", + "/gujr:ausign": "\u0ACC", + "/gujr:virama": "\u0ACD", + "/gujr:om": "\u0AD0", + "/gujr:vocalicrr": "\u0AE0", + "/gujr:vocalicll": "\u0AE1", + "/gujr:vocaliclsign": "\u0AE2", + "/gujr:vocalicllsign": "\u0AE3", + "/gujr:zero": "\u0AE6", + "/gujr:one": "\u0AE7", + "/gujr:two": "\u0AE8", + "/gujr:three": "\u0AE9", + "/gujr:four": "\u0AEA", + "/gujr:five": "\u0AEB", + "/gujr:six": "\u0AEC", + "/gujr:seven": "\u0AED", + "/gujr:eight": "\u0AEE", + "/gujr:nine": "\u0AEF", + "/gujr:abbreviation": "\u0AF0", + "/gujr:rupee": "\u0AF1", + "/gujr:zha": "\u0AF9", + "/gujr:sukun": "\u0AFA", + "/gujr:shadda": "\u0AFB", + "/gujr:maddah": "\u0AFC", + "/gujr:threedotnuktaabove": "\u0AFD", + "/gujr:circlenuktaabove": "\u0AFE", + "/gujr:twocirclenuktaabove": "\u0AFF", + "/orya:candrabindu": "\u0B01", + "/orya:anusvara": "\u0B02", + "/orya:visarga": "\u0B03", + "/orya:a": "\u0B05", + "/orya:aa": "\u0B06", + "/orya:i": "\u0B07", + "/orya:ii": "\u0B08", + "/orya:u": "\u0B09", + "/orya:uu": "\u0B0A", + "/orya:vocalicr": "\u0B0B", + "/orya:vocalicl": "\u0B0C", + "/orya:e": "\u0B0F", + "/orya:ai": "\u0B10", + "/orya:o": "\u0B13", + "/orya:au": "\u0B14", + "/orya:ka": "\u0B15", + "/orya:kha": "\u0B16", + "/orya:ga": "\u0B17", + "/orya:gha": "\u0B18", + "/orya:nga": "\u0B19", + "/orya:ca": "\u0B1A", + "/orya:cha": "\u0B1B", + "/orya:ja": "\u0B1C", + "/orya:jha": "\u0B1D", + "/orya:nya": "\u0B1E", + "/orya:tta": "\u0B1F", + "/orya:ttha": "\u0B20", + "/orya:dda": "\u0B21", + "/orya:ddha": "\u0B22", + "/orya:nna": "\u0B23", + "/orya:ta": "\u0B24", + "/orya:tha": "\u0B25", + "/orya:da": "\u0B26", + "/orya:dha": "\u0B27", + "/orya:na": "\u0B28", + "/orya:pa": "\u0B2A", + "/orya:pha": "\u0B2B", + "/orya:ba": "\u0B2C", + "/orya:bha": "\u0B2D", + "/orya:ma": "\u0B2E", + "/orya:ya": "\u0B2F", + "/orya:ra": "\u0B30", + "/orya:la": "\u0B32", + "/orya:lla": "\u0B33", + "/orya:va": "\u0B35", + "/orya:sha": "\u0B36", + "/orya:ssa": "\u0B37", + "/orya:sa": "\u0B38", + "/orya:ha": "\u0B39", + "/orya:nukta": "\u0B3C", + "/orya:avagraha": "\u0B3D", + "/orya:aasign": "\u0B3E", + "/orya:isign": "\u0B3F", + "/orya:iisign": "\u0B40", + "/orya:usign": "\u0B41", + "/orya:uusign": "\u0B42", + "/orya:vocalicrsign": "\u0B43", + "/orya:vocalicrrsign": "\u0B44", + "/orya:esign": "\u0B47", + "/orya:aisign": "\u0B48", + "/orya:osign": "\u0B4B", + "/orya:ausign": "\u0B4C", + "/orya:virama": "\u0B4D", + "/orya:signoverline": "\u0B55", + "/orya:ailengthmark": "\u0B56", + "/orya:aulengthmark": "\u0B57", + "/orya:rra": "\u0B5C", + "/orya:rha": "\u0B5D", + "/orya:yya": "\u0B5F", + "/orya:vocalicrr": "\u0B60", + "/orya:vocalicll": "\u0B61", + "/orya:vocaliclsign": "\u0B62", + "/orya:vocalicllsign": "\u0B63", + "/orya:zero": "\u0B66", + "/orya:one": "\u0B67", + "/orya:two": "\u0B68", + "/orya:three": "\u0B69", + "/orya:four": "\u0B6A", + "/orya:five": "\u0B6B", + "/orya:six": "\u0B6C", + "/orya:seven": "\u0B6D", + "/orya:eight": "\u0B6E", + "/orya:nine": "\u0B6F", + "/orya:isshar": "\u0B70", + "/orya:wa": "\u0B71", + "/orya:onequarter": "\u0B72", + "/orya:onehalf": "\u0B73", + "/orya:threequarters": "\u0B74", + "/orya:onesixteenth": "\u0B75", + "/orya:oneeighth": "\u0B76", + "/orya:threesixteenths": "\u0B77", + "/taml:anusvara": "\u0B82", + "/taml:visarga": "\u0B83", + "/taml:a": "\u0B85", + "/taml:aa": "\u0B86", + "/taml:i": "\u0B87", + "/taml:ii": "\u0B88", + "/taml:u": "\u0B89", + "/taml:uu": "\u0B8A", + "/taml:e": "\u0B8E", + "/taml:ee": "\u0B8F", + "/taml:ai": "\u0B90", + "/taml:o": "\u0B92", + "/taml:oo": "\u0B93", + "/taml:au": "\u0B94", + "/taml:ka": "\u0B95", + "/taml:nga": "\u0B99", + "/taml:ca": "\u0B9A", + "/taml:ja": "\u0B9C", + "/taml:nya": "\u0B9E", + "/taml:tta": "\u0B9F", + "/taml:nna": "\u0BA3", + "/taml:ta": "\u0BA4", + "/taml:na": "\u0BA8", + "/taml:nnna": "\u0BA9", + "/taml:pa": "\u0BAA", + "/taml:ma": "\u0BAE", + "/taml:ya": "\u0BAF", + "/taml:ra": "\u0BB0", + "/taml:rra": "\u0BB1", + "/taml:la": "\u0BB2", + "/taml:lla": "\u0BB3", + "/taml:llla": "\u0BB4", + "/taml:va": "\u0BB5", + "/taml:sha": "\u0BB6", + "/taml:ssa": "\u0BB7", + "/taml:sa": "\u0BB8", + "/taml:ha": "\u0BB9", + "/taml:aasign": "\u0BBE", + "/taml:isign": "\u0BBF", + "/taml:iisign": "\u0BC0", + "/taml:usign": "\u0BC1", + "/taml:uusign": "\u0BC2", + "/taml:esign": "\u0BC6", + "/taml:eesign": "\u0BC7", + "/taml:aisign": "\u0BC8", + "/taml:osign": "\u0BCA", + "/taml:oosign": "\u0BCB", + "/taml:ausign": "\u0BCC", + "/taml:virama": "\u0BCD", + "/taml:om": "\u0BD0", + "/taml:aulengthmark": "\u0BD7", + "/taml:zero": "\u0BE6", + "/taml:one": "\u0BE7", + "/taml:two": "\u0BE8", + "/taml:three": "\u0BE9", + "/taml:four": "\u0BEA", + "/taml:five": "\u0BEB", + "/taml:six": "\u0BEC", + "/taml:seven": "\u0BED", + "/taml:eight": "\u0BEE", + "/taml:nine": "\u0BEF", + "/taml:ten": "\u0BF0", + "/taml:onehundred": "\u0BF1", + "/taml:onethousand": "\u0BF2", + "/taml:daysign": "\u0BF3", + "/taml:monthsign": "\u0BF4", + "/taml:yearsign": "\u0BF5", + "/taml:debitsign": "\u0BF6", + "/taml:creditsign": "\u0BF7", + "/taml:asabovesign": "\u0BF8", + "/taml:rupee": "\u0BF9", + "/taml:sign": "\u0BFA", + "/telu:combiningcandrabinduabovesign": "\u0C00", + "/telu:candrabindusign": "\u0C01", + "/telu:anusvara": "\u0C02", + "/telu:visarga": "\u0C03", + "/telu:combininganusvaraabovesign": "\u0C04", + "/telu:a": "\u0C05", + "/telu:aa": "\u0C06", + "/telu:i": "\u0C07", + "/telu:ii": "\u0C08", + "/telu:u": "\u0C09", + "/telu:uu": "\u0C0A", + "/telu:vocalicr": "\u0C0B", + "/telu:vocalicl": "\u0C0C", + "/telu:e": "\u0C0E", + "/telu:ee": "\u0C0F", + "/telu:ai": "\u0C10", + "/telu:o": "\u0C12", + "/telu:oo": "\u0C13", + "/telu:au": "\u0C14", + "/telu:ka": "\u0C15", + "/telu:kha": "\u0C16", + "/telu:ga": "\u0C17", + "/telu:gha": "\u0C18", + "/telu:nga": "\u0C19", + "/telu:ca": "\u0C1A", + "/telu:cha": "\u0C1B", + "/telu:ja": "\u0C1C", + "/telu:jha": "\u0C1D", + "/telu:nya": "\u0C1E", + "/telu:tta": "\u0C1F", + "/telu:ttha": "\u0C20", + "/telu:dda": "\u0C21", + "/telu:ddha": "\u0C22", + "/telu:nna": "\u0C23", + "/telu:ta": "\u0C24", + "/telu:tha": "\u0C25", + "/telu:da": "\u0C26", + "/telu:dha": "\u0C27", + "/telu:na": "\u0C28", + "/telu:pa": "\u0C2A", + "/telu:pha": "\u0C2B", + "/telu:ba": "\u0C2C", + "/telu:bha": "\u0C2D", + "/telu:ma": "\u0C2E", + "/telu:ya": "\u0C2F", + "/telu:ra": "\u0C30", + "/telu:rra": "\u0C31", + "/telu:la": "\u0C32", + "/telu:lla": "\u0C33", + "/telu:llla": "\u0C34", + "/telu:va": "\u0C35", + "/telu:sha": "\u0C36", + "/telu:ssa": "\u0C37", + "/telu:sa": "\u0C38", + "/telu:ha": "\u0C39", + "/telu:nukta": "\u0C3C", + "/telu:avagraha": "\u0C3D", + "/telu:aasign": "\u0C3E", + "/telu:isign": "\u0C3F", + "/telu:iisign": "\u0C40", + "/telu:usign": "\u0C41", + "/telu:uusign": "\u0C42", + "/telu:vocalicrsign": "\u0C43", + "/telu:vocalicrrsign": "\u0C44", + "/telu:esign": "\u0C46", + "/telu:eesign": "\u0C47", + "/telu:aisign": "\u0C48", + "/telu:osign": "\u0C4A", + "/telu:oosign": "\u0C4B", + "/telu:ausign": "\u0C4C", + "/telu:virama": "\u0C4D", + "/telu:lengthmark": "\u0C55", + "/telu:ailengthmark": "\u0C56", + "/telu:tsa": "\u0C58", + "/telu:dza": "\u0C59", + "/telu:rrra": "\u0C5A", + "/telu:nakaarapollu": "\u0C5D", + "/telu:vocalicrr": "\u0C60", + "/telu:vocalicll": "\u0C61", + "/telu:vocaliclsign": "\u0C62", + "/telu:vocalicllsign": "\u0C63", + "/telu:zero": "\u0C66", + "/telu:one": "\u0C67", + "/telu:two": "\u0C68", + "/telu:three": "\u0C69", + "/telu:four": "\u0C6A", + "/telu:five": "\u0C6B", + "/telu:six": "\u0C6C", + "/telu:seven": "\u0C6D", + "/telu:eight": "\u0C6E", + "/telu:nine": "\u0C6F", + "/telu:siddhamsign": "\u0C77", + "/telu:fractionzeroforoddpowersoffour": "\u0C78", + "/telu:fractiononeforoddpowersoffour": "\u0C79", + "/telu:fractiontwoforoddpowersoffour": "\u0C7A", + "/telu:fractionthreeforoddpowersoffour": "\u0C7B", + "/telu:fractiononeforevenpowersoffour": "\u0C7C", + "/telu:fractiontwoforevenpowersoffour": "\u0C7D", + "/telu:fractionthreeforevenpowersoffour": "\u0C7E", + "/telu:tuumusign": "\u0C7F", + "/knda:signspacingcandrabindu": "\u0C80", + "/knda:signcandrabindu": "\u0C81", + "/knda:anusvara": "\u0C82", + "/knda:visarga": "\u0C83", + "/knda:signsiddham": "\u0C84", + "/knda:a": "\u0C85", + "/knda:aa": "\u0C86", + "/knda:i": "\u0C87", + "/knda:ii": "\u0C88", + "/knda:u": "\u0C89", + "/knda:uu": "\u0C8A", + "/knda:rvocal": "\u0C8B", + "/knda:lvocal": "\u0C8C", + "/knda:e": "\u0C8E", + "/knda:ee": "\u0C8F", + "/knda:ai": "\u0C90", + "/knda:o": "\u0C92", + "/knda:oo": "\u0C93", + "/knda:au": "\u0C94", + "/knda:ka": "\u0C95", + "/knda:kha": "\u0C96", + "/knda:ga": "\u0C97", + "/knda:gha": "\u0C98", + "/knda:nga": "\u0C99", + "/knda:ca": "\u0C9A", + "/knda:cha": "\u0C9B", + "/knda:ja": "\u0C9C", + "/knda:jha": "\u0C9D", + "/knda:nya": "\u0C9E", + "/knda:tta": "\u0C9F", + "/knda:ttha": "\u0CA0", + "/knda:dda": "\u0CA1", + "/knda:ddha": "\u0CA2", + "/knda:nna": "\u0CA3", + "/knda:ta": "\u0CA4", + "/knda:tha": "\u0CA5", + "/knda:da": "\u0CA6", + "/knda:dha": "\u0CA7", + "/knda:na": "\u0CA8", + "/knda:pa": "\u0CAA", + "/knda:pha": "\u0CAB", + "/knda:ba": "\u0CAC", + "/knda:bha": "\u0CAD", + "/knda:ma": "\u0CAE", + "/knda:ya": "\u0CAF", + "/knda:ra": "\u0CB0", + "/knda:rra": "\u0CB1", + "/knda:la": "\u0CB2", + "/knda:lla": "\u0CB3", + "/knda:va": "\u0CB5", + "/knda:sha": "\u0CB6", + "/knda:ssa": "\u0CB7", + "/knda:sa": "\u0CB8", + "/knda:ha": "\u0CB9", + "/knda:nukta": "\u0CBC", + "/knda:avagraha": "\u0CBD", + "/knda:aasign": "\u0CBE", + "/knda:isign": "\u0CBF", + "/knda:iisign": "\u0CC0", + "/knda:usign": "\u0CC1", + "/knda:uusign": "\u0CC2", + "/knda:rvocalsign": "\u0CC3", + "/knda:rrvocalsign": "\u0CC4", + "/knda:esign": "\u0CC6", + "/knda:eesign": "\u0CC7", + "/knda:aisign": "\u0CC8", + "/knda:osign": "\u0CCA", + "/knda:oosign": "\u0CCB", + "/knda:ausign": "\u0CCC", + "/knda:virama": "\u0CCD", + "/knda:length": "\u0CD5", + "/knda:ailength": "\u0CD6", + "/knda:nakaarapollu": "\u0CDD", + "/knda:fa": "\u0CDE", + "/knda:rrvocal": "\u0CE0", + "/knda:llvocal": "\u0CE1", + "/knda:lvocalsign": "\u0CE2", + "/knda:llvocalsign": "\u0CE3", + "/knda:zero": "\u0CE6", + "/knda:one": "\u0CE7", + "/knda:two": "\u0CE8", + "/knda:three": "\u0CE9", + "/knda:four": "\u0CEA", + "/knda:five": "\u0CEB", + "/knda:six": "\u0CEC", + "/knda:seven": "\u0CED", + "/knda:eight": "\u0CEE", + "/knda:nine": "\u0CEF", + "/knda:jihvamuliya": "\u0CF1", + "/knda:upadhmaniya": "\u0CF2", + "/knda:signcombininganusvaraaboveright": "\u0CF3", + "/mlym:combininganusvaraabovesign": "\u0D00", + "/mlym:candrabindusign": "\u0D01", + "/mlym:anusvarasign": "\u0D02", + "/mlym:visargasign": "\u0D03", + "/mlym:vedicanusvara": "\u0D04", + "/mlym:a": "\u0D05", + "/mlym:aa": "\u0D06", + "/mlym:i": "\u0D07", + "/mlym:ii": "\u0D08", + "/mlym:u": "\u0D09", + "/mlym:uu": "\u0D0A", + "/mlym:rvocal": "\u0D0B", + "/mlym:lvocal": "\u0D0C", + "/mlym:e": "\u0D0E", + "/mlym:ee": "\u0D0F", + "/mlym:ai": "\u0D10", + "/mlym:o": "\u0D12", + "/mlym:oo": "\u0D13", + "/mlym:au": "\u0D14", + "/mlym:ka": "\u0D15", + "/mlym:kha": "\u0D16", + "/mlym:ga": "\u0D17", + "/mlym:gha": "\u0D18", + "/mlym:nga": "\u0D19", + "/mlym:ca": "\u0D1A", + "/mlym:cha": "\u0D1B", + "/mlym:ja": "\u0D1C", + "/mlym:jha": "\u0D1D", + "/mlym:nya": "\u0D1E", + "/mlym:tta": "\u0D1F", + "/mlym:ttha": "\u0D20", + "/mlym:dda": "\u0D21", + "/mlym:ddha": "\u0D22", + "/mlym:nna": "\u0D23", + "/mlym:ta": "\u0D24", + "/mlym:tha": "\u0D25", + "/mlym:da": "\u0D26", + "/mlym:dha": "\u0D27", + "/mlym:na": "\u0D28", + "/mlym:nnna": "\u0D29", + "/mlym:pa": "\u0D2A", + "/mlym:pha": "\u0D2B", + "/mlym:ba": "\u0D2C", + "/mlym:bha": "\u0D2D", + "/mlym:ma": "\u0D2E", + "/mlym:ya": "\u0D2F", + "/mlym:ra": "\u0D30", + "/mlym:rra": "\u0D31", + "/mlym:la": "\u0D32", + "/mlym:lla": "\u0D33", + "/mlym:llla": "\u0D34", + "/mlym:va": "\u0D35", + "/mlym:sha": "\u0D36", + "/mlym:ssa": "\u0D37", + "/mlym:sa": "\u0D38", + "/mlym:ha": "\u0D39", + "/mlym:ttta": "\u0D3A", + "/mlym:verticalbarviramasign": "\u0D3B", + "/mlym:circularviramasign": "\u0D3C", + "/mlym:avagrahasign": "\u0D3D", + "/mlym:aasign": "\u0D3E", + "/mlym:isign": "\u0D3F", + "/mlym:iisign": "\u0D40", + "/mlym:usign": "\u0D41", + "/mlym:uusign": "\u0D42", + "/mlym:rvocalsign": "\u0D43", + "/mlym:rrvocalsign": "\u0D44", + "/mlym:esign": "\u0D46", + "/mlym:eesign": "\u0D47", + "/mlym:aisign": "\u0D48", + "/mlym:osign": "\u0D4A", + "/mlym:oosign": "\u0D4B", + "/mlym:ausign": "\u0D4C", + "/mlym:viramasign": "\u0D4D", + "/mlym:dotreph": "\u0D4E", + "/mlym:parasign": "\u0D4F", + "/mlym:mchillu": "\u0D54", + "/mlym:ychillu": "\u0D55", + "/mlym:lllchillu": "\u0D56", + "/mlym:aulength": "\u0D57", + "/mlym:oneone-hundred-and-sixtieth": "\u0D58", + "/mlym:onefortieth": "\u0D59", + "/mlym:threeeightieths": "\u0D5A", + "/mlym:onetwentieth": "\u0D5B", + "/mlym:onetenth": "\u0D5C", + "/mlym:threetwentieths": "\u0D5D", + "/mlym:onefifth": "\u0D5E", + "/mlym:archaicii": "\u0D5F", + "/mlym:rrvocal": "\u0D60", + "/mlym:llvocal": "\u0D61", + "/mlym:lvocalsign": "\u0D62", + "/mlym:llvocalsign": "\u0D63", + "/mlym:zero": "\u0D66", + "/mlym:one": "\u0D67", + "/mlym:two": "\u0D68", + "/mlym:three": "\u0D69", + "/mlym:four": "\u0D6A", + "/mlym:five": "\u0D6B", + "/mlym:six": "\u0D6C", + "/mlym:seven": "\u0D6D", + "/mlym:eight": "\u0D6E", + "/mlym:nine": "\u0D6F", + "/mlym:ten": "\u0D70", + "/mlym:onehundred": "\u0D71", + "/mlym:onethousand": "\u0D72", + "/mlym:onequarter": "\u0D73", + "/mlym:onehalf": "\u0D74", + "/mlym:threequarters": "\u0D75", + "/mlym:onesixteenth": "\u0D76", + "/mlym:oneeighth": "\u0D77", + "/mlym:threesixteenths": "\u0D78", + "/mlym:date": "\u0D79", + "/mlym:nnchillu": "\u0D7A", + "/mlym:nchillu": "\u0D7B", + "/mlym:rrchillu": "\u0D7C", + "/mlym:lchillu": "\u0D7D", + "/mlym:llchillu": "\u0D7E", + "/mlym:kchillu": "\u0D7F", + "/sinh:candrabindu": "\u0D81", + "/sinh:anusvara": "\u0D82", + "/sinh:visarga": "\u0D83", + "/sinh:a": "\u0D85", + "/sinh:aa": "\u0D86", + "/sinh:ae": "\u0D87", + "/sinh:aae": "\u0D88", + "/sinh:i": "\u0D89", + "/sinh:ii": "\u0D8A", + "/sinh:u": "\u0D8B", + "/sinh:uu": "\u0D8C", + "/sinh:vocalicr": "\u0D8D", + "/sinh:vocalicrr": "\u0D8E", + "/sinh:vocalicl": "\u0D8F", + "/sinh:vocalicll": "\u0D90", + "/sinh:e": "\u0D91", + "/sinh:ee": "\u0D92", + "/sinh:ai": "\u0D93", + "/sinh:o": "\u0D94", + "/sinh:oo": "\u0D95", + "/sinh:au": "\u0D96", + "/sinh:ka": "\u0D9A", + "/sinh:kha": "\u0D9B", + "/sinh:ga": "\u0D9C", + "/sinh:gha": "\u0D9D", + "/sinh:nga": "\u0D9E", + "/sinh:nnga": "\u0D9F", + "/sinh:ca": "\u0DA0", + "/sinh:cha": "\u0DA1", + "/sinh:ja": "\u0DA2", + "/sinh:jha": "\u0DA3", + "/sinh:nya": "\u0DA4", + "/sinh:jnya": "\u0DA5", + "/sinh:nyja": "\u0DA6", + "/sinh:tta": "\u0DA7", + "/sinh:ttha": "\u0DA8", + "/sinh:dda": "\u0DA9", + "/sinh:ddha": "\u0DAA", + "/sinh:nna": "\u0DAB", + "/sinh:nndda": "\u0DAC", + "/sinh:ta": "\u0DAD", + "/sinh:tha": "\u0DAE", + "/sinh:da": "\u0DAF", + "/sinh:dha": "\u0DB0", + "/sinh:na": "\u0DB1", + "/sinh:nda": "\u0DB3", + "/sinh:pa": "\u0DB4", + "/sinh:pha": "\u0DB5", + "/sinh:ba": "\u0DB6", + "/sinh:bha": "\u0DB7", + "/sinh:ma": "\u0DB8", + "/sinh:mba": "\u0DB9", + "/sinh:ya": "\u0DBA", + "/sinh:ra": "\u0DBB", + "/sinh:la": "\u0DBD", + "/sinh:va": "\u0DC0", + "/sinh:sha": "\u0DC1", + "/sinh:ssa": "\u0DC2", + "/sinh:sa": "\u0DC3", + "/sinh:ha": "\u0DC4", + "/sinh:lla": "\u0DC5", + "/sinh:fa": "\u0DC6", + "/sinh:virama": "\u0DCA", + "/sinh:aasign": "\u0DCF", + "/sinh:aesign": "\u0DD0", + "/sinh:aaesign": "\u0DD1", + "/sinh:isign": "\u0DD2", + "/sinh:iisign": "\u0DD3", + "/sinh:usign": "\u0DD4", + "/sinh:uusign": "\u0DD6", + "/sinh:vocalicrsign": "\u0DD8", + "/sinh:esign": "\u0DD9", + "/sinh:eesign": "\u0DDA", + "/sinh:aisign": "\u0DDB", + "/sinh:osign": "\u0DDC", + "/sinh:oosign": "\u0DDD", + "/sinh:ausign": "\u0DDE", + "/sinh:vocaliclsign": "\u0DDF", + "/sinh:zerolith": "\u0DE6", + "/sinh:onelith": "\u0DE7", + "/sinh:twolith": "\u0DE8", + "/sinh:threelith": "\u0DE9", + "/sinh:fourlith": "\u0DEA", + "/sinh:fivelith": "\u0DEB", + "/sinh:sixlith": "\u0DEC", + "/sinh:sevenlith": "\u0DED", + "/sinh:eightlith": "\u0DEE", + "/sinh:ninelith": "\u0DEF", + "/sinh:vocalicrrsign": "\u0DF2", + "/sinh:vocalicllsign": "\u0DF3", + "/sinh:kunddaliya": "\u0DF4", + "/th-kokai": "\u0E01", + "/th-khokhai": "\u0E02", + "/th-khokhuat": "\u0E03", + "/th-khokhwai": "\u0E04", + "/th-khokhon": "\u0E05", + "/th-khorakhang": "\u0E06", + "/th-ngongu": "\u0E07", + "/th-chochan": "\u0E08", + "/th-choching": "\u0E09", + "/th-chochang": "\u0E0A", + "/th-soso": "\u0E0B", + "/th-chochoe": "\u0E0C", + "/th-yoying": "\u0E0D", + "/th-dochada": "\u0E0E", + "/th-topatak": "\u0E0F", + "/th-thothan": "\u0E10", + "/th-thonangmontho": "\u0E11", + "/th-thophuthao": "\u0E12", + "/th-nonen": "\u0E13", + "/th-dodek": "\u0E14", + "/th-totao": "\u0E15", + "/th-thothung": "\u0E16", + "/th-thothahan": "\u0E17", + "/th-thothong": "\u0E18", + "/th-nonu": "\u0E19", + "/th-bobaimai": "\u0E1A", + "/th-popla": "\u0E1B", + "/th-phophung": "\u0E1C", + "/th-fofa": "\u0E1D", + "/th-phophan": "\u0E1E", + "/th-fofan": "\u0E1F", + "/th-phosamphao": "\u0E20", + "/th-moma": "\u0E21", + "/th-yoyak": "\u0E22", + "/th-rorua": "\u0E23", + "/th-ru": "\u0E24", + "/th-loling": "\u0E25", + "/th-lu": "\u0E26", + "/th-wowaen": "\u0E27", + "/th-sosala": "\u0E28", + "/th-sorusi": "\u0E29", + "/th-sosua": "\u0E2A", + "/th-hohip": "\u0E2B", + "/th-lochula": "\u0E2C", + "/th-oang": "\u0E2D", + "/th-honokhuk": "\u0E2E", + "/th-paiyannoi": "\u0E2F", + "/th-saraa": "\u0E30", + "/th-maihan-akat": "\u0E31", + "/th-saraaa": "\u0E32", + "/th-saraam": "\u0E33", + "/th-sarai": "\u0E34", + "/th-saraii": "\u0E35", + "/th-saraue": "\u0E36", + "/th-sarauee": "\u0E37", + "/th-sarau": "\u0E38", + "/th-sarauu": "\u0E39", + "/th-phinthu": "\u0E3A", + "/th-baht": "\u0E3F", + "/th-sarae": "\u0E40", + "/th-saraae": "\u0E41", + "/th-sarao": "\u0E42", + "/th-saraaimaimuan": "\u0E43", + "/th-saraaimaimalai": "\u0E44", + "/th-lakkhangyao": "\u0E45", + "/th-maiyamok": "\u0E46", + "/th-maitaikhu": "\u0E47", + "/th-maiek": "\u0E48", + "/th-maitho": "\u0E49", + "/th-maitri": "\u0E4A", + "/th-maichattawa": "\u0E4B", + "/th-thanthakhat": "\u0E4C", + "/th-nikhahit": "\u0E4D", + "/th-yamakkan": "\u0E4E", + "/th-fongman": "\u0E4F", + "/th-zero": "\u0E50", + "/th-one": "\u0E51", + "/th-two": "\u0E52", + "/th-three": "\u0E53", + "/th-four": "\u0E54", + "/th-five": "\u0E55", + "/th-six": "\u0E56", + "/th-seven": "\u0E57", + "/th-eight": "\u0E58", + "/th-nine": "\u0E59", + "/th-angkhankhu": "\u0E5A", + "/th-khomut": "\u0E5B", + "/tibt:omsyllable": "\u0F00", + "/tibt:gteryigmgotruncatedamark": "\u0F01", + "/tibt:gteryigmgoumrnambcadmamark": "\u0F02", + "/tibt:gteryigmgoumgtertshegmamark": "\u0F03", + "/tibt:yigmgomdunmainitialmark": "\u0F04", + "/tibt:yigmgosgabmaclosingmark": "\u0F05", + "/tibt:caretyigmgophurshadmamark": "\u0F06", + "/tibt:yigmgotshegshadmamark": "\u0F07", + "/tibt:sbrulshadmark": "\u0F08", + "/tibt:bskuryigmgomark": "\u0F09", + "/tibt:bkashogyigmgomark": "\u0F0A", + "/tibt:intersyllabictshegmark": "\u0F0B", + "/tibt:delimitertshegbstarmark": "\u0F0C", + "/tibt:shadmark": "\u0F0D", + "/tibt:nyisshadmark": "\u0F0E", + "/tibt:tshegshadmark": "\u0F0F", + "/tibt:nyistshegshadmark": "\u0F10", + "/tibt:rinchenspungsshadmark": "\u0F11", + "/tibt:rgyagramshadmark": "\u0F12", + "/tibt:caretdzudrtagsmelongcanmark": "\u0F13", + "/tibt:gtertshegmark": "\u0F14", + "/tibt:chadrtagslogotypesign": "\u0F15", + "/tibt:lhagrtagslogotypesign": "\u0F16", + "/tibt:astrologicalsgragcancharrtagssign": "\u0F17", + "/tibt:astrologicalkhyudpasign": "\u0F18", + "/tibt:astrologicalsdongtshugssign": "\u0F19", + "/tibt:rdeldkargcigsign": "\u0F1A", + "/tibt:rdeldkargnyissign": "\u0F1B", + "/tibt:rdeldkargsumsign": "\u0F1C", + "/tibt:rdelnaggcigsign": "\u0F1D", + "/tibt:rdelnaggnyissign": "\u0F1E", + "/tibt:rdeldkarrdelnagsign": "\u0F1F", + "/tibt:zero": "\u0F20", + "/tibt:one": "\u0F21", + "/tibt:two": "\u0F22", + "/tibt:three": "\u0F23", + "/tibt:four": "\u0F24", + "/tibt:five": "\u0F25", + "/tibt:six": "\u0F26", + "/tibt:seven": "\u0F27", + "/tibt:eight": "\u0F28", + "/tibt:nine": "\u0F29", + "/tibt:halfone": "\u0F2A", + "/tibt:halftwo": "\u0F2B", + "/tibt:halfthree": "\u0F2C", + "/tibt:halffour": "\u0F2D", + "/tibt:halffive": "\u0F2E", + "/tibt:halfsix": "\u0F2F", + "/tibt:halfseven": "\u0F30", + "/tibt:halfeight": "\u0F31", + "/tibt:halfnine": "\u0F32", + "/tibt:halfzero": "\u0F33", + "/tibt:bsdusrtagsmark": "\u0F34", + "/tibt:ngasbzungnyizlamark": "\u0F35", + "/tibt:caretdzudrtagsbzhimigcanmark": "\u0F36", + "/tibt:ngasbzungsgorrtagsmark": "\u0F37", + "/tibt:chemgomark": "\u0F38", + "/tibt:tsaphrumark": "\u0F39", + "/tibt:gugrtagsgyonmark": "\u0F3A", + "/tibt:gugrtagsgyasmark": "\u0F3B", + "/tibt:angkhanggyonmark": "\u0F3C", + "/tibt:angkhanggyasmark": "\u0F3D", + "/tibt:yartshessign": "\u0F3E", + "/tibt:martshessign": "\u0F3F", + "/tibt:ka": "\u0F40", + "/tibt:kha": "\u0F41", + "/tibt:ga": "\u0F42", + "/tibt:gha": "\u0F43", + "/tibt:nga": "\u0F44", + "/tibt:ca": "\u0F45", + "/tibt:cha": "\u0F46", + "/tibt:ja": "\u0F47", + "/tibt:nya": "\u0F49", + "/tibt:tta": "\u0F4A", + "/tibt:ttha": "\u0F4B", + "/tibt:dda": "\u0F4C", + "/tibt:ddha": "\u0F4D", + "/tibt:nna": "\u0F4E", + "/tibt:ta": "\u0F4F", + "/tibt:tha": "\u0F50", + "/tibt:da": "\u0F51", + "/tibt:dha": "\u0F52", + "/tibt:na": "\u0F53", + "/tibt:pa": "\u0F54", + "/tibt:pha": "\u0F55", + "/tibt:ba": "\u0F56", + "/tibt:bha": "\u0F57", + "/tibt:ma": "\u0F58", + "/tibt:tsa": "\u0F59", + "/tibt:tsha": "\u0F5A", + "/tibt:dza": "\u0F5B", + "/tibt:dzha": "\u0F5C", + "/tibt:wa": "\u0F5D", + "/tibt:zha": "\u0F5E", + "/tibt:za": "\u0F5F", + "/tibt:AA": "\u0F60", + "/tibt:ya": "\u0F61", + "/tibt:ra": "\u0F62", + "/tibt:la": "\u0F63", + "/tibt:sha": "\u0F64", + "/tibt:ssa": "\u0F65", + "/tibt:sa": "\u0F66", + "/tibt:ha": "\u0F67", + "/tibt:a": "\u0F68", + "/tibt:kssa": "\u0F69", + "/tibt:rafixed": "\u0F6A", + "/tibt:kka": "\u0F6B", + "/tibt:rra": "\u0F6C", + "/tibt:aavowelsign": "\u0F71", + "/tibt:ivowelsign": "\u0F72", + "/tibt:iivowelsign": "\u0F73", + "/tibt:uvowelsign": "\u0F74", + "/tibt:uuvowelsign": "\u0F75", + "/tibt:rvocalicvowelsign": "\u0F76", + "/tibt:rrvocalicvowelsign": "\u0F77", + "/tibt:lvocalicvowelsign": "\u0F78", + "/tibt:llvocalicvowelsign": "\u0F79", + "/tibt:evowelsign": "\u0F7A", + "/tibt:eevowelsign": "\u0F7B", + "/tibt:ovowelsign": "\u0F7C", + "/tibt:oovowelsign": "\u0F7D", + "/tibt:rjessungarosign": "\u0F7E", + "/tibt:rnambcadsign": "\u0F7F", + "/tibt:reversedivowelsign": "\u0F80", + "/tibt:reversediivowelsign": "\u0F81", + "/tibt:nyizlanaadasign": "\u0F82", + "/tibt:snaldansign": "\u0F83", + "/tibt:halantamark": "\u0F84", + "/tibt:palutamark": "\u0F85", + "/tibt:lcirtagssign": "\u0F86", + "/tibt:yangrtagssign": "\u0F87", + "/tibt:lcetsacansign": "\u0F88", + "/tibt:mchucansign": "\u0F89", + "/tibt:grucanrgyingssign": "\u0F8A", + "/tibt:grumedrgyingssign": "\u0F8B", + "/tibt:invertedmchucansign": "\u0F8C", + "/tibt:lcetsacansubjoinedsign": "\u0F8D", + "/tibt:mchucansubjoinedsign": "\u0F8E", + "/tibt:invertedmchucansubjoinedsign": "\u0F8F", + "/tibt:kasubjoined": "\u0F90", + "/tibt:khasubjoined": "\u0F91", + "/tibt:gasubjoined": "\u0F92", + "/tibt:ghasubjoined": "\u0F93", + "/tibt:ngasubjoined": "\u0F94", + "/tibt:casubjoined": "\u0F95", + "/tibt:chasubjoined": "\u0F96", + "/tibt:jasubjoined": "\u0F97", + "/tibt:nyasubjoined": "\u0F99", + "/tibt:ttasubjoined": "\u0F9A", + "/tibt:tthasubjoined": "\u0F9B", + "/tibt:ddasubjoined": "\u0F9C", + "/tibt:ddhasubjoined": "\u0F9D", + "/tibt:nnasubjoined": "\u0F9E", + "/tibt:tasubjoined": "\u0F9F", + "/tibt:thasubjoined": "\u0FA0", + "/tibt:dasubjoined": "\u0FA1", + "/tibt:dhasubjoined": "\u0FA2", + "/tibt:nasubjoined": "\u0FA3", + "/tibt:pasubjoined": "\u0FA4", + "/tibt:phasubjoined": "\u0FA5", + "/tibt:basubjoined": "\u0FA6", + "/tibt:bhasubjoined": "\u0FA7", + "/tibt:masubjoined": "\u0FA8", + "/tibt:tsasubjoined": "\u0FA9", + "/tibt:tshasubjoined": "\u0FAA", + "/tibt:dzasubjoined": "\u0FAB", + "/tibt:dzhasubjoined": "\u0FAC", + "/tibt:wasubjoined": "\u0FAD", + "/tibt:zhasubjoined": "\u0FAE", + "/tibt:zasubjoined": "\u0FAF", + "/tibt:subjoinedAA": "\u0FB0", + "/tibt:yasubjoined": "\u0FB1", + "/tibt:rasubjoined": "\u0FB2", + "/tibt:lasubjoined": "\u0FB3", + "/tibt:shasubjoined": "\u0FB4", + "/tibt:ssasubjoined": "\u0FB5", + "/tibt:sasubjoined": "\u0FB6", + "/tibt:hasubjoined": "\u0FB7", + "/tibt:asubjoined": "\u0FB8", + "/tibt:kssasubjoined": "\u0FB9", + "/tibt:wasubjoinedfixed": "\u0FBA", + "/tibt:yasubjoinedfixed": "\u0FBB", + "/tibt:rasubjoinedfixed": "\u0FBC", + "/tibt:kurukha": "\u0FBE", + "/tibt:kurukhabzhimigcan": "\u0FBF", + "/tibt:heavybeatcantillationsign": "\u0FC0", + "/tibt:lightbeatcantillationsign": "\u0FC1", + "/tibt:cangteucantillationsign": "\u0FC2", + "/tibt:sbubchalcantillationsign": "\u0FC3", + "/tibt:drilbusymbol": "\u0FC4", + "/tibt:rdorjesymbol": "\u0FC5", + "/tibt:padmagdansymbol": "\u0FC6", + "/tibt:rdorjergyagramsymbol": "\u0FC7", + "/tibt:phurpasymbol": "\u0FC8", + "/tibt:norbusymbol": "\u0FC9", + "/tibt:norbunyiskhyilsymbol": "\u0FCA", + "/tibt:norbugsumkhyilsymbol": "\u0FCB", + "/tibt:norbubzhikhyilsymbol": "\u0FCC", + "/tibt:rdelnagrdeldkarsign": "\u0FCE", + "/tibt:rdelnaggsumsign": "\u0FCF", + "/tibt:bskashoggimgorgyanmark": "\u0FD0", + "/tibt:mnyamyiggimgorgyanmark": "\u0FD1", + "/tibt:nyistshegmark": "\u0FD2", + "/tibt:brdarnyingyigmgomdunmainitialmark": "\u0FD3", + "/tibt:brdarnyingyigmgosgabmaclosingmark": "\u0FD4", + "/tibt:svastiright": "\u0FD5", + "/tibt:svastileft": "\u0FD6", + "/tibt:svastirightdot": "\u0FD7", + "/tibt:svastileftdot": "\u0FD8", + "/tibt:leadingmchanrtagsmark": "\u0FD9", + "/tibt:trailingmchanrtagsmark": "\u0FDA", + "/AnGeok": "\u10A0", + "/BanGeok": "\u10A1", + "/GanGeok": "\u10A2", + "/DonGeok": "\u10A3", + "/EnGeok": "\u10A4", + "/VinGeok": "\u10A5", + "/ZenGeok": "\u10A6", + "/TanGeok": "\u10A7", + "/InGeok": "\u10A8", + "/KanGeok": "\u10A9", + "/LasGeok": "\u10AA", + "/ManGeok": "\u10AB", + "/NarGeok": "\u10AC", + "/OnGeok": "\u10AD", + "/ParGeok": "\u10AE", + "/ZharGeok": "\u10AF", + "/RaeGeok": "\u10B0", + "/SanGeok": "\u10B1", + "/TarGeok": "\u10B2", + "/UnGeok": "\u10B3", + "/PharGeok": "\u10B4", + "/KharGeok": "\u10B5", + "/GhanGeok": "\u10B6", + "/QarGeok": "\u10B7", + "/ShinGeok": "\u10B8", + "/ChinGeok": "\u10B9", + "/CanGeok": "\u10BA", + "/JilGeok": "\u10BB", + "/CilGeok": "\u10BC", + "/CharGeok": "\u10BD", + "/XanGeok": "\u10BE", + "/JhanGeok": "\u10BF", + "/HaeGeok": "\u10C0", + "/HeGeok": "\u10C1", + "/HieGeok": "\u10C2", + "/WeGeok": "\u10C3", + "/HarGeok": "\u10C4", + "/HoeGeok": "\u10C5", + "/YnGeok": "\u10C7", + "/AenGeok": "\u10CD", + "/anGeor": "\u10D0", + "/banGeor": "\u10D1", + "/ganGeor": "\u10D2", + "/donGeor": "\u10D3", + "/enGeor": "\u10D4", + "/vinGeor": "\u10D5", + "/zenGeor": "\u10D6", + "/tanGeor": "\u10D7", + "/inGeor": "\u10D8", + "/kanGeor": "\u10D9", + "/lasGeor": "\u10DA", + "/manGeor": "\u10DB", + "/narGeor": "\u10DC", + "/onGeor": "\u10DD", + "/parGeor": "\u10DE", + "/zharGeor": "\u10DF", + "/raeGeor": "\u10E0", + "/sanGeor": "\u10E1", + "/tarGeor": "\u10E2", + "/unGeor": "\u10E3", + "/pharGeor": "\u10E4", + "/kharGeor": "\u10E5", + "/ghanGeor": "\u10E6", + "/qarGeor": "\u10E7", + "/shinGeor": "\u10E8", + "/chinGeor": "\u10E9", + "/canGeor": "\u10EA", + "/jilGeor": "\u10EB", + "/cilGeor": "\u10EC", + "/charGeor": "\u10ED", + "/xanGeor": "\u10EE", + "/jhanGeor": "\u10EF", + "/haeGeor": "\u10F0", + "/heGeor": "\u10F1", + "/hieGeor": "\u10F2", + "/weGeor": "\u10F3", + "/harGeor": "\u10F4", + "/hoeGeor": "\u10F5", + "/fiGeor": "\u10F6", + "/ynGeor": "\u10F7", + "/elifiGeor": "\u10F8", + "/turnedganGeor": "\u10F9", + "/ainGeor": "\u10FA", + "/geor:paragraphseparator": "\u10FB", + "/narmodGeor": "\u10FC", + "/aenGeor": "\u10FD", + "/hardsignGeor": "\u10FE", + "/labialsignGeor": "\u10FF", + "/ko:kiyeokchoseong": "\u1100", + "/ko:ssangkiyeokchoseong": "\u1101", + "/ko:nieunchoseong": "\u1102", + "/ko:tikeutchoseong": "\u1103", + "/ko:ssangtikeutchoseong": "\u1104", + "/ko:rieulchoseong": "\u1105", + "/ko:mieumchoseong": "\u1106", + "/ko:pieupchoseong": "\u1107", + "/ko:ssangpieupchoseong": "\u1108", + "/ko:sioschoseong": "\u1109", + "/ko:ssangsioschoseong": "\u110A", + "/ko:ieungchoseong": "\u110B", + "/ko:cieucchoseong": "\u110C", + "/ko:ssangcieucchoseong": "\u110D", + "/ko:chieuchchoseong": "\u110E", + "/ko:khieukhchoseong": "\u110F", + "/ko:thieuthchoseong": "\u1110", + "/ko:phieuphchoseong": "\u1111", + "/ko:hieuhchoseong": "\u1112", + "/ko:nieunkiyeokchoseong": "\u1113", + "/ko:ssangnieunchoseong": "\u1114", + "/ko:nieuntikeutchoseong": "\u1115", + "/ko:nieunpieupchoseong": "\u1116", + "/ko:tikeutkiyeokchoseong": "\u1117", + "/ko:rieulnieunchoseong": "\u1118", + "/ko:ssangrieulchoseong": "\u1119", + "/ko:rieulhieuhchoseong": "\u111A", + "/ko:kapyeounrieulchoseong": "\u111B", + "/ko:mieumpieupchoseong": "\u111C", + "/ko:kapyeounmieumchoseong": "\u111D", + "/ko:pieupkiyeokchoseong": "\u111E", + "/ko:pieupnieunchoseong": "\u111F", + "/ko:pieuptikeutchoseong": "\u1120", + "/ko:pieupsioschoseong": "\u1121", + "/ko:pieupsioskiyeokchoseong": "\u1122", + "/ko:pieupsiostikeutchoseong": "\u1123", + "/ko:pieupsiospieupchoseong": "\u1124", + "/ko:pieupssangsioschoseong": "\u1125", + "/ko:pieupsioscieucchoseong": "\u1126", + "/ko:pieupcieucchoseong": "\u1127", + "/ko:pieupchieuchchoseong": "\u1128", + "/ko:pieupthieuthchoseong": "\u1129", + "/ko:pieupphieuphchoseong": "\u112A", + "/ko:kapyeounpieupchoseong": "\u112B", + "/ko:kapyeounssangpieupchoseong": "\u112C", + "/ko:sioskiyeokchoseong": "\u112D", + "/ko:siosnieunchoseong": "\u112E", + "/ko:siostikeutchoseong": "\u112F", + "/ko:siosrieulchoseong": "\u1130", + "/ko:siosmieumchoseong": "\u1131", + "/ko:siospieupchoseong": "\u1132", + "/ko:siospieupkiyeokchoseong": "\u1133", + "/ko:siosssangsioschoseong": "\u1134", + "/ko:siosieungchoseong": "\u1135", + "/ko:sioscieucchoseong": "\u1136", + "/ko:sioschieuchchoseong": "\u1137", + "/ko:sioskhieukhchoseong": "\u1138", + "/ko:siosthieuthchoseong": "\u1139", + "/ko:siosphieuphchoseong": "\u113A", + "/ko:sioshieuhchoseong": "\u113B", + "/ko:chitueumsioschoseong": "\u113C", + "/ko:chitueumssangsioschoseong": "\u113D", + "/ko:ceongchieumsioschoseong": "\u113E", + "/ko:ceongchieumssangsioschoseong": "\u113F", + "/ko:pansioschoseong": "\u1140", + "/ko:ieungkiyeokchoseong": "\u1141", + "/ko:ieungtikeutchoseong": "\u1142", + "/ko:ieungmieumchoseong": "\u1143", + "/ko:ieungpieupchoseong": "\u1144", + "/ko:ieungsioschoseong": "\u1145", + "/ko:ieungpansioschoseong": "\u1146", + "/ko:ssangieungchoseong": "\u1147", + "/ko:ieungcieucchoseong": "\u1148", + "/ko:ieungchieuchchoseong": "\u1149", + "/ko:ieungthieuthchoseong": "\u114A", + "/ko:ieungphieuphchoseong": "\u114B", + "/ko:yesieungchoseong": "\u114C", + "/ko:cieucieungchoseong": "\u114D", + "/ko:chitueumcieucchoseong": "\u114E", + "/ko:chitueumssangcieucchoseong": "\u114F", + "/ko:ceongchieumcieucchoseong": "\u1150", + "/ko:ceongchieumssangcieucchoseong": "\u1151", + "/ko:chieuchkhieukhchoseong": "\u1152", + "/ko:chieuchhieuhchoseong": "\u1153", + "/ko:chitueumchieuchchoseong": "\u1154", + "/ko:ceongchieumchieuchchoseong": "\u1155", + "/ko:phieuphpieupchoseong": "\u1156", + "/ko:kapyeounphieuphchoseong": "\u1157", + "/ko:ssanghieuhchoseong": "\u1158", + "/ko:yeorinhieuhchoseong": "\u1159", + "/ko:kiyeoktikeutchoseong": "\u115A", + "/ko:nieunsioschoseong": "\u115B", + "/ko:nieuncieucchoseong": "\u115C", + "/ko:nieunhieuhchoseong": "\u115D", + "/ko:tikeutrieulchoseong": "\u115E", + "/ko:fillerchoseong": "\u115F", + "/ko:fillerjungseong": "\u1160", + "/ko:ajungseong": "\u1161", + "/ko:aejungseong": "\u1162", + "/ko:yajungseong": "\u1163", + "/ko:yaejungseong": "\u1164", + "/ko:eojungseong": "\u1165", + "/ko:ejungseong": "\u1166", + "/ko:yeojungseong": "\u1167", + "/ko:yejungseong": "\u1168", + "/ko:ojungseong": "\u1169", + "/ko:wajungseong": "\u116A", + "/ko:waejungseong": "\u116B", + "/ko:oejungseong": "\u116C", + "/ko:yojungseong": "\u116D", + "/ko:ujungseong": "\u116E", + "/ko:weojungseong": "\u116F", + "/ko:wejungseong": "\u1170", + "/ko:wijungseong": "\u1171", + "/ko:yujungseong": "\u1172", + "/ko:eujungseong": "\u1173", + "/ko:yijungseong": "\u1174", + "/ko:ijungseong": "\u1175", + "/ko:aojungseong": "\u1176", + "/ko:aujungseong": "\u1177", + "/ko:yaojungseong": "\u1178", + "/ko:yayojungseong": "\u1179", + "/ko:eoojungseong": "\u117A", + "/ko:eoujungseong": "\u117B", + "/ko:eo_eujungseong": "\u117C", + "/ko:yeoojungseong": "\u117D", + "/ko:yeoujungseong": "\u117E", + "/ko:o_eojungseong": "\u117F", + "/ko:o_ejungseong": "\u1180", + "/ko:oyejungseong": "\u1181", + "/ko:oojungseong": "\u1182", + "/ko:oujungseong": "\u1183", + "/ko:yoyajungseong": "\u1184", + "/ko:yoyaejungseong": "\u1185", + "/ko:yoyeojungseong": "\u1186", + "/ko:yoojungseong": "\u1187", + "/ko:yoijungseong": "\u1188", + "/ko:uajungseong": "\u1189", + "/ko:uaejungseong": "\u118A", + "/ko:ueo_eujungseong": "\u118B", + "/ko:uyejungseong": "\u118C", + "/ko:uujungseong": "\u118D", + "/ko:yuajungseong": "\u118E", + "/ko:yueojungseong": "\u118F", + "/ko:yuejungseong": "\u1190", + "/ko:yuyeojungseong": "\u1191", + "/ko:yuyejungseong": "\u1192", + "/ko:yuujungseong": "\u1193", + "/ko:yuijungseong": "\u1194", + "/ko:euujungseong": "\u1195", + "/ko:eueujungseong": "\u1196", + "/ko:yiujungseong": "\u1197", + "/ko:iajungseong": "\u1198", + "/ko:iyajungseong": "\u1199", + "/ko:iojungseong": "\u119A", + "/ko:iujungseong": "\u119B", + "/ko:ieujungseong": "\u119C", + "/ko:iaraeajungseong": "\u119D", + "/ko:araeajungseong": "\u119E", + "/ko:araeaeojungseong": "\u119F", + "/ko:araeaujungseong": "\u11A0", + "/ko:araeaijungseong": "\u11A1", + "/ko:ssangaraeajungseong": "\u11A2", + "/ko:aeujungseong": "\u11A3", + "/ko:yaujungseong": "\u11A4", + "/ko:yeoyajungseong": "\u11A5", + "/ko:oyajungseong": "\u11A6", + "/ko:oyaejungseong": "\u11A7", + "/ko:kiyeokjongseong": "\u11A8", + "/ko:ssangkiyeokjongseong": "\u11A9", + "/ko:kiyeoksiosjongseong": "\u11AA", + "/ko:nieunjongseong": "\u11AB", + "/ko:nieuncieucjongseong": "\u11AC", + "/ko:nieunhieuhjongseong": "\u11AD", + "/ko:tikeutjongseong": "\u11AE", + "/ko:rieuljongseong": "\u11AF", + "/ko:rieulkiyeokjongseong": "\u11B0", + "/ko:rieulmieumjongseong": "\u11B1", + "/ko:rieulpieupjongseong": "\u11B2", + "/ko:rieulsiosjongseong": "\u11B3", + "/ko:rieulthieuthjongseong": "\u11B4", + "/ko:rieulphieuphjongseong": "\u11B5", + "/ko:rieulhieuhjongseong": "\u11B6", + "/ko:mieumjongseong": "\u11B7", + "/ko:pieupjongseong": "\u11B8", + "/ko:pieupsiosjongseong": "\u11B9", + "/ko:siosjongseong": "\u11BA", + "/ko:ssangsiosjongseong": "\u11BB", + "/ko:ieungjongseong": "\u11BC", + "/ko:cieucjongseong": "\u11BD", + "/ko:chieuchjongseong": "\u11BE", + "/ko:khieukhjongseong": "\u11BF", + "/ko:thieuthjongseong": "\u11C0", + "/ko:phieuphjongseong": "\u11C1", + "/ko:hieuhjongseong": "\u11C2", + "/ko:kiyeokrieuljongseong": "\u11C3", + "/ko:kiyeoksioskiyeokjongseong": "\u11C4", + "/ko:nieunkiyeokjongseong": "\u11C5", + "/ko:nieuntikeutjongseong": "\u11C6", + "/ko:nieunsiosjongseong": "\u11C7", + "/ko:nieunpansiosjongseong": "\u11C8", + "/ko:nieunthieuthjongseong": "\u11C9", + "/ko:tikeutkiyeokjongseong": "\u11CA", + "/ko:tikeutrieuljongseong": "\u11CB", + "/ko:rieulkiyeoksiosjongseong": "\u11CC", + "/ko:rieulnieunjongseong": "\u11CD", + "/ko:rieultikeutjongseong": "\u11CE", + "/ko:rieultikeuthieuhjongseong": "\u11CF", + "/ko:ssangrieuljongseong": "\u11D0", + "/ko:rieulmieumkiyeokjongseong": "\u11D1", + "/ko:rieulmieumsiosjongseong": "\u11D2", + "/ko:rieulpieupsiosjongseong": "\u11D3", + "/ko:rieulpieuphieuhjongseong": "\u11D4", + "/ko:rieulkapyeounpieupjongseong": "\u11D5", + "/ko:rieulssangsiosjongseong": "\u11D6", + "/ko:rieulpansiosjongseong": "\u11D7", + "/ko:rieulkhieukhjongseong": "\u11D8", + "/ko:rieulyeorinhieuhjongseong": "\u11D9", + "/ko:mieumkiyeokjongseong": "\u11DA", + "/ko:mieumrieuljongseong": "\u11DB", + "/ko:mieumpieupjongseong": "\u11DC", + "/ko:mieumsiosjongseong": "\u11DD", + "/ko:mieumssangsiosjongseong": "\u11DE", + "/ko:mieumpansiosjongseong": "\u11DF", + "/ko:mieumchieuchjongseong": "\u11E0", + "/ko:mieumhieuhjongseong": "\u11E1", + "/ko:kapyeounmieumjongseong": "\u11E2", + "/ko:pieuprieuljongseong": "\u11E3", + "/ko:pieupphieuphjongseong": "\u11E4", + "/ko:pieuphieuhjongseong": "\u11E5", + "/ko:kapyeounpieupjongseong": "\u11E6", + "/ko:sioskiyeokjongseong": "\u11E7", + "/ko:siostikeutjongseong": "\u11E8", + "/ko:siosrieuljongseong": "\u11E9", + "/ko:siospieupjongseong": "\u11EA", + "/ko:pansiosjongseong": "\u11EB", + "/ko:ieungkiyeokjongseong": "\u11EC", + "/ko:ieungssangkiyeokjongseong": "\u11ED", + "/ko:ssangieungjongseong": "\u11EE", + "/ko:ieungkhieukhjongseong": "\u11EF", + "/ko:yesieungjongseong": "\u11F0", + "/ko:yesieungsiosjongseong": "\u11F1", + "/ko:yesieungpansiosjongseong": "\u11F2", + "/ko:phieuphpieupjongseong": "\u11F3", + "/ko:kapyeounphieuphjongseong": "\u11F4", + "/ko:hieuhnieunjongseong": "\u11F5", + "/ko:hieuhrieuljongseong": "\u11F6", + "/ko:hieuhmieumjongseong": "\u11F7", + "/ko:hieuhpieupjongseong": "\u11F8", + "/ko:yeorinhieuhjongseong": "\u11F9", + "/ko:kiyeoknieunjongseong": "\u11FA", + "/ko:kiyeokpieupjongseong": "\u11FB", + "/ko:kiyeokchieuchjongseong": "\u11FC", + "/ko:kiyeokkhieukhjongseong": "\u11FD", + "/ko:kiyeokhieuhjongseong": "\u11FE", + "/ko:ssangnieunjongseong": "\u11FF", + "/ethi:ha": "\u1200", + "/ethi:hu": "\u1201", + "/ethi:hi": "\u1202", + "/ethi:haa": "\u1203", + "/ethi:hee": "\u1204", + "/ethi:he": "\u1205", + "/ethi:ho": "\u1206", + "/ethi:hoa": "\u1207", + "/ethi:la": "\u1208", + "/ethi:lu": "\u1209", + "/ethi:li": "\u120A", + "/ethi:laa": "\u120B", + "/ethi:lee": "\u120C", + "/ethi:le": "\u120D", + "/ethi:lo": "\u120E", + "/ethi:lwa": "\u120F", + "/ethi:hha": "\u1210", + "/ethi:hhu": "\u1211", + "/ethi:hhi": "\u1212", + "/ethi:hhaa": "\u1213", + "/ethi:hhee": "\u1214", + "/ethi:hhe": "\u1215", + "/ethi:hho": "\u1216", + "/ethi:hhwa": "\u1217", + "/ethi:ma": "\u1218", + "/ethi:mu": "\u1219", + "/ethi:mi": "\u121A", + "/ethi:maa": "\u121B", + "/ethi:mee": "\u121C", + "/ethi:me": "\u121D", + "/ethi:mo": "\u121E", + "/ethi:mwa": "\u121F", + "/ethi:sza": "\u1220", + "/ethi:szu": "\u1221", + "/ethi:szi": "\u1222", + "/ethi:szaa": "\u1223", + "/ethi:szee": "\u1224", + "/ethi:sze": "\u1225", + "/ethi:szo": "\u1226", + "/ethi:szwa": "\u1227", + "/ethi:ra": "\u1228", + "/ethi:ru": "\u1229", + "/ethi:ri": "\u122A", + "/ethi:raa": "\u122B", + "/ethi:ree": "\u122C", + "/ethi:re": "\u122D", + "/ethi:ro": "\u122E", + "/ethi:rwa": "\u122F", + "/ethi:sa": "\u1230", + "/ethi:su": "\u1231", + "/ethi:si": "\u1232", + "/ethi:saa": "\u1233", + "/ethi:see": "\u1234", + "/ethi:se": "\u1235", + "/ethi:so": "\u1236", + "/ethi:swa": "\u1237", + "/ethi:sha": "\u1238", + "/ethi:shu": "\u1239", + "/ethi:shi": "\u123A", + "/ethi:shaa": "\u123B", + "/ethi:shee": "\u123C", + "/ethi:she": "\u123D", + "/ethi:sho": "\u123E", + "/ethi:shwa": "\u123F", + "/ethi:qa": "\u1240", + "/ethi:qu": "\u1241", + "/ethi:qi": "\u1242", + "/ethi:qaa": "\u1243", + "/ethi:qee": "\u1244", + "/ethi:qe": "\u1245", + "/ethi:qo": "\u1246", + "/ethi:qoa": "\u1247", + "/ethi:qwa": "\u1248", + "/ethi:qwi": "\u124A", + "/ethi:qwaa": "\u124B", + "/ethi:qwee": "\u124C", + "/ethi:qwe": "\u124D", + "/ethi:qha": "\u1250", + "/ethi:qhu": "\u1251", + "/ethi:qhi": "\u1252", + "/ethi:qhaa": "\u1253", + "/ethi:qhee": "\u1254", + "/ethi:qhe": "\u1255", + "/ethi:qho": "\u1256", + "/ethi:qhwa": "\u1258", + "/ethi:qhwi": "\u125A", + "/ethi:qhwaa": "\u125B", + "/ethi:qhwee": "\u125C", + "/ethi:qhwe": "\u125D", + "/ethi:ba": "\u1260", + "/ethi:bu": "\u1261", + "/ethi:bi": "\u1262", + "/ethi:baa": "\u1263", + "/ethi:bee": "\u1264", + "/ethi:be": "\u1265", + "/ethi:bo": "\u1266", + "/ethi:bwa": "\u1267", + "/ethi:va": "\u1268", + "/ethi:vu": "\u1269", + "/ethi:vi": "\u126A", + "/ethi:vaa": "\u126B", + "/ethi:vee": "\u126C", + "/ethi:ve": "\u126D", + "/ethi:vo": "\u126E", + "/ethi:vwa": "\u126F", + "/ethi:ta": "\u1270", + "/ethi:tu": "\u1271", + "/ethi:ti": "\u1272", + "/ethi:taa": "\u1273", + "/ethi:tee": "\u1274", + "/ethi:te": "\u1275", + "/ethi:to": "\u1276", + "/ethi:twa": "\u1277", + "/ethi:ca": "\u1278", + "/ethi:cu": "\u1279", + "/ethi:ci": "\u127A", + "/ethi:caa": "\u127B", + "/ethi:cee": "\u127C", + "/ethi:ce": "\u127D", + "/ethi:co": "\u127E", + "/ethi:cwa": "\u127F", + "/ethi:xa": "\u1280", + "/ethi:xu": "\u1281", + "/ethi:xi": "\u1282", + "/ethi:xaa": "\u1283", + "/ethi:xee": "\u1284", + "/ethi:xe": "\u1285", + "/ethi:xo": "\u1286", + "/ethi:xoa": "\u1287", + "/ethi:xwa": "\u1288", + "/ethi:xwi": "\u128A", + "/ethi:xwaa": "\u128B", + "/ethi:xwee": "\u128C", + "/ethi:xwe": "\u128D", + "/ethi:na": "\u1290", + "/ethi:nu": "\u1291", + "/ethi:ni": "\u1292", + "/ethi:naa": "\u1293", + "/ethi:nee": "\u1294", + "/ethi:ne": "\u1295", + "/ethi:no": "\u1296", + "/ethi:nwa": "\u1297", + "/ethi:nya": "\u1298", + "/ethi:nyu": "\u1299", + "/ethi:nyi": "\u129A", + "/ethi:nyaa": "\u129B", + "/ethi:nyee": "\u129C", + "/ethi:nye": "\u129D", + "/ethi:nyo": "\u129E", + "/ethi:nywa": "\u129F", + "/ethi:aglottal": "\u12A0", + "/ethi:uglottal": "\u12A1", + "/ethi:iglottal": "\u12A2", + "/ethi:aaglottal": "\u12A3", + "/ethi:eeglottal": "\u12A4", + "/ethi:eglottal": "\u12A5", + "/ethi:oglottal": "\u12A6", + "/ethi:waglottal": "\u12A7", + "/ethi:ka": "\u12A8", + "/ethi:ku": "\u12A9", + "/ethi:ki": "\u12AA", + "/ethi:kaa": "\u12AB", + "/ethi:kee": "\u12AC", + "/ethi:ke": "\u12AD", + "/ethi:ko": "\u12AE", + "/ethi:koa": "\u12AF", + "/ethi:kwa": "\u12B0", + "/ethi:kwi": "\u12B2", + "/ethi:kwaa": "\u12B3", + "/ethi:kwee": "\u12B4", + "/ethi:kwe": "\u12B5", + "/ethi:kxa": "\u12B8", + "/ethi:kxu": "\u12B9", + "/ethi:kxi": "\u12BA", + "/ethi:kxaa": "\u12BB", + "/ethi:kxee": "\u12BC", + "/ethi:kxe": "\u12BD", + "/ethi:kxo": "\u12BE", + "/ethi:kxwa": "\u12C0", + "/ethi:kxwi": "\u12C2", + "/ethi:kxwaa": "\u12C3", + "/ethi:kxwee": "\u12C4", + "/ethi:kxwe": "\u12C5", + "/ethi:wa": "\u12C8", + "/ethi:wu": "\u12C9", + "/ethi:wi": "\u12CA", + "/ethi:waa": "\u12CB", + "/ethi:wee": "\u12CC", + "/ethi:we": "\u12CD", + "/ethi:wo": "\u12CE", + "/ethi:woa": "\u12CF", + "/ethi:pharyngeala": "\u12D0", + "/ethi:pharyngealu": "\u12D1", + "/ethi:pharyngeali": "\u12D2", + "/ethi:pharyngealaa": "\u12D3", + "/ethi:pharyngealee": "\u12D4", + "/ethi:pharyngeale": "\u12D5", + "/ethi:pharyngealo": "\u12D6", + "/ethi:za": "\u12D8", + "/ethi:zu": "\u12D9", + "/ethi:zi": "\u12DA", + "/ethi:zaa": "\u12DB", + "/ethi:zee": "\u12DC", + "/ethi:ze": "\u12DD", + "/ethi:zo": "\u12DE", + "/ethi:zwa": "\u12DF", + "/ethi:zha": "\u12E0", + "/ethi:zhu": "\u12E1", + "/ethi:zhi": "\u12E2", + "/ethi:zhaa": "\u12E3", + "/ethi:zhee": "\u12E4", + "/ethi:zhe": "\u12E5", + "/ethi:zho": "\u12E6", + "/ethi:zhwa": "\u12E7", + "/ethi:ya": "\u12E8", + "/ethi:yu": "\u12E9", + "/ethi:yi": "\u12EA", + "/ethi:yaa": "\u12EB", + "/ethi:yee": "\u12EC", + "/ethi:ye": "\u12ED", + "/ethi:yo": "\u12EE", + "/ethi:yoa": "\u12EF", + "/ethi:da": "\u12F0", + "/ethi:du": "\u12F1", + "/ethi:di": "\u12F2", + "/ethi:daa": "\u12F3", + "/ethi:dee": "\u12F4", + "/ethi:de": "\u12F5", + "/ethi:do": "\u12F6", + "/ethi:dwa": "\u12F7", + "/ethi:dda": "\u12F8", + "/ethi:ddu": "\u12F9", + "/ethi:ddi": "\u12FA", + "/ethi:ddaa": "\u12FB", + "/ethi:ddee": "\u12FC", + "/ethi:dde": "\u12FD", + "/ethi:ddo": "\u12FE", + "/ethi:ddwa": "\u12FF", + "/ethi:ja": "\u1300", + "/ethi:ju": "\u1301", + "/ethi:ji": "\u1302", + "/ethi:jaa": "\u1303", + "/ethi:jee": "\u1304", + "/ethi:je": "\u1305", + "/ethi:jo": "\u1306", + "/ethi:jwa": "\u1307", + "/ethi:ga": "\u1308", + "/ethi:gu": "\u1309", + "/ethi:gi": "\u130A", + "/ethi:gaa": "\u130B", + "/ethi:gee": "\u130C", + "/ethi:ge": "\u130D", + "/ethi:go": "\u130E", + "/ethi:goa": "\u130F", + "/ethi:gwa": "\u1310", + "/ethi:gwi": "\u1312", + "/ethi:gwaa": "\u1313", + "/ethi:gwee": "\u1314", + "/ethi:gwe": "\u1315", + "/ethi:gga": "\u1318", + "/ethi:ggu": "\u1319", + "/ethi:ggi": "\u131A", + "/ethi:ggaa": "\u131B", + "/ethi:ggee": "\u131C", + "/ethi:gge": "\u131D", + "/ethi:ggo": "\u131E", + "/ethi:ggwaa": "\u131F", + "/ethi:tha": "\u1320", + "/ethi:thu": "\u1321", + "/ethi:thi": "\u1322", + "/ethi:thaa": "\u1323", + "/ethi:thee": "\u1324", + "/ethi:the": "\u1325", + "/ethi:tho": "\u1326", + "/ethi:thwa": "\u1327", + "/ethi:cha": "\u1328", + "/ethi:chu": "\u1329", + "/ethi:chi": "\u132A", + "/ethi:chaa": "\u132B", + "/ethi:chee": "\u132C", + "/ethi:che": "\u132D", + "/ethi:cho": "\u132E", + "/ethi:chwa": "\u132F", + "/ethi:pha": "\u1330", + "/ethi:phu": "\u1331", + "/ethi:phi": "\u1332", + "/ethi:phaa": "\u1333", + "/ethi:phee": "\u1334", + "/ethi:phe": "\u1335", + "/ethi:pho": "\u1336", + "/ethi:phwa": "\u1337", + "/ethi:tsa": "\u1338", + "/ethi:tsu": "\u1339", + "/ethi:tsi": "\u133A", + "/ethi:tsaa": "\u133B", + "/ethi:tsee": "\u133C", + "/ethi:tse": "\u133D", + "/ethi:tso": "\u133E", + "/ethi:tswa": "\u133F", + "/ethi:tza": "\u1340", + "/ethi:tzu": "\u1341", + "/ethi:tzi": "\u1342", + "/ethi:tzaa": "\u1343", + "/ethi:tzee": "\u1344", + "/ethi:tze": "\u1345", + "/ethi:tzo": "\u1346", + "/ethi:tzoa": "\u1347", + "/ethi:fa": "\u1348", + "/ethi:fu": "\u1349", + "/ethi:fi": "\u134A", + "/ethi:faa": "\u134B", + "/ethi:fee": "\u134C", + "/ethi:fe": "\u134D", + "/ethi:fo": "\u134E", + "/ethi:fwa": "\u134F", + "/ethi:pa": "\u1350", + "/ethi:pu": "\u1351", + "/ethi:pi": "\u1352", + "/ethi:paa": "\u1353", + "/ethi:pee": "\u1354", + "/ethi:pe": "\u1355", + "/ethi:po": "\u1356", + "/ethi:pwa": "\u1357", + "/ethi:rya": "\u1358", + "/ethi:mya": "\u1359", + "/ethi:fya": "\u135A", + "/ethi:geminationandvowellengthmarkcmb": "\u135D", + "/ethi:vowellengthmarkcmb": "\u135E", + "/ethi:geminationmarkcmb": "\u135F", + "/ethi:sectionmark": "\u1360", + "/ethi:wordspace": "\u1361", + "/ethi:fullstop": "\u1362", + "/ethi:comma": "\u1363", + "/ethi:semicolon": "\u1364", + "/ethi:colon": "\u1365", + "/ethi:prefacecolon": "\u1366", + "/ethi:questionmark": "\u1367", + "/ethi:paragraphseparator": "\u1368", + "/ethi:one": "\u1369", + "/ethi:two": "\u136A", + "/ethi:three": "\u136B", + "/ethi:four": "\u136C", + "/ethi:five": "\u136D", + "/ethi:six": "\u136E", + "/ethi:seven": "\u136F", + "/ethi:eight": "\u1370", + "/ethi:nine": "\u1371", + "/ethi:ten": "\u1372", + "/ethi:twenty": "\u1373", + "/ethi:thirty": "\u1374", + "/ethi:forty": "\u1375", + "/ethi:fifty": "\u1376", + "/ethi:sixty": "\u1377", + "/ethi:seventy": "\u1378", + "/ethi:eighty": "\u1379", + "/ethi:ninety": "\u137A", + "/ethi:hundred": "\u137B", + "/ethi:tenthousand": "\u137C", + "/ethi:mwasebatbeit": "\u1380", + "/ethi:mwi": "\u1381", + "/ethi:mwee": "\u1382", + "/ethi:mwe": "\u1383", + "/ethi:bwasebatbeit": "\u1384", + "/ethi:bwi": "\u1385", + "/ethi:bwee": "\u1386", + "/ethi:bwe": "\u1387", + "/ethi:fwasebatbeit": "\u1388", + "/ethi:fwi": "\u1389", + "/ethi:fwee": "\u138A", + "/ethi:fwe": "\u138B", + "/ethi:pwasebatbeit": "\u138C", + "/ethi:pwi": "\u138D", + "/ethi:pwee": "\u138E", + "/ethi:pwe": "\u138F", + "/ethi:yizet": "\u1390", + "/ethi:deret": "\u1391", + "/ethi:rikrik": "\u1392", + "/ethi:shortrikrik": "\u1393", + "/ethi:difat": "\u1394", + "/ethi:kenat": "\u1395", + "/ethi:chiret": "\u1396", + "/ethi:hidet": "\u1397", + "/ethi:derethidet": "\u1398", + "/ethi:kurt": "\u1399", + "/cher:a": "\u13A0", + "/cher:e": "\u13A1", + "/cher:i": "\u13A2", + "/cher:o": "\u13A3", + "/cher:u": "\u13A4", + "/cher:v": "\u13A5", + "/cher:ga": "\u13A6", + "/cher:ka": "\u13A7", + "/cher:ge": "\u13A8", + "/cher:gi": "\u13A9", + "/cher:go": "\u13AA", + "/cher:gu": "\u13AB", + "/cher:gv": "\u13AC", + "/cher:ha": "\u13AD", + "/cher:he": "\u13AE", + "/cher:hi": "\u13AF", + "/cher:ho": "\u13B0", + "/cher:hu": "\u13B1", + "/cher:hv": "\u13B2", + "/cher:la": "\u13B3", + "/cher:le": "\u13B4", + "/cher:li": "\u13B5", + "/cher:lo": "\u13B6", + "/cher:lu": "\u13B7", + "/cher:lv": "\u13B8", + "/cher:ma": "\u13B9", + "/cher:me": "\u13BA", + "/cher:mi": "\u13BB", + "/cher:mo": "\u13BC", + "/cher:mu": "\u13BD", + "/cher:na": "\u13BE", + "/cher:hna": "\u13BF", + "/cher:nah": "\u13C0", + "/cher:ne": "\u13C1", + "/cher:ni": "\u13C2", + "/cher:no": "\u13C3", + "/cher:nu": "\u13C4", + "/cher:nv": "\u13C5", + "/cher:qua": "\u13C6", + "/cher:que": "\u13C7", + "/cher:qui": "\u13C8", + "/cher:quo": "\u13C9", + "/cher:quu": "\u13CA", + "/cher:quv": "\u13CB", + "/cher:sa": "\u13CC", + "/cher:s": "\u13CD", + "/cher:se": "\u13CE", + "/cher:si": "\u13CF", + "/cher:so": "\u13D0", + "/cher:su": "\u13D1", + "/cher:sv": "\u13D2", + "/cher:da": "\u13D3", + "/cher:ta": "\u13D4", + "/cher:de": "\u13D5", + "/cher:te": "\u13D6", + "/cher:di": "\u13D7", + "/cher:ti": "\u13D8", + "/cher:do": "\u13D9", + "/cher:du": "\u13DA", + "/cher:dv": "\u13DB", + "/cher:dla": "\u13DC", + "/cher:tla": "\u13DD", + "/cher:tle": "\u13DE", + "/cher:tli": "\u13DF", + "/cher:tlo": "\u13E0", + "/cher:tlu": "\u13E1", + "/cher:tlv": "\u13E2", + "/cher:tsa": "\u13E3", + "/cher:tse": "\u13E4", + "/cher:tsi": "\u13E5", + "/cher:tso": "\u13E6", + "/cher:tsu": "\u13E7", + "/cher:tsv": "\u13E8", + "/cher:wa": "\u13E9", + "/cher:we": "\u13EA", + "/cher:wi": "\u13EB", + "/cher:wo": "\u13EC", + "/cher:wu": "\u13ED", + "/cher:wv": "\u13EE", + "/cher:ya": "\u13EF", + "/cher:ye": "\u13F0", + "/cher:yi": "\u13F1", + "/cher:yo": "\u13F2", + "/cher:yu": "\u13F3", + "/cher:yv": "\u13F4", + "/cher:mv": "\u13F5", + "/cher:yesmall": "\u13F8", + "/cher:yismall": "\u13F9", + "/cher:yosmall": "\u13FA", + "/cher:yusmall": "\u13FB", + "/cher:yvsmall": "\u13FC", + "/cher:mvsmall": "\u13FD", + "/runr:fehu_feoh_fe_f": "\u16A0", + "/runr:v": "\u16A1", + "/runr:uruz_ur_u": "\u16A2", + "/runr:yr": "\u16A3", + "/runr:y": "\u16A4", + "/runr:w": "\u16A5", + "/runr:thurisaz_thurs_thorn": "\u16A6", + "/runr:eth": "\u16A7", + "/runr:ansuz_a": "\u16A8", + "/runr:os_o": "\u16A9", + "/runr:ac_a": "\u16AA", + "/runr:aesc": "\u16AB", + "/runr:long_branch_oss_o": "\u16AC", + "/runr:short_twig_oss_o": "\u16AD", + "/runr:o": "\u16AE", + "/runr:oe": "\u16AF", + "/runr:on": "\u16B0", + "/runr:raido_rad_reid_r": "\u16B1", + "/runr:kauna": "\u16B2", + "/runr:cen": "\u16B3", + "/runr:kaun_k": "\u16B4", + "/runr:g": "\u16B5", + "/runr:eng": "\u16B6", + "/runr:gebo_gyfu_g": "\u16B7", + "/runr:gar": "\u16B8", + "/runr:wunjo_wynn_w": "\u16B9", + "/runr:haglaz_h": "\u16BA", + "/runr:haegl_h": "\u16BB", + "/runr:long_branch_hagall_h": "\u16BC", + "/runr:short_twig_hagall_h": "\u16BD", + "/runr:naudiz_nyd_naud_n": "\u16BE", + "/runr:short_twig_naud_n": "\u16BF", + "/runr:dotted_n": "\u16C0", + "/runr:isaz_is_iss_i": "\u16C1", + "/runr:e": "\u16C2", + "/runr:jeran_j": "\u16C3", + "/runr:ger": "\u16C4", + "/runr:long_branch_ar_ae": "\u16C5", + "/runr:short_twig_ar_a": "\u16C6", + "/runr:iwaz_eoh": "\u16C7", + "/runr:pertho_peorth_p": "\u16C8", + "/runr:algiz_eolhx": "\u16C9", + "/runr:sowilo_s": "\u16CA", + "/runr:sigel_long_branch_sol_s": "\u16CB", + "/runr:short_twig_sol_s": "\u16CC", + "/runr:c": "\u16CD", + "/runr:z": "\u16CE", + "/runr:tiwaz_tir_tyr_t": "\u16CF", + "/runr:short_twig_tyr_t": "\u16D0", + "/runr:d": "\u16D1", + "/runr:berkanan_beorc_bjarkan_b": "\u16D2", + "/runr:short_twig_bjarkan_b": "\u16D3", + "/runr:dotted_p": "\u16D4", + "/runr:open_p": "\u16D5", + "/runr:ehwaz_eh_e": "\u16D6", + "/runr:mannaz_man_m": "\u16D7", + "/runr:long_branch_madr_m": "\u16D8", + "/runr:short_twig_madr_m": "\u16D9", + "/runr:laukaz_lagu_logr_l": "\u16DA", + "/runr:dotted_l": "\u16DB", + "/runr:ingwaz": "\u16DC", + "/runr:ing": "\u16DD", + "/runr:dagaz_daeg_d": "\u16DE", + "/runr:othalan_ethel_o": "\u16DF", + "/runr:ear": "\u16E0", + "/runr:ior": "\u16E1", + "/runr:cweorth": "\u16E2", + "/runr:calc": "\u16E3", + "/runr:cealc": "\u16E4", + "/runr:stan": "\u16E5", + "/runr:long_branch_yr": "\u16E6", + "/runr:short_twig_yr": "\u16E7", + "/runr:icelandic_yr": "\u16E8", + "/runr:q": "\u16E9", + "/runr:x": "\u16EA", + "/runr:single_punctuation": "\u16EB", + "/runr:multiple_punctuation": "\u16EC", + "/runr:cross_punctuation": "\u16ED", + "/runr:arlaug_symbol": "\u16EE", + "/runr:tvimadur_symbol": "\u16EF", + "/runr:belgthor_symbol": "\u16F0", + "/runr:k": "\u16F1", + "/runr:sh": "\u16F2", + "/runr:oo": "\u16F3", + "/runr:franks_casket_os": "\u16F4", + "/runr:franks_casket_is": "\u16F5", + "/runr:franks_casket_eh": "\u16F6", + "/runr:franks_casket_ac": "\u16F7", + "/runr:franks_casket_aesc": "\u16F8", + "/mong:birga": "\u1800", + "/mong:ellipsis": "\u1801", + "/mong:comma": "\u1802", + "/mong:period": "\u1803", + "/mong:colon": "\u1804", + "/mong:fourdots": "\u1805", + "/mong:softhyphentodo": "\u1806", + "/mong:syllableboundarymarkersibe": "\u1807", + "/mong:commamanchu": "\u1808", + "/mong:periodmanchu": "\u1809", + "/mong:nirugu": "\u180A", + "/mong:freevariationselectorone": "\u180B", + "/mong:freevariationselectortwo": "\u180C", + "/mong:freevariationselectorthree": "\u180D", + "/mong:vowelseparator": "\u180E", + "/mong:freevariationselectorfour": "\u180F", + "/mong:zero": "\u1810", + "/mong:one": "\u1811", + "/mong:two": "\u1812", + "/mong:three": "\u1813", + "/mong:four": "\u1814", + "/mong:five": "\u1815", + "/mong:six": "\u1816", + "/mong:seven": "\u1817", + "/mong:eight": "\u1818", + "/mong:nine": "\u1819", + "/mong:a": "\u1820", + "/mong:e": "\u1821", + "/mong:i": "\u1822", + "/mong:o": "\u1823", + "/mong:u": "\u1824", + "/mong:oe": "\u1825", + "/mong:ue": "\u1826", + "/mong:ee": "\u1827", + "/mong:na": "\u1828", + "/mong:ang": "\u1829", + "/mong:ba": "\u182A", + "/mong:pa": "\u182B", + "/mong:qa": "\u182C", + "/mong:ga": "\u182D", + "/mong:ma": "\u182E", + "/mong:la": "\u182F", + "/mong:sa": "\u1830", + "/mong:sha": "\u1831", + "/mong:ta": "\u1832", + "/mong:da": "\u1833", + "/mong:cha": "\u1834", + "/mong:ja": "\u1835", + "/mong:ya": "\u1836", + "/mong:ra": "\u1837", + "/mong:wa": "\u1838", + "/mong:fa": "\u1839", + "/mong:ka": "\u183A", + "/mong:kha": "\u183B", + "/mong:tsa": "\u183C", + "/mong:za": "\u183D", + "/mong:haa": "\u183E", + "/mong:zra": "\u183F", + "/mong:lha": "\u1840", + "/mong:zhi": "\u1841", + "/mong:chi": "\u1842", + "/mong:longvowelsigntodo": "\u1843", + "/mong:etodo": "\u1844", + "/mong:itodo": "\u1845", + "/mong:otodo": "\u1846", + "/mong:utodo": "\u1847", + "/mong:oetodo": "\u1848", + "/mong:uetodo": "\u1849", + "/mong:angtodo": "\u184A", + "/mong:batodo": "\u184B", + "/mong:patodo": "\u184C", + "/mong:qatodo": "\u184D", + "/mong:gatodo": "\u184E", + "/mong:matodo": "\u184F", + "/mong:tatodo": "\u1850", + "/mong:datodo": "\u1851", + "/mong:chatodo": "\u1852", + "/mong:jatodo": "\u1853", + "/mong:tsatodo": "\u1854", + "/mong:yatodo": "\u1855", + "/mong:watodo": "\u1856", + "/mong:katodo": "\u1857", + "/mong:gaatodo": "\u1858", + "/mong:haatodo": "\u1859", + "/mong:jiatodo": "\u185A", + "/mong:niatodo": "\u185B", + "/mong:dzatodo": "\u185C", + "/mong:esibe": "\u185D", + "/mong:isibe": "\u185E", + "/mong:iysibe": "\u185F", + "/mong:uesibe": "\u1860", + "/mong:usibe": "\u1861", + "/mong:angsibe": "\u1862", + "/mong:kasibe": "\u1863", + "/mong:gasibe": "\u1864", + "/mong:hasibe": "\u1865", + "/mong:pasibe": "\u1866", + "/mong:shasibe": "\u1867", + "/mong:tasibe": "\u1868", + "/mong:dasibe": "\u1869", + "/mong:jasibe": "\u186A", + "/mong:fasibe": "\u186B", + "/mong:gaasibe": "\u186C", + "/mong:haasibe": "\u186D", + "/mong:tsasibe": "\u186E", + "/mong:zasibe": "\u186F", + "/mong:raasibe": "\u1870", + "/mong:chasibe": "\u1871", + "/mong:zhasibe": "\u1872", + "/mong:imanchu": "\u1873", + "/mong:kamanchu": "\u1874", + "/mong:ramanchu": "\u1875", + "/mong:famanchu": "\u1876", + "/mong:zhamanchu": "\u1877", + "/mong:chawithtwodots": "\u1878", + "/mong:anusvaraonealigali": "\u1880", + "/mong:visargaonealigali": "\u1881", + "/mong:damarualigali": "\u1882", + "/mong:ubadamaaligali": "\u1883", + "/mong:ubadamaaligaliinverted": "\u1884", + "/mong:baludaaligali": "\u1885", + "/mong:baludaaligalithree": "\u1886", + "/mong:aaligali": "\u1887", + "/mong:ialigali": "\u1888", + "/mong:kaaligali": "\u1889", + "/mong:ngaaligali": "\u188A", + "/mong:caaligali": "\u188B", + "/mong:ttaaligali": "\u188C", + "/mong:tthaaligali": "\u188D", + "/mong:ddaaligali": "\u188E", + "/mong:nnaaligali": "\u188F", + "/mong:taaligali": "\u1890", + "/mong:daaligali": "\u1891", + "/mong:paaligali": "\u1892", + "/mong:phaaligali": "\u1893", + "/mong:ssaaligali": "\u1894", + "/mong:zhaaligali": "\u1895", + "/mong:zaaligali": "\u1896", + "/mong:ahaligali": "\u1897", + "/mong:tatodoaligali": "\u1898", + "/mong:zhatodoaligali": "\u1899", + "/mong:ghamanchualigali": "\u189A", + "/mong:ngamanchualigali": "\u189B", + "/mong:camanchualigali": "\u189C", + "/mong:jhamanchualigali": "\u189D", + "/mong:ttamanchualigali": "\u189E", + "/mong:ddhamanchualigali": "\u189F", + "/mong:tamanchualigali": "\u18A0", + "/mong:dhamanchualigali": "\u18A1", + "/mong:ssamanchualigali": "\u18A2", + "/mong:cyamanchualigali": "\u18A3", + "/mong:zhamanchualigali": "\u18A4", + "/mong:zamanchualigali": "\u18A5", + "/mong:ualigalihalf": "\u18A6", + "/mong:yaaligalihalf": "\u18A7", + "/mong:bhamanchualigali": "\u18A8", + "/mong:dagalgaaligali": "\u18A9", + "/mong:lhamanchualigali": "\u18AA", + "/AnGeor": "\u1C90", + "/BanGeor": "\u1C91", + "/GanGeor": "\u1C92", + "/DonGeor": "\u1C93", + "/EnGeor": "\u1C94", + "/VinGeor": "\u1C95", + "/ZenGeor": "\u1C96", + "/TanGeor": "\u1C97", + "/InGeor": "\u1C98", + "/KanGeor": "\u1C99", + "/LasGeor": "\u1C9A", + "/ManGeor": "\u1C9B", + "/NarGeor": "\u1C9C", + "/OnGeor": "\u1C9D", + "/ParGeor": "\u1C9E", + "/ZharGeor": "\u1C9F", + "/RaeGeor": "\u1CA0", + "/SanGeor": "\u1CA1", + "/TarGeor": "\u1CA2", + "/UnGeor": "\u1CA3", + "/PharGeor": "\u1CA4", + "/KharGeor": "\u1CA5", + "/GhanGeor": "\u1CA6", + "/QarGeor": "\u1CA7", + "/ShinGeor": "\u1CA8", + "/ChinGeor": "\u1CA9", + "/CanGeor": "\u1CAA", + "/JilGeor": "\u1CAB", + "/CilGeor": "\u1CAC", + "/CharGeor": "\u1CAD", + "/XanGeor": "\u1CAE", + "/JhanGeor": "\u1CAF", + "/HaeGeor": "\u1CB0", + "/HeGeor": "\u1CB1", + "/HieGeor": "\u1CB2", + "/WeGeor": "\u1CB3", + "/HarGeor": "\u1CB4", + "/HoeGeor": "\u1CB5", + "/FiGeor": "\u1CB6", + "/YnGeor": "\u1CB7", + "/ElifiGeor": "\u1CB8", + "/TurnedganGeor": "\u1CB9", + "/AinGeor": "\u1CBA", + "/AenGeor": "\u1CBD", + "/HardsignGeor": "\u1CBE", + "/LabialsignGeor": "\u1CBF", + "/ve:karshanatone": "\u1CD0", + "/ve:sharatone": "\u1CD1", + "/ve:prenkhatone": "\u1CD2", + "/ve:nihshvasasign": "\u1CD3", + "/ve:yajurmidlinesvaritasign": "\u1CD4", + "/ve:yajuraggravatedindependentsvaritatone": "\u1CD5", + "/ve:yajurindependentsvaritatone": "\u1CD6", + "/ve:yajurkathakaindependentsvaritatone": "\u1CD7", + "/ve:belowtonecandra": "\u1CD8", + "/ve:yajurkathakaindependentsvaritaschroedertone": "\u1CD9", + "/ve:svaritatonedbl": "\u1CDA", + "/ve:svaritatonetpl": "\u1CDB", + "/ve:kathakaanudattatone": "\u1CDC", + "/ve:dotbelowtone": "\u1CDD", + "/ve:twodotsbelowtone": "\u1CDE", + "/ve:threedotsbelowtone": "\u1CDF", + "/ve:rigkashmiriindependentsvaritatone": "\u1CE0", + "/ve:atharvaindependentsvaritatone": "\u1CE1", + "/ve:visargasvaritasign": "\u1CE2", + "/ve:visargaudattasign": "\u1CE3", + "/ve:visargaudattasignreversed": "\u1CE4", + "/ve:visargaanudattasign": "\u1CE5", + "/ve:visargaanudattasignreversed": "\u1CE6", + "/ve:visargaudattawithtailsign": "\u1CE7", + "/ve:visargaanudattawithtailsign": "\u1CE8", + "/ve:anusvaraantargomukhasign": "\u1CE9", + "/ve:anusvarabahirgomukhasign": "\u1CEA", + "/ve:anusvaravamagomukhasign": "\u1CEB", + "/ve:anusvaravamagomukhawithtailsign": "\u1CEC", + "/ve:tiryaksign": "\u1CED", + "/ve:hexiformanusvarasignlong": "\u1CEE", + "/ve:anusvarasignlong": "\u1CEF", + "/ve:rthanganusvarasignlong": "\u1CF0", + "/ve:anusvaraubhayatomukhasign": "\u1CF1", + "/ve:ardhavisargasign": "\u1CF2", + "/ve:rotatedardhavisargasign": "\u1CF3", + "/ve:abovetonecandra": "\u1CF4", + "/ve:jihvamuliyasign": "\u1CF5", + "/ve:upadhmaniyasign": "\u1CF6", + "/ve:atikramasign": "\u1CF7", + "/ve:ringabovetone": "\u1CF8", + "/ve:ringabovetonedbl": "\u1CF9", + "/ve:anusvaraantargomukhasigndbl": "\u1CFA", + "/aeturned": "\u1D02", + "/Bbarsmall": "\u1D03", + "/eopenturned": "\u1D08", + "/iturned": "\u1D09", + "/Lsmallstroke": "\u1D0C", + "/Nsmallreversed": "\u1D0E", + "/Oopensmall": "\u1D10", + "/osideways": "\u1D11", + "/oopensideways": "\u1D12", + "/ostrokesideways": "\u1D13", + "/oeturned": "\u1D14", + "/OUsmall": "\u1D15", + "/otophalf": "\u1D16", + "/obottomhalf": "\u1D17", + "/Rsmallreversed": "\u1D19", + "/Rsmallturned": "\u1D1A", + "/usideways": "\u1D1D", + "/usidewaysdieresised": "\u1D1E", + "/mturnedsideways": "\u1D1F", + "/Ezhsmall": "\u1D23", + "/spirantvoicedlaryngeal": "\u1D24", + "/phon:ain": "\u1D25", + "/gr:Gammasmall": "\u1D26", + "/gr:Lambdasmall": "\u1D27", + "/gr:Pismall": "\u1D28", + "/gr:RsmallHO": "\u1D29", + "/gr:Psismall": "\u1D2A", + "/Elsmallcyr": "\u1D2B", + "/Amod": "\u1D2C", + "/Aemod": "\u1D2D", + "/Bmod": "\u1D2E", + "/Bbarmod": "\u1D2F", + "/Dmod": "\u1D30", + "/Emod": "\u1D31", + "/Ereversedmod": "\u1D32", + "/Gmod": "\u1D33", + "/Hmod": "\u1D34", + "/Imod": "\u1D35", + "/Jmod": "\u1D36", + "/Kmod": "\u1D37", + "/Lmod": "\u1D38", + "/Mmod": "\u1D39", + "/Nmod": "\u1D3A", + "/Nreversedmod": "\u1D3B", + "/Omod": "\u1D3C", + "/Oumod": "\u1D3D", + "/Pmod": "\u1D3E", + "/Rmod": "\u1D3F", + "/Tmod": "\u1D40", + "/Umod": "\u1D41", + "/Wmod": "\u1D42", + "/amod": "\u1D43", + "/aturnedmod": "\u1D44", + "/alphamod": "\u1D45", + "/aeturnedmod": "\u1D46", + "/bmod": "\u1D47", + "/dmod": "\u1D48", + "/emod": "\u1D49", + "/schwamod": "\u1D4A", + "/eopenmod": "\u1D4B", + "/eopenturnedmod": "\u1D4C", + "/gmod": "\u1D4D", + "/iturnedmod": "\u1D4E", + "/kmod": "\u1D4F", + "/mmod": "\u1D50", + "/engmod": "\u1D51", + "/omod": "\u1D52", + "/oopenmod": "\u1D53", + "/otophalfmod": "\u1D54", + "/obottomhalfmod": "\u1D55", + "/pmod": "\u1D56", + "/tmod": "\u1D57", + "/umod": "\u1D58", + "/usidewaysmod": "\u1D59", + "/mturnedmod": "\u1D5A", + "/vmod": "\u1D5B", + "/phon:ainmod": "\u1D5C", + "/betamod": "\u1D5D", + "/gr:gammamod": "\u1D5E", + "/deltamod": "\u1D5F", + "/gr:phimod": "\u1D60", + "/chimod": "\u1D61", + "/isubscript": "\u1D62", + "/rsubscript": "\u1D63", + "/usubscript": "\u1D64", + "/vsubscript": "\u1D65", + "/gr:betasubscript": "\u1D66", + "/gr:gammasubscript": "\u1D67", + "/gr:rhosubscript": "\u1D68", + "/gr:phisubscript": "\u1D69", + "/gr:chisubscript": "\u1D6A", + "/ue": "\u1D6B", + "/bmiddletilde": "\u1D6C", + "/dmiddletilde": "\u1D6D", + "/fmiddletilde": "\u1D6E", + "/mmiddletilde": "\u1D6F", + "/nmiddletilde": "\u1D70", + "/pmiddletilde": "\u1D71", + "/rmiddletilde": "\u1D72", + "/rfishmiddletilde": "\u1D73", + "/smiddletilde": "\u1D74", + "/tmiddletilde": "\u1D75", + "/zmiddletilde": "\u1D76", + "/gturned": "\u1D77", + "/ENcyrmod": "\u1D78", + "/ginsular": "\u1D79", + "/thstrike": "\u1D7A", + "/Ismallstroke": "\u1D7B", + "/iotastroke": "\u1D7C", + "/pstroke": "\u1D7D", + "/Usmallstroke": "\u1D7E", + "/upsilonstroke": "\u1D7F", + "/bpalatalhook": "\u1D80", + "/dpalatalhook": "\u1D81", + "/fpalatalhook": "\u1D82", + "/gpalatalhook": "\u1D83", + "/kpalatalhook": "\u1D84", + "/lpalatalhook": "\u1D85", + "/mpalatalhook": "\u1D86", + "/npalatalhook": "\u1D87", + "/ppalatalhook": "\u1D88", + "/rpalatalhook": "\u1D89", + "/spalatalhook": "\u1D8A", + "/eshpalatalhook": "\u1D8B", + "/vpalatalhook": "\u1D8C", + "/xpalatalhook": "\u1D8D", + "/zpalatalhook": "\u1D8E", + "/aretroflexhook": "\u1D8F", + "/alpharetroflexhook": "\u1D90", + "/dhooktail": "\u1D91", + "/eretroflexhook": "\u1D92", + "/eopenretroflexhook": "\u1D93", + "/eopenreversedretroflexhook": "\u1D94", + "/schwaretroflexhook": "\u1D95", + "/iretroflexhook": "\u1D96", + "/oopenretroflexhook": "\u1D97", + "/eshretroflexhook": "\u1D98", + "/uretroflexhook": "\u1D99", + "/ezhretroflexhook": "\u1D9A", + "/alphaturnedmod": "\u1D9B", + "/cmod": "\u1D9C", + "/ccurlmod": "\u1D9D", + "/ethmod": "\u1D9E", + "/eopenreversedmod": "\u1D9F", + "/fmod": "\u1DA0", + "/dotlessjstrokemod": "\u1DA1", + "/gscriptmod": "\u1DA2", + "/hturnedmod": "\u1DA3", + "/istrokemod": "\u1DA4", + "/iotamod": "\u1DA5", + "/Ismallmod": "\u1DA6", + "/Istrokesmallmod": "\u1DA7", + "/jcrossedtailmod": "\u1DA8", + "/lretroflexhookmod": "\u1DA9", + "/lpalatalhookmod": "\u1DAA", + "/Lsmallmod": "\u1DAB", + "/mhookmod": "\u1DAC", + "/mlonglegturnedmod": "\u1DAD", + "/nlefthookmod": "\u1DAE", + "/nretroflexhookmod": "\u1DAF", + "/Nsmallmod": "\u1DB0", + "/obarmod": "\u1DB1", + "/phimod": "\u1DB2", + "/shookmod": "\u1DB3", + "/eshmod": "\u1DB4", + "/tpalatalhookmod": "\u1DB5", + "/ubarmod": "\u1DB6", + "/upsilonmod": "\u1DB7", + "/Usmallmod": "\u1DB8", + "/vhookmod": "\u1DB9", + "/vturnedmod": "\u1DBA", + "/zmod": "\u1DBB", + "/zretroflexhookmod": "\u1DBC", + "/zcurlmod": "\u1DBD", + "/ezhmod": "\u1DBE", + "/thetamod": "\u1DBF", + "/dottedgravecmb": "\u1DC0", + "/dottedacutecmb": "\u1DC1", + "/snakebelowcmb": "\u1DC2", + "/suspensionmarkcmb": "\u1DC3", + "/macronacutecmb": "\u1DC4", + "/gravemacroncmb": "\u1DC5", + "/macrongravecmb": "\u1DC6", + "/acutemacroncmb": "\u1DC7", + "/graveacutegravecmb": "\u1DC8", + "/acutegraveacutecmb": "\u1DC9", + "/rbelowcmb": "\u1DCA", + "/brevemacroncmb": "\u1DCB", + "/macronbrevecmb": "\u1DCC", + "/doubleabovecircumflexcmb": "\u1DCD", + "/aboveogonekcmb": "\u1DCE", + "/zigzagbelowcmb": "\u1DCF", + "/isbelowcmb": "\u1DD0", + "/urabovecmb": "\u1DD1", + "/usabovecmb": "\u1DD2", + "/aaboveflatcmb": "\u1DD3", + "/aecmb": "\u1DD4", + "/aocmb": "\u1DD5", + "/avcmb": "\u1DD6", + "/ccedillacmb": "\u1DD7", + "/insulardcmb": "\u1DD8", + "/ethcmb": "\u1DD9", + "/gcmb": "\u1DDA", + "/gsmallcmb": "\u1DDB", + "/kcmb": "\u1DDC", + "/lcmb": "\u1DDD", + "/lsmallcmb": "\u1DDE", + "/msmallcmb": "\u1DDF", + "/ncmb": "\u1DE0", + "/nsmallcmb": "\u1DE1", + "/rsmallcmb": "\u1DE2", + "/rrotundacmb": "\u1DE3", + "/scmb": "\u1DE4", + "/longscmb": "\u1DE5", + "/zcmb": "\u1DE6", + "/alphacmb": "\u1DE7", + "/bcmb": "\u1DE8", + "/betacmb": "\u1DE9", + "/schwacmb": "\u1DEA", + "/fcmb": "\u1DEB", + "/lwithdoublemiddletildecmb": "\u1DEC", + "/owithlightcentralizationstrokecmb": "\u1DED", + "/pcmb": "\u1DEE", + "/eshcmb": "\u1DEF", + "/uwithlightcentralizationstrokecmb": "\u1DF0", + "/wcmb": "\u1DF1", + "/adieresiscmb": "\u1DF2", + "/odieresiscmb": "\u1DF3", + "/udieresiscmb": "\u1DF4", + "/uptackabovecmb": "\u1DF5", + "/kavykaaboverightcmb": "\u1DF6", + "/kavykaaboveleftcmb": "\u1DF7", + "/dotaboveleftcmb": "\u1DF8", + "/wideinvertedbridgebelowcmb": "\u1DF9", + "/dotbelowleftcmb": "\u1DFA", + "/deletionmarkcmb": "\u1DFB", + "/doubleinvertedbelowbrevecmb": "\u1DFC", + "/almostequaltobelowcmb": "\u1DFD", + "/leftarrowheadabovecmb": "\u1DFE", + "/rightarrowheadanddownarrowheadbelowcmb": "\u1DFF", + "/Bdot": "\u1E02", + "/bdot": "\u1E03", + "/Ddot": "\u1E0A", + "/ddot": "\u1E0B", + "/Dcommaaccent": "\u1E10", + "/dcommaaccent": "\u1E11", + "/Fdot": "\u1E1E", + "/fdot": "\u1E1F", + "/Hdot": "\u1E22", + "/hdot": "\u1E23", + "/Lmacrondot": "\u1E38", + "/lmacrondot": "\u1E39", + "/Mdot": "\u1E40", + "/mdot": "\u1E41", + "/Ndot": "\u1E44", + "/ndot": "\u1E45", + "/Pdot": "\u1E56", + "/pdot": "\u1E57", + "/Rdot": "\u1E58", + "/rdot": "\u1E59", + "/Rmacrondot": "\u1E5C", + "/rmacrondot": "\u1E5D", + "/Sdot": "\u1E60", + "/sdot": "\u1E61", + "/Scarondot": "\u1E66", + "/scarondot": "\u1E67", + "/Sdotbelowdotabove": "\u1E68", + "/sdotbelowdotabove": "\u1E69", + "/Tdot": "\u1E6A", + "/tdot": "\u1E6B", + "/Wdot": "\u1E86", + "/wdot": "\u1E87", + "/Xdot": "\u1E8A", + "/xdot": "\u1E8B", + "/Ydot": "\u1E8E", + "/ydot": "\u1E8F", + "/longsdot": "\u1E9B", + "/longswithdiagonalstroke": "\u1E9C", + "/longswithhighstroke": "\u1E9D", + "/Germandbls": "\u1E9E", + "/lt:delta": "\u1E9F", + "/Ahoi": "\u1EA2", + "/ahoi": "\u1EA3", + "/Acircumflexhoi": "\u1EA8", + "/acircumflexhoi": "\u1EA9", + "/Abrevehoi": "\u1EB2", + "/abrevehoi": "\u1EB3", + "/Ehoi": "\u1EBA", + "/ehoi": "\u1EBB", + "/Ecircumflexhoi": "\u1EC2", + "/ecircumflexhoi": "\u1EC3", + "/Ihoi": "\u1EC8", + "/ihoi": "\u1EC9", + "/Ohoi": "\u1ECE", + "/ohoi": "\u1ECF", + "/Ocircumflexhoi": "\u1ED4", + "/ocircumflexhoi": "\u1ED5", + "/Ohornhoi": "\u1EDE", + "/ohornhoi": "\u1EDF", + "/Uhoi": "\u1EE6", + "/uhoi": "\u1EE7", + "/Uhornhoi": "\u1EEC", + "/uhornhoi": "\u1EED", + "/Yhoi": "\u1EF6", + "/yhoi": "\u1EF7", + "/LLwelsh": "\u1EFA", + "/llwelsh": "\u1EFB", + "/Vwelsh": "\u1EFC", + "/vwelsh": "\u1EFD", + "/Yloop": "\u1EFE", + "/yloop": "\u1EFF", + "/alphalenis": "\u1F00", + "/alphaasper": "\u1F01", + "/alphalenisgrave": "\u1F02", + "/alphaaspergrave": "\u1F03", + "/alphalenisacute": "\u1F04", + "/alphaasperacute": "\u1F05", + "/alphalenistilde": "\u1F06", + "/alphaaspertilde": "\u1F07", + "/Alphalenis": "\u1F08", + "/Alphaasper": "\u1F09", + "/Alphalenisgrave": "\u1F0A", + "/Alphaaspergrave": "\u1F0B", + "/Alphalenisacute": "\u1F0C", + "/Alphaasperacute": "\u1F0D", + "/Alphalenistilde": "\u1F0E", + "/Alphaaspertilde": "\u1F0F", + "/epsilonlenis": "\u1F10", + "/epsilonasper": "\u1F11", + "/epsilonlenisgrave": "\u1F12", + "/epsilonaspergrave": "\u1F13", + "/epsilonlenisacute": "\u1F14", + "/epsilonasperacute": "\u1F15", + "/Epsilonlenis": "\u1F18", + "/Epsilonasper": "\u1F19", + "/Epsilonlenisgrave": "\u1F1A", + "/Epsilonaspergrave": "\u1F1B", + "/Epsilonlenisacute": "\u1F1C", + "/Epsilonasperacute": "\u1F1D", + "/etalenis": "\u1F20", + "/etaasper": "\u1F21", + "/etalenisgrave": "\u1F22", + "/etaaspergrave": "\u1F23", + "/etalenisacute": "\u1F24", + "/etaasperacute": "\u1F25", + "/etalenistilde": "\u1F26", + "/etaaspertilde": "\u1F27", + "/Etalenis": "\u1F28", + "/Etaasper": "\u1F29", + "/Etalenisgrave": "\u1F2A", + "/Etaaspergrave": "\u1F2B", + "/Etalenisacute": "\u1F2C", + "/Etaasperacute": "\u1F2D", + "/Etalenistilde": "\u1F2E", + "/Etaaspertilde": "\u1F2F", + "/iotalenis": "\u1F30", + "/iotaasper": "\u1F31", + "/iotalenisgrave": "\u1F32", + "/iotaaspergrave": "\u1F33", + "/iotalenisacute": "\u1F34", + "/iotaasperacute": "\u1F35", + "/iotalenistilde": "\u1F36", + "/iotaaspertilde": "\u1F37", + "/Iotalenis": "\u1F38", + "/Iotaasper": "\u1F39", + "/Iotalenisgrave": "\u1F3A", + "/Iotaaspergrave": "\u1F3B", + "/Iotalenisacute": "\u1F3C", + "/Iotaasperacute": "\u1F3D", + "/Iotalenistilde": "\u1F3E", + "/Iotaaspertilde": "\u1F3F", + "/omicronlenis": "\u1F40", + "/omicronasper": "\u1F41", + "/omicronlenisgrave": "\u1F42", + "/omicronaspergrave": "\u1F43", + "/omicronlenisacute": "\u1F44", + "/omicronasperacute": "\u1F45", + "/Omicronlenis": "\u1F48", + "/Omicronasper": "\u1F49", + "/Omicronlenisgrave": "\u1F4A", + "/Omicronaspergrave": "\u1F4B", + "/Omicronlenisacute": "\u1F4C", + "/Omicronasperacute": "\u1F4D", + "/upsilonlenis": "\u1F50", + "/upsilonasper": "\u1F51", + "/upsilonlenisgrave": "\u1F52", + "/upsilonaspergrave": "\u1F53", + "/upsilonlenisacute": "\u1F54", + "/upsilonasperacute": "\u1F55", + "/upsilonlenistilde": "\u1F56", + "/upsilonaspertilde": "\u1F57", + "/Upsilonasper": "\u1F59", + "/Upsilonaspergrave": "\u1F5B", + "/Upsilonasperacute": "\u1F5D", + "/Upsilonaspertilde": "\u1F5F", + "/omegalenis": "\u1F60", + "/omegaasper": "\u1F61", + "/omegalenisgrave": "\u1F62", + "/omegaaspergrave": "\u1F63", + "/omegalenisacute": "\u1F64", + "/omegaasperacute": "\u1F65", + "/omegalenistilde": "\u1F66", + "/omegaaspertilde": "\u1F67", + "/Omegalenis": "\u1F68", + "/Omegaasper": "\u1F69", + "/Omegalenisgrave": "\u1F6A", + "/Omegaaspergrave": "\u1F6B", + "/Omegalenisacute": "\u1F6C", + "/Omegaasperacute": "\u1F6D", + "/Omegalenistilde": "\u1F6E", + "/Omegaaspertilde": "\u1F6F", + "/alphagrave": "\u1F70", + "/alphaacute": "\u1F71", + "/epsilongrave": "\u1F72", + "/epsilonacute": "\u1F73", + "/etagrave": "\u1F74", + "/etaacute": "\u1F75", + "/iotagrave": "\u1F76", + "/iotaacute": "\u1F77", + "/omicrongrave": "\u1F78", + "/omicronacute": "\u1F79", + "/upsilongrave": "\u1F7A", + "/upsilonacute": "\u1F7B", + "/omegagrave": "\u1F7C", + "/omegaacute": "\u1F7D", + "/alphalenisiotasub": "\u1F80", + "/alphaasperiotasub": "\u1F81", + "/alphalenisgraveiotasub": "\u1F82", + "/alphaaspergraveiotasub": "\u1F83", + "/alphalenisacuteiotasub": "\u1F84", + "/alphaasperacuteiotasub": "\u1F85", + "/alphalenistildeiotasub": "\u1F86", + "/alphaaspertildeiotasub": "\u1F87", + "/Alphalenisiotasub": "\u1F88", + "/Alphaasperiotasub": "\u1F89", + "/Alphalenisgraveiotasub": "\u1F8A", + "/Alphaaspergraveiotasub": "\u1F8B", + "/Alphalenisacuteiotasub": "\u1F8C", + "/Alphaasperacuteiotasub": "\u1F8D", + "/Alphalenistildeiotasub": "\u1F8E", + "/Alphaaspertildeiotasub": "\u1F8F", + "/etalenisiotasub": "\u1F90", + "/etaasperiotasub": "\u1F91", + "/etalenisgraveiotasub": "\u1F92", + "/etaaspergraveiotasub": "\u1F93", + "/etalenisacuteiotasub": "\u1F94", + "/etaasperacuteiotasub": "\u1F95", + "/etalenistildeiotasub": "\u1F96", + "/etaaspertildeiotasub": "\u1F97", + "/Etalenisiotasub": "\u1F98", + "/Etaasperiotasub": "\u1F99", + "/Etalenisgraveiotasub": "\u1F9A", + "/Etaaspergraveiotasub": "\u1F9B", + "/Etalenisacuteiotasub": "\u1F9C", + "/Etaasperacuteiotasub": "\u1F9D", + "/Etalenistildeiotasub": "\u1F9E", + "/Etaaspertildeiotasub": "\u1F9F", + "/omegalenisiotasub": "\u1FA0", + "/omegaasperiotasub": "\u1FA1", + "/omegalenisgraveiotasub": "\u1FA2", + "/omegaaspergraveiotasub": "\u1FA3", + "/omegalenisacuteiotasub": "\u1FA4", + "/omegaasperacuteiotasub": "\u1FA5", + "/omegalenistildeiotasub": "\u1FA6", + "/omegaaspertildeiotasub": "\u1FA7", + "/Omegalenisiotasub": "\u1FA8", + "/Omegaasperiotasub": "\u1FA9", + "/Omegalenisgraveiotasub": "\u1FAA", + "/Omegaaspergraveiotasub": "\u1FAB", + "/Omegalenisacuteiotasub": "\u1FAC", + "/Omegaasperacuteiotasub": "\u1FAD", + "/Omegalenistildeiotasub": "\u1FAE", + "/Omegaaspertildeiotasub": "\u1FAF", + "/alphabreve": "\u1FB0", + "/alphawithmacron": "\u1FB1", + "/alphagraveiotasub": "\u1FB2", + "/alphaiotasub": "\u1FB3", + "/alphaacuteiotasub": "\u1FB4", + "/alphatilde": "\u1FB6", + "/alphatildeiotasub": "\u1FB7", + "/Alphabreve": "\u1FB8", + "/Alphawithmacron": "\u1FB9", + "/Alphagrave": "\u1FBA", + "/Alphaacute": "\u1FBB", + "/Alphaiotasub": "\u1FBC", + "/KORONIS": "\u1FBD", + "/iotaadscript": "\u1FBE", + "/lenis": "\u1FBF", + "/gr:tilde": "\u1FC0", + "/dieresistilde": "\u1FC1", + "/etagraveiotasub": "\u1FC2", + "/etaiotasub": "\u1FC3", + "/etaacuteiotasub": "\u1FC4", + "/etatilde": "\u1FC6", + "/etatildeiotasub": "\u1FC7", + "/Epsilongrave": "\u1FC8", + "/Epsilonacute": "\u1FC9", + "/Etagrave": "\u1FCA", + "/Etaacute": "\u1FCB", + "/Etaiotasub": "\u1FCC", + "/lenisgrave": "\u1FCD", + "/lenisacute": "\u1FCE", + "/lenistilde": "\u1FCF", + "/iotabreve": "\u1FD0", + "/iotawithmacron": "\u1FD1", + "/iotadieresisgrave": "\u1FD2", + "/iotadieresisacute": "\u1FD3", + "/iotatilde": "\u1FD6", + "/iotadieresistilde": "\u1FD7", + "/Iotabreve": "\u1FD8", + "/Iotawithmacron": "\u1FD9", + "/Iotagrave": "\u1FDA", + "/Iotaacute": "\u1FDB", + "/aspergrave": "\u1FDD", + "/asperacute": "\u1FDE", + "/aspertilde": "\u1FDF", + "/upsilonbreve": "\u1FE0", + "/upsilonwithmacron": "\u1FE1", + "/upsilondieresisgrave": "\u1FE2", + "/upsilondieresisacute": "\u1FE3", + "/rholenis": "\u1FE4", + "/rhoasper": "\u1FE5", + "/upsilontilde": "\u1FE6", + "/upsilondieresistilde": "\u1FE7", + "/Upsilonbreve": "\u1FE8", + "/Upsilonwithmacron": "\u1FE9", + "/Upsilongrave": "\u1FEA", + "/Upsilonacute": "\u1FEB", + "/Rhoasper": "\u1FEC", + "/gr:grave": "\u1FEF", + "/omegagraveiotasub": "\u1FF2", + "/omegaiotasub": "\u1FF3", + "/omegaacuteiotasub": "\u1FF4", + "/omegatilde": "\u1FF6", + "/omegatildeiotasub": "\u1FF7", + "/Omicrongrave": "\u1FF8", + "/Omicronacute": "\u1FF9", + "/Omegagrave": "\u1FFA", + "/Omegaacute": "\u1FFB", + "/Omegaiotasub": "\u1FFC", + "/gr:acute": "\u1FFD", + "/asper": "\u1FFE", + "/enquad": "\u2000", + "/emquad": "\u2001", + "/emspace": "\u2003", + "/threeperemspace": "\u2004", + "/fourperemspace": "\u2005", + "/sixperemspace": "\u2006", + "/figurespace": "\u2007", + "/punctuationspace": "\u2008", + "/thinspace": "\u2009", + "/hairspace": "\u200A", + "/lefttorightmark": "\u200E", + "/righttoleftmark": "\u200F", + "/gnrl:hyphen": "\u2010", + "/nonbreakinghyphen": "\u2011", + "/verticalbardbl": "\u2016", + "/quotedblreversed": "\u201F", + "/triangularbullet": "\u2023", + "/hyphenationpoint": "\u2027", + "/lineseparator": "\u2028", + "/paragraphseparator": "\u2029", + "/lefttorightembed": "\u202A", + "/righttoleftembed": "\u202B", + "/popdirectionalformatting": "\u202C", + "/lefttorightoverride": "\u202D", + "/righttoleftoverride": "\u202E", + "/narrownobreakspace": "\u202F", + "/pertenthousandsign": "\u2031", + "/millisecond": "\u2034", + "/minutereversed": "\u2035", + "/secondreversed": "\u2036", + "/millisecondreversed": "\u2037", + "/caret": "\u2038", + "/interrobang": "\u203D", + "/undertie": "\u203F", + "/charactertie": "\u2040", + "/caretinsertionpoint": "\u2041", + "/hyphenbullet": "\u2043", + "/bracketleftsquarequill": "\u2045", + "/bracketrightsquarequill": "\u2046", + "/questiondbl": "\u2047", + "/questionexclamationmark": "\u2048", + "/exclamationquestion": "\u2049", + "/tironiansignet": "\u204A", + "/pilcrowsignreversed": "\u204B", + "/blackwardsbulletleft": "\u204C", + "/blackwardsbulletright": "\u204D", + "/lowasterisk": "\u204E", + "/semicolonreversed": "\u204F", + "/closeup": "\u2050", + "/twoasterisksalignedvertically": "\u2051", + "/commercialminussign": "\u2052", + "/swungdash": "\u2053", + "/invertedundertie": "\u2054", + "/flowerpunctuationmark": "\u2055", + "/threedotpunctuation": "\u2056", + "/quadrupleminute": "\u2057", + "/fourdotpunctuation": "\u2058", + "/fivedotpunctuation": "\u2059", + "/twodotpunctuation": "\u205A", + "/fourdotmark": "\u205B", + "/dottedcross": "\u205C", + "/tricolon": "\u205D", + "/verticalfourdots": "\u205E", + "/mediummathematicalspace": "\u205F", + "/wordjoiner": "\u2060", + "/functionapplication": "\u2061", + "/invisibletimes": "\u2062", + "/invisibleseparator": "\u2063", + "/invisibleplus": "\u2064", + "/lefttorightisolate": "\u2066", + "/righttoleftisolate": "\u2067", + "/firststrongisolate": "\u2068", + "/popdirectionalisolate": "\u2069", + "/inhibitsymmetricswapping": "\u206A", + "/activatesymmetricswapping": "\u206B", + "/inhibitarabicformshaping": "\u206C", + "/activatearabicformshaping": "\u206D", + "/nationaldigitshapes": "\u206E", + "/nominaldigitshapes": "\u206F", + "/zero.superior": "\u2070", + "/i.superior": "\u2071", + "/four.superior": "\u2074", + "/five.superior": "\u2075", + "/six.superior": "\u2076", + "/seven.superior": "\u2077", + "/eight.superior": "\u2078", + "/nine.superior": "\u2079", + "/plus.superior": "\u207A", + "/minus.superior": "\u207B", + "/equal.superior": "\u207C", + "/parenleft.superior": "\u207D", + "/parenright.superior": "\u207E", + "/n.superior": "\u207F", + "/zero.inferior": "\u2080", + "/one.inferior": "\u2081", + "/two.inferior": "\u2082", + "/three.inferior": "\u2083", + "/four.inferior": "\u2084", + "/five.inferior": "\u2085", + "/six.inferior": "\u2086", + "/seven.inferior": "\u2087", + "/eight.inferior": "\u2088", + "/nine.inferior": "\u2089", + "/plus.inferior": "\u208A", + "/minus.inferior": "\u208B", + "/equal.inferior": "\u208C", + "/parenleft.inferior": "\u208D", + "/parenright.inferior": "\u208E", + "/a.inferior": "\u2090", + "/e.inferior": "\u2091", + "/o.inferior": "\u2092", + "/x.inferior": "\u2093", + "/schwa.inferior": "\u2094", + "/h.inferior": "\u2095", + "/k.inferior": "\u2096", + "/l.inferior": "\u2097", + "/m.inferior": "\u2098", + "/n.inferior": "\u2099", + "/p.inferior": "\u209A", + "/s.inferior": "\u209B", + "/t.inferior": "\u209C", + "/euroarchaic": "\u20A0", + "/mill": "\u20A5", + "/naira": "\u20A6", + "/rupee": "\u20A8", + "/newsheqel": "\u20AA", + "/kip": "\u20AD", + "/tugrik": "\u20AE", + "/drachma": "\u20AF", + "/germanpenny": "\u20B0", + "/peso": "\u20B1", + "/guarani": "\u20B2", + "/austral": "\u20B3", + "/hryvnia": "\u20B4", + "/cedi": "\u20B5", + "/livretournois": "\u20B6", + "/spesmilo": "\u20B7", + "/tenge": "\u20B8", + "/indianrupee": "\u20B9", + "/turkishlira": "\u20BA", + "/nordicmark": "\u20BB", + "/manat": "\u20BC", + "/ruble": "\u20BD", + "/lari": "\u20BE", + "/bitcoin": "\u20BF", + "/som": "\u20C0", + "/leftHarpoonAbovecmb": "\u20D0", + "/rightHarpoonAbovecmb": "\u20D1", + "/longVerticalLineOverlaycmb": "\u20D2", + "/shortVerticalLineOverlaycmb": "\u20D3", + "/anticlockwiseArrowAbovecmb": "\u20D4", + "/clockwiseArrowAbovecmb": "\u20D5", + "/leftArrowAbovecmb": "\u20D6", + "/rightArrowAbovecmb": "\u20D7", + "/ringOverlaycmb": "\u20D8", + "/clockwiseRingOverlaycmb": "\u20D9", + "/anticlockwiseRingOverlaycmb": "\u20DA", + "/threeDotsAbovecmb": "\u20DB", + "/fourDotsAbovecmb": "\u20DC", + "/enclosingCirclecmb": "\u20DD", + "/enclosingSquarecmb": "\u20DE", + "/enclosingDiamondcmb": "\u20DF", + "/enclosingCircleBackslashcmb": "\u20E0", + "/leftRightArrowAbovecmb": "\u20E1", + "/enclosingScreencmb": "\u20E2", + "/enclosingKeycapcmb": "\u20E3", + "/enclosingUpwardPointingTrianglecmb": "\u20E4", + "/reverseSolidusOverlaycmb": "\u20E5", + "/doubleVerticalOverlaystrokecmb": "\u20E6", + "/annuitySymbolcmb": "\u20E7", + "/tripleUnderdotcmb": "\u20E8", + "/wideBridgeAbovecmb": "\u20E9", + "/leftwardsArrowOverlaycmb": "\u20EA", + "/longDoubleSolidusOverlaycmb": "\u20EB", + "/rightwardsHarpoonBDownwardsbarcmb": "\u20EC", + "/leftwardsHarpoonBDownwardsbarcmb": "\u20ED", + "/leftArrowBelowcmb": "\u20EE", + "/rightArrowBelowcmb": "\u20EF", + "/asteriskAbovecmb": "\u20F0", + "/accountof": "\u2100", + "/addressedsubject": "\u2101", + "/Cdblstruck": "\u2102", + "/degreecelsius": "\u2103", + "/centreline": "\u2104", + "/cadauna": "\u2106", + "/euler": "\u2107", + "/scruple": "\u2108", + "/degreefahrenheit": "\u2109", + "/lttr:gscript": "\u210A", + "/Hscript": "\u210B", + "/Hfraktur": "\u210C", + "/Hdblstruck": "\u210D", + "/planck": "\u210E", + "/plancktwopi": "\u210F", + "/Iscript": "\u2110", + "/Lscript": "\u2112", + "/litre": "\u2113", + "/lbbar": "\u2114", + "/Ndblstruck": "\u2115", + "/soundcopyright": "\u2117", + "/Pdblstruck": "\u2119", + "/Qdblstruck": "\u211A", + "/Rscript": "\u211B", + "/Rdblstruck": "\u211D", + "/response": "\u211F", + "/servicemark": "\u2120", + "/versicle": "\u2123", + "/Zdblstruck": "\u2124", + "/ounce": "\u2125", + "/ohm": "\u2126", + "/ohminverted": "\u2127", + "/Zfraktur": "\u2128", + "/iotaturned": "\u2129", + "/kelvin": "\u212A", + "/Bscript": "\u212C", + "/Cfraktur": "\u212D", + "/escript": "\u212F", + "/Escript": "\u2130", + "/Fscript": "\u2131", + "/Fturned": "\u2132", + "/Mscript": "\u2133", + "/oscript": "\u2134", + "/information": "\u2139", + "/Qrotated": "\u213A", + "/facsimile": "\u213B", + "/pidblstruck": "\u213C", + "/gammadblstruck": "\u213D", + "/Gammadblstruck": "\u213E", + "/Pidblstruck": "\u213F", + "/summationdblstruck": "\u2140", + "/Gturnedsans": "\u2141", + "/Lturnedsans": "\u2142", + "/Lreversedsans": "\u2143", + "/Yturnedsans": "\u2144", + "/Ddblstruckitalic": "\u2145", + "/ddblstruckitalic": "\u2146", + "/edblstruckitalic": "\u2147", + "/idblstruckitalic": "\u2148", + "/jdblstruckitalic": "\u2149", + "/propertyline": "\u214A", + "/ampersandturned": "\u214B", + "/per": "\u214C", + "/aktieselskab": "\u214D", + "/fturned": "\u214E", + "/forsamaritan": "\u214F", + "/oneseventh": "\u2150", + "/oneninth": "\u2151", + "/onetenth": "\u2152", + "/onefifth": "\u2155", + "/twofifths": "\u2156", + "/threefifths": "\u2157", + "/fourfifths": "\u2158", + "/onesixth": "\u2159", + "/fivesixths": "\u215A", + "/onefraction": "\u215F", + "/one.roman": "\u2160", + "/two.roman": "\u2161", + "/three.roman": "\u2162", + "/four.roman": "\u2163", + "/five.roman": "\u2164", + "/six.roman": "\u2165", + "/seven.roman": "\u2166", + "/eight.roman": "\u2167", + "/nine.roman": "\u2168", + "/ten.roman": "\u2169", + "/eleven.roman": "\u216A", + "/twelve.roman": "\u216B", + "/fifty.roman": "\u216C", + "/onehundred.roman": "\u216D", + "/fivehundred.roman": "\u216E", + "/onethousand.roman": "\u216F", + "/one.romansmall": "\u2170", + "/two.romansmall": "\u2171", + "/three.romansmall": "\u2172", + "/four.romansmall": "\u2173", + "/five.romansmall": "\u2174", + "/six.romansmall": "\u2175", + "/seven.romansmall": "\u2176", + "/eight.romansmall": "\u2177", + "/nine.romansmall": "\u2178", + "/ten.romansmall": "\u2179", + "/eleven.romansmall": "\u217A", + "/twelve.romansmall": "\u217B", + "/fifty.romansmall": "\u217C", + "/onehundred.romansmall": "\u217D", + "/fivehundred.romansmall": "\u217E", + "/onethousand.romansmall": "\u217F", + "/onethousandcd.roman": "\u2180", + "/fivethousand.roman": "\u2181", + "/tenthousand.roman": "\u2182", + "/reversedonehundred.roman": "\u2183", + "/creversed": "\u2184", + "/sixlateform.roman": "\u2185", + "/fiftyearlyform.roman": "\u2186", + "/fiftythousand.roman": "\u2187", + "/onehundredthousand.roman": "\u2188", + "/zerothirds": "\u2189", + "/turneddigittwo": "\u218A", + "/turneddigitthree": "\u218B", + "/arrowleftright": "\u2194", + "/arrowupdown": "\u2195", + "/arrowNW": "\u2196", + "/arrowNE": "\u2197", + "/arrowSE": "\u2198", + "/arrowSW": "\u2199", + "/arrowleftstroke": "\u219A", + "/arrowrightstroke": "\u219B", + "/arrowleftwave": "\u219C", + "/arrowrightwave": "\u219D", + "/arrowlefttwoheaded": "\u219E", + "/arrowuptwoheaded": "\u219F", + "/arrowrighttwoheaded": "\u21A0", + "/arrowdowntwoheaded": "\u21A1", + "/arrowlefttail": "\u21A2", + "/arrowrighttail": "\u21A3", + "/arrowleftfrombar": "\u21A4", + "/arrowupfrombar": "\u21A5", + "/arrowrightfrombar": "\u21A6", + "/arrowdownfrombar": "\u21A7", + "/arrowupdownwithbase": "\u21A8", + "/arrowlefthook": "\u21A9", + "/arrowrighthook": "\u21AA", + "/arrowleftloop": "\u21AB", + "/arrowrightloop": "\u21AC", + "/arrowleftrightwave": "\u21AD", + "/arrowleftrightstroke": "\u21AE", + "/arrowdownzigzag": "\u21AF", + "/arrowleftuptip": "\u21B0", + "/arrowrightuptip": "\u21B1", + "/arrowleftdowntip": "\u21B2", + "/arrowrightdowntip": "\u21B3", + "/arrowrightdowncorner": "\u21B4", + "/arrowleftdowncorner": "\u21B5", + "/arrowanticlockwisesemicircle": "\u21B6", + "/arrowclockwisesemicircle": "\u21B7", + "/arrowlongNWtobar": "\u21B8", + "/arrowleftoverrighttobar": "\u21B9", + "/arrowanticlockwiseopencircle": "\u21BA", + "/arrowclockwiseopencircle": "\u21BB", + "/harpoonleftbarbdown": "\u21BD", + "/harpoonupbarbright": "\u21BE", + "/harpoonupbarbleft": "\u21BF", + "/harpoonrightbarbdown": "\u21C1", + "/harpoondownbarbright": "\u21C2", + "/harpoondownbarbleft": "\u21C3", + "/rightarrowoverleftarrow": "\u21C4", + "/uparrowleftofdownarrow": "\u21C5", + "/leftarrowoverrightarrow": "\u21C6", + "/arrowspairedleft": "\u21C7", + "/arrowspairedup": "\u21C8", + "/arrowspairedright": "\u21C9", + "/arrowspaireddown": "\u21CA", + "/leftharpoonoverrightharpoon": "\u21CB", + "/rightharpoonoverleftharpoon": "\u21CC", + "/dblarrowleftstroke": "\u21CD", + "/dblarrowleftrightstroke": "\u21CE", + "/dblarrowrightstroke": "\u21CF", + "/dblarrowup": "\u21D1", + "/dblarrowdown": "\u21D3", + "/dblarrowleftright": "\u21D4", + "/dblarrowupdown": "\u21D5", + "/dblarrowNW": "\u21D6", + "/dblarrowNE": "\u21D7", + "/dblarrowSE": "\u21D8", + "/dblarrowSW": "\u21D9", + "/triplearrowleft": "\u21DA", + "/triplearrowright": "\u21DB", + "/arrowleftsquiggle": "\u21DC", + "/arrowrightsquiggle": "\u21DD", + "/dblstrokearrowup": "\u21DE", + "/dblstrokearrowdown": "\u21DF", + "/arrowleftdashed": "\u21E0", + "/arrowupdashed": "\u21E1", + "/arrowrightdashed": "\u21E2", + "/arrowdowndashed": "\u21E3", + "/arrowlefttobar": "\u21E4", + "/arrowrighttobar": "\u21E5", + "/whitearrowleft": "\u21E6", + "/whitearrowup": "\u21E7", + "/whitearrowright": "\u21E8", + "/whitearrowdown": "\u21E9", + "/whitearrowupfrombar": "\u21EA", + "/whitearrowonpedestalup": "\u21EB", + "/horizontalbarwhitearrowonpedestalup": "\u21EC", + "/verticalbarwhitearrowonpedestalup": "\u21ED", + "/whitedblarrowup": "\u21EE", + "/whitedblarrowonpedestalup": "\u21EF", + "/whitearrowfromwallright": "\u21F0", + "/tocornerarrowNW": "\u21F1", + "/tocornerarrowSE": "\u21F2", + "/whitearrowupdown": "\u21F3", + "/arrowrightsmallcircle": "\u21F4", + "/downwarrowleftofuparrow": "\u21F5", + "/threerightarrows": "\u21F6", + "/verticalstrokearrowleft": "\u21F7", + "/verticalstrokearrowright": "\u21F8", + "/verticalstrokearrowleftright": "\u21F9", + "/verticalsdbltrokearrowleft": "\u21FA", + "/verticalsdbltrokearrowright": "\u21FB", + "/verticalsdbltrokearrowleftright": "\u21FC", + "/openheadarrowleft": "\u21FD", + "/openheadarrowright": "\u21FE", + "/openheadarrowleftright": "\u21FF", + "/complement": "\u2201", + "/notexistential": "\u2204", + "/elementsmall": "\u220A", + "/containsasmembersmall": "\u220D", + "/endpro": "\u220E", + "/coproductarray": "\u2210", + "/dotplus": "\u2214", + "/setminus": "\u2216", + "/ringoperator": "\u2218", + "/math:cuberoot": "\u221B", + "/math:fourthroot": "\u221C", + "/measuredangle": "\u2221", + "/sphericalangle": "\u2222", + "/doesnotdivide": "\u2224", + "/integraldbl": "\u222C", + "/integraltpl": "\u222D", + "/integralcontour": "\u222E", + "/integralsurface": "\u222F", + "/integralvolume": "\u2230", + "/integralclockwise": "\u2231", + "/integralcontourclockwise": "\u2232", + "/integralcontouranticlockwise": "\u2233", + "/dotminus": "\u2238", + "/excess": "\u2239", + "/geometricproportion": "\u223A", + "/homotic": "\u223B", + "/tildereversed": "\u223D", + "/lazysinverted": "\u223E", + "/sinewave": "\u223F", + "/wreathproduct": "\u2240", + "/nottilde": "\u2241", + "/minustilde": "\u2242", + "/asympticallyequal": "\u2243", + "/notasympticallyequal": "\u2244", + "/approximatelybutnotactuallyequal": "\u2246", + "/neirapproximatelynoractuallyequal": "\u2247", + "/notalmostequal": "\u2249", + "/almostequalorequal": "\u224A", + "/tildetpl": "\u224B", + "/equivalent": "\u224D", + "/geometricallyequivalent": "\u224E", + "/differencebetween": "\u224F", + "/approacheslimit": "\u2250", + "/approximatelyequalorimage": "\u2252", + "/colonequals": "\u2254", + "/equalscolon": "\u2255", + "/ringinequal": "\u2256", + "/ringequal": "\u2257", + "/corresponds": "\u2258", + "/estimates": "\u2259", + "/equiangular": "\u225A", + "/starequals": "\u225B", + "/deltaequal": "\u225C", + "/equalbydefinition": "\u225D", + "/measuredby": "\u225E", + "/questionedequal": "\u225F", + "/strictlyequivalent": "\u2263", + "/lessbutnotequal": "\u2268", + "/greaterbutnotequal": "\u2269", + "/between": "\u226C", + "/notequivalent": "\u226D", + "/neirlessnorequal": "\u2270", + "/neirgreaternorequal": "\u2271", + "/neirlessnorequivalent": "\u2274", + "/neirgreaternorequivalent": "\u2275", + "/neirlessnorgreater": "\u2278", + "/neirgreaternorless": "\u2279", + "/precedesorequal": "\u227C", + "/succeedsorequal": "\u227D", + "/precedesorequivalent": "\u227E", + "/succeedsorequivalent": "\u227F", + "/doesnotprecede": "\u2280", + "/doesnotsucceed": "\u2281", + "/notasersetup": "\u2285", + "/neirasubsetnorequal": "\u2288", + "/neirasersetnorequalup": "\u2289", + "/sersetnotequalup": "\u228B", + "/multiset": "\u228C", + "/multisetmultiplication": "\u228D", + "/multisetunion": "\u228E", + "/squareimage": "\u228F", + "/squareoriginal": "\u2290", + "/squareimageorequal": "\u2291", + "/squareoriginalorequal": "\u2292", + "/squarecap": "\u2293", + "/squarecup": "\u2294", + "/circledminus": "\u2296", + "/circleddivisionslash": "\u2298", + "/circleddotoperator": "\u2299", + "/circledringoperator": "\u229A", + "/circledasteriskoperator": "\u229B", + "/circledequals": "\u229C", + "/circleddash": "\u229D", + "/squaredplus": "\u229E", + "/squaredminus": "\u229F", + "/squaredtimes": "\u22A0", + "/squareddotoperator": "\u22A1", + "/tackright": "\u22A2", + "/tackup": "\u22A5", + "/assertion": "\u22A6", + "/models": "\u22A7", + "/true": "\u22A8", + "/forces": "\u22A9", + "/turnstiletplverticalbarright": "\u22AA", + "/turnstiledblverticalbarright": "\u22AB", + "/doesnotprove": "\u22AC", + "/nottrue": "\u22AD", + "/doesnotforce": "\u22AE", + "/negatedturnstiledblverticalbarright": "\u22AF", + "/precedesunderrelation": "\u22B0", + "/succeedsunderrelation": "\u22B1", + "/normalsubgroup": "\u22B2", + "/containsasnormalsubgroup": "\u22B3", + "/normalsubgroorequalup": "\u22B4", + "/containsasnormalsubgroorequalup": "\u22B5", + "/original": "\u22B6", + "/image": "\u22B7", + "/multimap": "\u22B8", + "/hermitianconjugatematrix": "\u22B9", + "/intercalate": "\u22BA", + "/xor": "\u22BB", + "/nand": "\u22BC", + "/nor": "\u22BD", + "/anglearcright": "\u22BE", + "/triangleright": "\u22BF", + "/logicalandarray": "\u22C0", + "/logicalorarray": "\u22C1", + "/intersectionarray": "\u22C2", + "/unionarray": "\u22C3", + "/diamondoperator": "\u22C4", + "/staroperator": "\u22C6", + "/divisiontimes": "\u22C7", + "/math:bowtie": "\u22C8", + "/normalfacrsemidirectproductleft": "\u22C9", + "/normalfacrsemidirectproductright": "\u22CA", + "/semidirectproductleft": "\u22CB", + "/semidirectproductright": "\u22CC", + "/tildeequalsreversed": "\u22CD", + "/curlylogicalor": "\u22CE", + "/curlylogicaland": "\u22CF", + "/subsetdbl": "\u22D0", + "/sersetdblup": "\u22D1", + "/intersectiondbl": "\u22D2", + "/uniondbl": "\u22D3", + "/pitchfork": "\u22D4", + "/equalandparallel": "\u22D5", + "/lessdot": "\u22D6", + "/greaterdot": "\u22D7", + "/verymuchless": "\u22D8", + "/verymuchgreater": "\u22D9", + "/equalorless": "\u22DC", + "/equalorgreater": "\u22DD", + "/equalorprecedes": "\u22DE", + "/equalorsucceeds": "\u22DF", + "/doesnotprecedeorequal": "\u22E0", + "/doesnotsucceedorequal": "\u22E1", + "/notsquareimageorequal": "\u22E2", + "/notsquareoriginalorequal": "\u22E3", + "/squareimageornotequal": "\u22E4", + "/squareoriginalornotequal": "\u22E5", + "/lessbutnotequivalent": "\u22E6", + "/greaterbutnotequivalent": "\u22E7", + "/precedesbutnotequivalent": "\u22E8", + "/succeedsbutnotequivalent": "\u22E9", + "/notnormalsubgroup": "\u22EA", + "/doesnotcontainasnormalsubgroup": "\u22EB", + "/notnormalsubgroorequalup": "\u22EC", + "/doesnotcontainasnormalsubgroorequalup": "\u22ED", + "/ellipsismidhorizontal": "\u22EF", + "/ellipsisdiagonalupright": "\u22F0", + "/ellipsisdiagonaldownright": "\u22F1", + "/elementlonghorizontalstroke": "\u22F2", + "/elementverticalbarhorizontalstroke": "\u22F3", + "/elementsmallverticalbarhorizontalstroke": "\u22F4", + "/elementdotabove": "\u22F5", + "/elementoverbar": "\u22F6", + "/elementoverbarsmall": "\u22F7", + "/elementunderbar": "\u22F8", + "/elementtwoshorizontalstroke": "\u22F9", + "/containslonghorizontalstroke": "\u22FA", + "/containsverticalbarhorizontalstroke": "\u22FB", + "/containssmallverticalbarhorizontalstroke": "\u22FC", + "/containsoverbar": "\u22FD", + "/containsoverbarsmall": "\u22FE", + "/znotationbagmembership": "\u22FF", + "/diametersign": "\u2300", + "/electricarrow": "\u2301", + "/arrowheadup": "\u2303", + "/arrowheaddown": "\u2304", + "/perspective": "\u2306", + "/wavyline": "\u2307", + "/ceilingleft": "\u2308", + "/ceilingright": "\u2309", + "/floorleft": "\u230A", + "/floorright": "\u230B", + "/cropbottomright": "\u230C", + "/cropbottomleft": "\u230D", + "/croptopright": "\u230E", + "/croptopleft": "\u230F", + "/lozengesquare": "\u2311", + "/segment": "\u2313", + "/sector": "\u2314", + "/telephonerecorder": "\u2315", + "/positionindicator": "\u2316", + "/viewdatasquare": "\u2317", + "/placeofinterestsign": "\u2318", + "/notsignturned": "\u2319", + "/watch": "\u231A", + "/hourglass": "\u231B", + "/cornertopleft": "\u231C", + "/cornertopright": "\u231D", + "/cornerbottomleft": "\u231E", + "/cornerbottomright": "\u231F", + "/frown": "\u2322", + "/smile": "\u2323", + "/arrowheadtwobarsuphorizontal": "\u2324", + "/eraseright": "\u2326", + "/board": "\u2328", + "/eraseleft": "\u232B", + "/benzenering": "\u232C", + "/cylindricity": "\u232D", + "/allaroundprofile": "\u232E", + "/symmetry": "\u232F", + "/totalrunout": "\u2330", + "/dimensionorigin": "\u2331", + "/conicaltaper": "\u2332", + "/slope": "\u2333", + "/counterbore": "\u2334", + "/countersink": "\u2335", + "/beamfunc": "\u2336", + "/squishquadfunc": "\u2337", + "/quadequalfunc": "\u2338", + "/quaddividefunc": "\u2339", + "/quaddiamondfunc": "\u233A", + "/quadjotfunc": "\u233B", + "/quadcirclefunc": "\u233C", + "/circlestilefunc": "\u233D", + "/circlejotfunc": "\u233E", + "/slashbarfunc": "\u233F", + "/backslashbarfunc": "\u2340", + "/quadslashfunc": "\u2341", + "/quadbackslashfunc": "\u2342", + "/quadlessfunc": "\u2343", + "/quadgreaterfunc": "\u2344", + "/vaneleftfunc": "\u2345", + "/vanerightfunc": "\u2346", + "/quadarrowleftfunc": "\u2347", + "/quadarrowrightfunc": "\u2348", + "/circlebackslashfunc": "\u2349", + "/tackunderlinedownfunc": "\u234A", + "/deltastilefunc": "\u234B", + "/quadcaretdownfunc": "\u234C", + "/quaddeltafunc": "\u234D", + "/tackjotdownfunc": "\u234E", + "/vaneupfunc": "\u234F", + "/quadarrowupfunc": "\u2350", + "/tackoverbarupfunc": "\u2351", + "/delstilefunc": "\u2352", + "/quadcaretupfunc": "\u2353", + "/quaddelfunc": "\u2354", + "/tackjotupfunc": "\u2355", + "/vanedownfunc": "\u2356", + "/quadarrowdownfunc": "\u2357", + "/quoteunderlinefunc": "\u2358", + "/deltaunderlinefunc": "\u2359", + "/diamondunderlinefunc": "\u235A", + "/jotunderlinefunc": "\u235B", + "/circleunderlinefunc": "\u235C", + "/shoejotupfunc": "\u235D", + "/quotequadfunc": "\u235E", + "/circlestarfunc": "\u235F", + "/quadcolonfunc": "\u2360", + "/tackdiaeresisupfunc": "\u2361", + "/deldiaeresisfunc": "\u2362", + "/stardiaeresisfunc": "\u2363", + "/jotdiaeresisfunc": "\u2364", + "/circlediaeresisfunc": "\u2365", + "/shoestiledownfunc": "\u2366", + "/shoestileleftfunc": "\u2367", + "/tildediaeresisfunc": "\u2368", + "/diaeresisgreaterfunc": "\u2369", + "/commabarfunc": "\u236A", + "/deltildefunc": "\u236B", + "/zildefunc": "\u236C", + "/stiletildefunc": "\u236D", + "/semicolonunderlinefunc": "\u236E", + "/quadnotequalfunc": "\u236F", + "/quadquestionfunc": "\u2370", + "/carettildedownfunc": "\u2371", + "/carettildeupfunc": "\u2372", + "/iotafunc": "\u2373", + "/rhofunc": "\u2374", + "/omegafunc": "\u2375", + "/alphaunderlinefunc": "\u2376", + "/epsilonunderlinefunc": "\u2377", + "/iotaunderlinefunc": "\u2378", + "/omegaunderlinefunc": "\u2379", + "/alphafunc": "\u237A", + "/notcheckmark": "\u237B", + "/anglezigzagarrowdownright": "\u237C", + "/shoulderedopenbox": "\u237D", + "/bell": "\u237E", + "/linemiddledotvertical": "\u237F", + "/insertion": "\u2380", + "/continuousunderline": "\u2381", + "/discontinuousunderline": "\u2382", + "/emphasis": "\u2383", + "/composition": "\u2384", + "/centrelineverticalsquarewhite": "\u2385", + "/enter": "\u2386", + "/alternative": "\u2387", + "/helm": "\u2388", + "/circledbarnotchhorizontal": "\u2389", + "/circledtriangledown": "\u238A", + "/brokencirclenorthwestarrow": "\u238B", + "/undo": "\u238C", + "/monostable": "\u238D", + "/hysteresis": "\u238E", + "/htypeopencircuit": "\u238F", + "/ltypeopencircuit": "\u2390", + "/passivedown": "\u2391", + "/outputpassiveup": "\u2392", + "/directcurrentformtwo": "\u2393", + "/softwarefunction": "\u2394", + "/quadfunc": "\u2395", + "/misc:decimalseparator": "\u2396", + "/previouspage": "\u2397", + "/nextpage": "\u2398", + "/printscreen": "\u2399", + "/clearscreen": "\u239A", + "/parenhookupleft": "\u239B", + "/parenextensionleft": "\u239C", + "/parenlowerhookleft": "\u239D", + "/parenhookupright": "\u239E", + "/parenextensionright": "\u239F", + "/parenlowerhookright": "\u23A0", + "/bracketcornerupleftsquare": "\u23A1", + "/bracketextensionleftsquare": "\u23A2", + "/bracketlowercornerleftsquare": "\u23A3", + "/bracketcorneruprightsquare": "\u23A4", + "/bracketextensionrightsquare": "\u23A5", + "/bracketlowercornerrightsquare": "\u23A6", + "/brackethookupleftcurly": "\u23A7", + "/bracketmiddlepieceleftcurly": "\u23A8", + "/bracketlowerhookleftcurly": "\u23A9", + "/bracketextensioncurly": "\u23AA", + "/brackethookuprightcurly": "\u23AB", + "/bracketmiddlepiecerightcurly": "\u23AC", + "/bracketlowerhookrightcurly": "\u23AD", + "/integralextension": "\u23AE", + "/lineextensionhorizontal": "\u23AF", + "/bracketsectionupleftlowerrightcurly": "\u23B0", + "/bracketsectionuprightlowerleftcurly": "\u23B1", + "/summationtop": "\u23B2", + "/summationbottom": "\u23B3", + "/brackettopsquare": "\u23B4", + "/bracketbottomsquare": "\u23B5", + "/bracketoverbrackettopbottomsquare": "\u23B6", + "/radicalbottom": "\u23B7", + "/boxlineverticalleft": "\u23B8", + "/boxlineverticalright": "\u23B9", + "/scanonehorizontal": "\u23BA", + "/scanthreehorizontal": "\u23BB", + "/scansevenhorizontal": "\u23BC", + "/scanninehorizontal": "\u23BD", + "/dentistrytopverticalright": "\u23BE", + "/dentistrybottomverticalright": "\u23BF", + "/dentistrycirclevertical": "\u23C0", + "/dentistrycircledownhorizontal": "\u23C1", + "/dentistrycircleuphorizontal": "\u23C2", + "/dentistrytrianglevertical": "\u23C3", + "/dentistrytriangledownhorizontal": "\u23C4", + "/dentistrytriangleuphorizontal": "\u23C5", + "/dentistrywavevertical": "\u23C6", + "/dentistrywavedownhorizontal": "\u23C7", + "/dentistrywaveuphorizontal": "\u23C8", + "/dentistrydownhorizontal": "\u23C9", + "/dentistryuphorizontal": "\u23CA", + "/dentistrytopverticalleft": "\u23CB", + "/dentistrybottomverticalleft": "\u23CC", + "/footsquare": "\u23CD", + "/return": "\u23CE", + "/eject": "\u23CF", + "/lineextensionvertical": "\u23D0", + "/brevemetrical": "\u23D1", + "/longovershortmetrical": "\u23D2", + "/shortoverlongmetrical": "\u23D3", + "/longovertwoshortsmetrical": "\u23D4", + "/twoshortsoverlongmetrical": "\u23D5", + "/twoshortsjoinedmetrical": "\u23D6", + "/trisememetrical": "\u23D7", + "/tetrasememetrical": "\u23D8", + "/pentasememetrical": "\u23D9", + "/earthground": "\u23DA", + "/fuse": "\u23DB", + "/parentop": "\u23DC", + "/parenbottom": "\u23DD", + "/brackettopcurly": "\u23DE", + "/bracketbottomcurly": "\u23DF", + "/bracketshelltop": "\u23E0", + "/bracketshellbottom": "\u23E1", + "/trapeziumwhite": "\u23E2", + "/benzeneringcircle": "\u23E3", + "/straightness": "\u23E4", + "/flatness": "\u23E5", + "/accurrent": "\u23E6", + "/electricalintersection": "\u23E7", + "/decimalexponent": "\u23E8", + "/blackpointingdoubletriangleright": "\u23E9", + "/blackpointingdoubletriangleleft": "\u23EA", + "/blackpointingdoubletriangleup": "\u23EB", + "/blackpointingdoubletriangledown": "\u23EC", + "/blackpointingdoubletrianglebarverticalright": "\u23ED", + "/blackpointingdoubletrianglebarverticalleft": "\u23EE", + "/blackpointingtriangledoublebarverticalright": "\u23EF", + "/alarmclock": "\u23F0", + "/swatchtop": "\u23F1", + "/timerclock": "\u23F2", + "/hourglassflowings": "\u23F3", + "/blackmediumpointingtriangleleft": "\u23F4", + "/blackmediumpointingtriangleright": "\u23F5", + "/blackmediumpointingtriangleup": "\u23F6", + "/blackmediumpointingtriangledown": "\u23F7", + "/doublebarvertical": "\u23F8", + "/blackforstopsquare": "\u23F9", + "/blackcircleforrecord": "\u23FA", + "/power": "\u23FB", + "/poweronoff": "\u23FC", + "/poweron": "\u23FD", + "/powersleep": "\u23FE", + "/observereye": "\u23FF", + "/cntr:null": "\u2400", + "/cntr:startofheading": "\u2401", + "/cntr:startoftext": "\u2402", + "/cntr:endoftext": "\u2403", + "/cntr:endoftransmission": "\u2404", + "/cntr:enquiry": "\u2405", + "/cntr:acknowledge": "\u2406", + "/cntr:bell": "\u2407", + "/cntr:backspace": "\u2408", + "/cntr:horizontaltab": "\u2409", + "/cntr:linefeed": "\u240A", + "/cntr:verticaltab": "\u240B", + "/cntr:formfeed": "\u240C", + "/cntr:carriagereturn": "\u240D", + "/cntr:shiftout": "\u240E", + "/cntr:shiftin": "\u240F", + "/cntr:datalinkescape": "\u2410", + "/cntr:devicecontrolone": "\u2411", + "/cntr:devicecontroltwo": "\u2412", + "/cntr:devicecontrolthree": "\u2413", + "/cntr:devicecontrolfour": "\u2414", + "/cntr:negativeacknowledge": "\u2415", + "/cntr:synchronousidle": "\u2416", + "/cntr:endoftransmissionblock": "\u2417", + "/cntr:cancel": "\u2418", + "/cntr:endofmedium": "\u2419", + "/cntr:substitute": "\u241A", + "/cntr:escape": "\u241B", + "/cntr:fileseparator": "\u241C", + "/cntr:groupseparator": "\u241D", + "/cntr:recordseparator": "\u241E", + "/cntr:unitseparator": "\u241F", + "/cntr:space": "\u2420", + "/cntr:delete": "\u2421", + "/cntr:blank": "\u2422", + "/cntr:openbox": "\u2423", + "/cntr:newline": "\u2424", + "/cntr:deleteformtwo": "\u2425", + "/cntr:substituteformtwo": "\u2426", + "/cntr:deletesquarecheckerboardform": "\u2427", + "/cntr:deleterectangularcheckerboardform": "\u2428", + "/cntr:deletemediumshadeform": "\u2429", + "/ocr:hook": "\u2440", + "/ocr:chair": "\u2441", + "/fork": "\u2442", + "/invertedfork": "\u2443", + "/beltbuckle": "\u2444", + "/ocr:bowtie": "\u2445", + "/branchbankidentification": "\u2446", + "/amountofcheck": "\u2447", + "/ocr:dash": "\u2448", + "/customeraccountnumber": "\u2449", + "/backslashdbl": "\u244A", + "/oneparenthesized": "\u2474", + "/twoparenthesized": "\u2475", + "/threeparenthesized": "\u2476", + "/fourparenthesized": "\u2477", + "/fiveparenthesized": "\u2478", + "/sixparenthesized": "\u2479", + "/sevenparenthesized": "\u247A", + "/eightparenthesized": "\u247B", + "/nineparenthesized": "\u247C", + "/tenparenthesized": "\u247D", + "/elevenparenthesized": "\u247E", + "/twelveparenthesized": "\u247F", + "/thirteenparenthesized": "\u2480", + "/fourteenparenthesized": "\u2481", + "/fifteenparenthesized": "\u2482", + "/sixteenparenthesized": "\u2483", + "/seventeenparenthesized": "\u2484", + "/eighteenparenthesized": "\u2485", + "/nineteenparenthesized": "\u2486", + "/twentyparenthesized": "\u2487", + "/aparenthesized": "\u249C", + "/bparenthesized": "\u249D", + "/cparenthesized": "\u249E", + "/dparenthesized": "\u249F", + "/eparenthesized": "\u24A0", + "/fparenthesized": "\u24A1", + "/gparenthesized": "\u24A2", + "/hparenthesized": "\u24A3", + "/iparenthesized": "\u24A4", + "/jparenthesized": "\u24A5", + "/kparenthesized": "\u24A6", + "/lparenthesized": "\u24A7", + "/mparenthesized": "\u24A8", + "/nparenthesized": "\u24A9", + "/oparenthesized": "\u24AA", + "/pparenthesized": "\u24AB", + "/qparenthesized": "\u24AC", + "/rparenthesized": "\u24AD", + "/sparenthesized": "\u24AE", + "/tparenthesized": "\u24AF", + "/uparenthesized": "\u24B0", + "/vparenthesized": "\u24B1", + "/wparenthesized": "\u24B2", + "/xparenthesized": "\u24B3", + "/yparenthesized": "\u24B4", + "/zparenthesized": "\u24B5", + "/zerocircle": "\u24EA", + "/elevencircleblack": "\u24EB", + "/twelvecircleblack": "\u24EC", + "/thirteencircleblack": "\u24ED", + "/fourteencircleblack": "\u24EE", + "/fifteencircleblack": "\u24EF", + "/sixteencircleblack": "\u24F0", + "/seventeencircleblack": "\u24F1", + "/eighteencircleblack": "\u24F2", + "/nineteencircleblack": "\u24F3", + "/twentycircleblack": "\u24F4", + "/onecircledbl": "\u24F5", + "/twocircledbl": "\u24F6", + "/threecircledbl": "\u24F7", + "/fourcircledbl": "\u24F8", + "/fivecircledbl": "\u24F9", + "/sixcircledbl": "\u24FA", + "/sevencircledbl": "\u24FB", + "/eightcircledbl": "\u24FC", + "/ninecircledbl": "\u24FD", + "/tencircledbl": "\u24FE", + "/zerocircleblack": "\u24FF", + "/lighthorz": "\u2500", + "/heavyhorz": "\u2501", + "/lightvert": "\u2502", + "/heavyvert": "\u2503", + "/lighttrpldashhorz": "\u2504", + "/heavytrpldashhorz": "\u2505", + "/lighttrpldashvert": "\u2506", + "/heavytrpldashvert": "\u2507", + "/lightquaddashhorz": "\u2508", + "/heavyquaddashhorz": "\u2509", + "/lightquaddashvert": "\u250A", + "/heavyquaddashvert": "\u250B", + "/lightdnright": "\u250C", + "/dnlightrightheavy": "\u250D", + "/dnheavyrightlight": "\u250E", + "/heavydnright": "\u250F", + "/lightdnleft": "\u2510", + "/dnlightleftheavy": "\u2511", + "/dnheavyleftlight": "\u2512", + "/heavydnleft": "\u2513", + "/lightupright": "\u2514", + "/uplightrightheavy": "\u2515", + "/upheavyrightlight": "\u2516", + "/heavyupright": "\u2517", + "/lightupleft": "\u2518", + "/uplightleftheavy": "\u2519", + "/upheavyleftlight": "\u251A", + "/heavyupleft": "\u251B", + "/lightvertright": "\u251C", + "/vertlightrightheavy": "\u251D", + "/upheavyrightdnlight": "\u251E", + "/dnheavyrightuplight": "\u251F", + "/vertheavyrightlight": "\u2520", + "/dnlightrightupheavy": "\u2521", + "/uplightrightdnheavy": "\u2522", + "/heavyvertright": "\u2523", + "/lightvertleft": "\u2524", + "/vertlightleftheavy": "\u2525", + "/upheavyleftdnlight": "\u2526", + "/dnheavyleftuplight": "\u2527", + "/vertheavyleftlight": "\u2528", + "/dnlightleftupheavy": "\u2529", + "/uplightleftdnheavy": "\u252A", + "/heavyvertleft": "\u252B", + "/lightdnhorz": "\u252C", + "/leftheavyrightdnlight": "\u252D", + "/rightheavyleftdnlight": "\u252E", + "/dnlighthorzheavy": "\u252F", + "/dnheavyhorzlight": "\u2530", + "/rightlightleftdnheavy": "\u2531", + "/leftlightrightdnheavy": "\u2532", + "/heavydnhorz": "\u2533", + "/lightuphorz": "\u2534", + "/leftheavyrightuplight": "\u2535", + "/rightheavyleftuplight": "\u2536", + "/uplighthorzheavy": "\u2537", + "/upheavyhorzlight": "\u2538", + "/rightlightleftupheavy": "\u2539", + "/leftlightrightupheavy": "\u253A", + "/heavyuphorz": "\u253B", + "/lightverthorz": "\u253C", + "/leftheavyrightvertlight": "\u253D", + "/rightheavyleftvertlight": "\u253E", + "/vertlighthorzheavy": "\u253F", + "/upheavydnhorzlight": "\u2540", + "/dnheavyuphorzlight": "\u2541", + "/vertheavyhorzlight": "\u2542", + "/leftupheavyrightdnlight": "\u2543", + "/rightupheavyleftdnlight": "\u2544", + "/leftdnheavyrightuplight": "\u2545", + "/rightdnheavyleftuplight": "\u2546", + "/dnlightuphorzheavy": "\u2547", + "/uplightdnhorzheavy": "\u2548", + "/rightlightleftvertheavy": "\u2549", + "/leftlightrightvertheavy": "\u254A", + "/heavyverthorz": "\u254B", + "/lightdbldashhorz": "\u254C", + "/heavydbldashhorz": "\u254D", + "/lightdbldashvert": "\u254E", + "/heavydbldashvert": "\u254F", + "/dblhorz": "\u2550", + "/dblvert": "\u2551", + "/dnsngrightdbl": "\u2552", + "/dndblrightsng": "\u2553", + "/dbldnright": "\u2554", + "/dnsngleftdbl": "\u2555", + "/dndblleftsng": "\u2556", + "/dbldnleft": "\u2557", + "/upsngrightdbl": "\u2558", + "/updblrightsng": "\u2559", + "/dblupright": "\u255A", + "/upsngleftdbl": "\u255B", + "/updblleftsng": "\u255C", + "/dblupleft": "\u255D", + "/vertsngrightdbl": "\u255E", + "/vertdblrightsng": "\u255F", + "/dblvertright": "\u2560", + "/vertsngleftdbl": "\u2561", + "/vertdblleftsng": "\u2562", + "/dblvertleft": "\u2563", + "/dnsnghorzdbl": "\u2564", + "/dndblhorzsng": "\u2565", + "/dbldnhorz": "\u2566", + "/upsnghorzdbl": "\u2567", + "/updblhorzsng": "\u2568", + "/dbluphorz": "\u2569", + "/vertsnghorzdbl": "\u256A", + "/vertdblhorzsng": "\u256B", + "/dblverthorz": "\u256C", + "/lightarcdnright": "\u256D", + "/lightarcdnleft": "\u256E", + "/lightarcupleft": "\u256F", + "/lightarcupright": "\u2570", + "/lightdiaguprightdnleft": "\u2571", + "/lightdiagupleftdnright": "\u2572", + "/lightdiagcross": "\u2573", + "/lightleft": "\u2574", + "/lightup": "\u2575", + "/lightright": "\u2576", + "/lightdn": "\u2577", + "/heavyleft": "\u2578", + "/heavyup": "\u2579", + "/heavyright": "\u257A", + "/heavydn": "\u257B", + "/lightleftheavyright": "\u257C", + "/lightupheavydn": "\u257D", + "/heavyleftlightright": "\u257E", + "/heavyuplightdn": "\u257F", + "/upperHalfBlock": "\u2580", + "/lowerOneEighthBlock": "\u2581", + "/lowerOneQuarterBlock": "\u2582", + "/lowerThreeEighthsBlock": "\u2583", + "/lowerHalfBlock": "\u2584", + "/lowerFiveEighthsBlock": "\u2585", + "/lowerThreeQuartersBlock": "\u2586", + "/lowerSevenEighthsBlock": "\u2587", + "/fullBlock": "\u2588", + "/leftSevenEighthsBlock": "\u2589", + "/leftThreeQuartersBlock": "\u258A", + "/leftFiveEighthsBlock": "\u258B", + "/leftHalfBlock": "\u258C", + "/leftThreeEighthsBlock": "\u258D", + "/leftOneQuarterBlock": "\u258E", + "/leftOneEighthBlock": "\u258F", + "/rightHalfBlock": "\u2590", + "/lightShade": "\u2591", + "/mediumShade": "\u2592", + "/darkShade": "\u2593", + "/upperOneEighthBlock": "\u2594", + "/rightOneEighthBlock": "\u2595", + "/quadrantLowerLeft": "\u2596", + "/quadrantLowerRight": "\u2597", + "/quadrantUpperLeft": "\u2598", + "/quadrantUpperLeftAndLowerLeftAndLowerRight": "\u2599", + "/quadrantUpperLeftAndLowerRight": "\u259A", + "/quadrantUpperLeftAndUpperRightAndLowerLeft": "\u259B", + "/quadrantUpperLeftAndUpperRightAndLowerRight": "\u259C", + "/quadrantUpperRight": "\u259D", + "/quadrantUpperRightAndLowerLeft": "\u259E", + "/quadrantUpperRightAndLowerLeftAndLowerRight": "\u259F", + "/squareblack": "\u25A0", + "/squarewhite": "\u25A1", + "/squarewhiteround": "\u25A2", + "/squarewhitewithsquaresmallblack": "\u25A3", + "/squaresmallblack": "\u25AA", + "/squaresmallwhite": "\u25AB", + "/rectangleblack": "\u25AC", + "/rectanglewhite": "\u25AD", + "/rectangleverticalblack": "\u25AE", + "/rectangleverticalwhite": "\u25AF", + "/parallelogramblack": "\u25B0", + "/parallelogramwhite": "\u25B1", + "/triangleupblack": "\u25B2", + "/triangleupwhite": "\u25B3", + "/triangleupsmallblack": "\u25B4", + "/triangleupsmallwhite": "\u25B5", + "/trianglerightblack": "\u25B6", + "/trianglerightwhite": "\u25B7", + "/trianglerightsmallblack": "\u25B8", + "/trianglerightsmallwhite": "\u25B9", + "/pointerrightblack": "\u25BA", + "/pointerrightwhite": "\u25BB", + "/triangledownblack": "\u25BC", + "/triangledownwhite": "\u25BD", + "/triangledownsmallblack": "\u25BE", + "/triangledownsmallwhite": "\u25BF", + "/triangleleftblack": "\u25C0", + "/triangleleftwhite": "\u25C1", + "/triangleleftsmallblack": "\u25C2", + "/triangleleftsmallwhite": "\u25C3", + "/pointerleftblack": "\u25C4", + "/pointerleftwhite": "\u25C5", + "/gmtr:diamondblack": "\u25C6", + "/gmtr:diamondwhite": "\u25C7", + "/diamondwhitewithdiamondsmallblack": "\u25C8", + "/circlewhite": "\u25CB", + "/circledotted": "\u25CC", + "/circleverticalfill": "\u25CD", + "/circleblack": "\u25CF", + "/circlehalfleftblack": "\u25D0", + "/circlehalfrightblack": "\u25D1", + "/circlelowerhalfblack": "\u25D2", + "/circleupperhalfblack": "\u25D3", + "/circleupperquadrantrightblack": "\u25D4", + "/circleallbutupperquadrantleftblack": "\u25D5", + "/halfcircleleftblack": "\u25D6", + "/halfcirclerightblack": "\u25D7", + "/circleinversewhite": "\u25D9", + "/upperhalfcircleinversewhite": "\u25DA", + "/lowerhalfcircleinversewhite": "\u25DB", + "/upperquadrantcirculararcleft": "\u25DC", + "/upperquadrantcirculararcright": "\u25DD", + "/lowerquadrantcirculararcright": "\u25DE", + "/lowerquadrantcirculararcleft": "\u25DF", + "/upperhalfcircle": "\u25E0", + "/lowerhalfcircle": "\u25E1", + "/lowertrianglerightblack": "\u25E2", + "/lowertriangleleftblack": "\u25E3", + "/uppertriangleleftblack": "\u25E4", + "/uppertrianglerightblack": "\u25E5", + "/squarehalfleftblack": "\u25E7", + "/squarehalfrightblack": "\u25E8", + "/squareupperdiagonalhalfleftblack": "\u25E9", + "/squarelowerdiagonalhalfrightblack": "\u25EA", + "/squarewhitebisectinglinevertical": "\u25EB", + "/triangledotupwhite": "\u25EC", + "/trianglehalfupleftblack": "\u25ED", + "/trianglehalfuprightblack": "\u25EE", + "/squarewhiteupperquadrantleft": "\u25F0", + "/squarewhitelowerquadrantleft": "\u25F1", + "/squarewhitelowerquadrantright": "\u25F2", + "/squarewhiteupperquadrantright": "\u25F3", + "/circleupperquadrantleftwhite": "\u25F4", + "/circlelowerquadrantleftwhite": "\u25F5", + "/circlelowerquadrantrightwhite": "\u25F6", + "/circleupperquadrantrightwhite": "\u25F7", + "/uppertriangleleft": "\u25F8", + "/uppertriangleright": "\u25F9", + "/lowertriangleleft": "\u25FA", + "/squaremediumwhite": "\u25FB", + "/squaremediumblack": "\u25FC", + "/squaresmallmediumwhite": "\u25FD", + "/squaresmallmediumblack": "\u25FE", + "/lowertriangleright": "\u25FF", + "/sunraysblack": "\u2600", + "/cloud": "\u2601", + "/umbrella": "\u2602", + "/snowman": "\u2603", + "/comet": "\u2604", + "/starblack": "\u2605", + "/starwhite": "\u2606", + "/lightning": "\u2607", + "/thunderstorm": "\u2608", + "/nodeascending": "\u260A", + "/nodedescending": "\u260B", + "/conjunction": "\u260C", + "/opposition": "\u260D", + "/telephonewhite": "\u260F", + "/checkbox": "\u2610", + "/checkboxchecked": "\u2611", + "/checkboxx": "\u2612", + "/saltire": "\u2613", + "/umbrellaraindrops": "\u2614", + "/hotbeverage": "\u2615", + "/shogipiecewhite": "\u2616", + "/shogipieceblack": "\u2617", + "/shamrock": "\u2618", + "/floralheartbulletreversedrotated": "\u2619", + "/pointingindexleftblack": "\u261A", + "/pointingindexrightblack": "\u261B", + "/skullcrossbones": "\u2620", + "/caution": "\u2621", + "/radioactive": "\u2622", + "/biohazard": "\u2623", + "/caduceus": "\u2624", + "/ankh": "\u2625", + "/orthodoxcross": "\u2626", + "/chirho": "\u2627", + "/crossoflorraine": "\u2628", + "/crossofjerusalem": "\u2629", + "/starcrescent": "\u262A", + "/farsi": "\u262B", + "/adishakti": "\u262C", + "/hammersickle": "\u262D", + "/peace": "\u262E", + "/trigramheaven": "\u2630", + "/trigramlake": "\u2631", + "/trigramfire": "\u2632", + "/trigramthunder": "\u2633", + "/trigramwind": "\u2634", + "/trigramwater": "\u2635", + "/trigrammountain": "\u2636", + "/trigramearth": "\u2637", + "/wheelofdharma": "\u2638", + "/frowningfacewhite": "\u2639", + "/smilingfacewhite": "\u263A", + "/smilingfaceblack": "\u263B", + "/sunrayswhite": "\u263C", + "/firstquartermoon": "\u263D", + "/lastquartermoon": "\u263E", + "/mercury": "\u263F", + "/jiterup": "\u2643", + "/saturn": "\u2644", + "/uranus": "\u2645", + "/neptune": "\u2646", + "/pluto": "\u2647", + "/aries": "\u2648", + "/taurus": "\u2649", + "/gemini": "\u264A", + "/cancer": "\u264B", + "/leo": "\u264C", + "/virgo": "\u264D", + "/libra": "\u264E", + "/scorpius": "\u264F", + "/sagittarius": "\u2650", + "/capricorn": "\u2651", + "/aquarius": "\u2652", + "/pisces": "\u2653", + "/kingwhite": "\u2654", + "/queenwhite": "\u2655", + "/rookwhite": "\u2656", + "/bishopwhite": "\u2657", + "/knightwhite": "\u2658", + "/pawnwhite": "\u2659", + "/kingblack": "\u265A", + "/queenblack": "\u265B", + "/rookblack": "\u265C", + "/bishopblack": "\u265D", + "/knightblack": "\u265E", + "/pawnblack": "\u265F", + "/spadeblack": "\u2660", + "/heartwhite": "\u2661", + "/misc:diamondwhite": "\u2662", + "/clubblack": "\u2663", + "/spadewhite": "\u2664", + "/heartblack": "\u2665", + "/misc:diamondblack": "\u2666", + "/clubwhite": "\u2667", + "/eighthnote": "\u266A", + "/beamedeighthnotes": "\u266B", + "/musicflat": "\u266D", + "/musicnatural": "\u266E", + "/musicsharp": "\u266F", + "/westsyriaccross": "\u2670", + "/eastsyriaccross": "\u2671", + "/recycleuniversal": "\u2672", + "/recycleoneplastics": "\u2673", + "/recycletwoplastics": "\u2674", + "/recyclethreeplastics": "\u2675", + "/recyclefourplastics": "\u2676", + "/recyclefiveplastics": "\u2677", + "/recyclesixplastics": "\u2678", + "/recyclesevenplastics": "\u2679", + "/recyclegeneric": "\u267A", + "/recycleuniversalblack": "\u267B", + "/recycledpaper": "\u267C", + "/recyclepartiallypaper": "\u267D", + "/permanentpaper": "\u267E", + "/wheelchair": "\u267F", + "/dieone": "\u2680", + "/dietwo": "\u2681", + "/diethree": "\u2682", + "/diefour": "\u2683", + "/diefive": "\u2684", + "/diesix": "\u2685", + "/circledotrightwhite": "\u2686", + "/circletwodotswhite": "\u2687", + "/circlewhitedotrightblack": "\u2688", + "/circletwodotsblackwhite": "\u2689", + "/monogramyang": "\u268A", + "/monogramyin": "\u268B", + "/digramgreateryang": "\u268C", + "/digramlesseryin": "\u268D", + "/digramlesseryang": "\u268E", + "/digramgreateryin": "\u268F", + "/flagwhite": "\u2690", + "/flagblack": "\u2691", + "/hammerpick": "\u2692", + "/anchor": "\u2693", + "/crossedswords": "\u2694", + "/staffofaesculapius": "\u2695", + "/scales": "\u2696", + "/alembic": "\u2697", + "/flower": "\u2698", + "/gear": "\u2699", + "/staffofhermes": "\u269A", + "/atom": "\u269B", + "/fleurdelis": "\u269C", + "/staroutlinedwhite": "\u269D", + "/threelinesconvergingright": "\u269E", + "/threelinesconvergingleft": "\u269F", + "/warning": "\u26A0", + "/highvoltage": "\u26A1", + "/dfemaledbl": "\u26A2", + "/dmaledbl": "\u26A3", + "/interlockedfemalemale": "\u26A4", + "/malefemale": "\u26A5", + "/malestroke": "\u26A6", + "/malestrokemalefemale": "\u26A7", + "/verticalmalestroke": "\u26A8", + "/horizontalmalestroke": "\u26A9", + "/mediumcirclewhite": "\u26AA", + "/mediumcircleblack": "\u26AB", + "/mediumsmallcirclewhite": "\u26AC", + "/marriage": "\u26AD", + "/divorce": "\u26AE", + "/unmarriedpartnership": "\u26AF", + "/coffin": "\u26B0", + "/funeralurn": "\u26B1", + "/neuter": "\u26B2", + "/ceres": "\u26B3", + "/pallas": "\u26B4", + "/juno": "\u26B5", + "/vesta": "\u26B6", + "/chiron": "\u26B7", + "/moonlilithblack": "\u26B8", + "/sextile": "\u26B9", + "/semisextile": "\u26BA", + "/quincunx": "\u26BB", + "/sesquiquadrate": "\u26BC", + "/soccerball": "\u26BD", + "/baseball": "\u26BE", + "/squaredkey": "\u26BF", + "/draughtsmanwhite": "\u26C0", + "/draughtskingwhite": "\u26C1", + "/draughtsmanblack": "\u26C2", + "/draughtskingblack": "\u26C3", + "/snowmanoutsnow": "\u26C4", + "/sunbehindcloud": "\u26C5", + "/rain": "\u26C6", + "/snowmanblack": "\u26C7", + "/thundercloudrain": "\u26C8", + "/turnedshogipiecewhite": "\u26C9", + "/turnedshogipieceblack": "\u26CA", + "/diamondinsquarewhite": "\u26CB", + "/crossinglanes": "\u26CC", + "/disabledcar": "\u26CD", + "/ophiuchus": "\u26CE", + "/pick": "\u26CF", + "/carsliding": "\u26D0", + "/helmetcrosswhite": "\u26D1", + "/circledcrossinglanes": "\u26D2", + "/chains": "\u26D3", + "/noentry": "\u26D4", + "/alternateonewayleftwaytraffic": "\u26D5", + "/twowayleftwaytrafficblack": "\u26D6", + "/twowayleftwaytrafficwhite": "\u26D7", + "/lanemergeleftblack": "\u26D8", + "/lanemergeleftwhite": "\u26D9", + "/driveslow": "\u26DA", + "/pointingtriangledownheavywhite": "\u26DB", + "/closedentryleft": "\u26DC", + "/squaredsaltire": "\u26DD", + "/fallingdiagonalincircleinsquareblackwhite": "\u26DE", + "/truckblack": "\u26DF", + "/restrictedentryoneleft": "\u26E0", + "/restrictedentrytwoleft": "\u26E1", + "/astronomicaluranus": "\u26E2", + "/circlestroketwodotsaboveheavy": "\u26E3", + "/pentagram": "\u26E4", + "/hedinterlacedpentagramright": "\u26E5", + "/hedinterlacedpentagramleft": "\u26E6", + "/invertedpentagram": "\u26E7", + "/crossonshieldblack": "\u26E8", + "/shintoshrine": "\u26E9", + "/church": "\u26EA", + "/castle": "\u26EB", + "/historicsite": "\u26EC", + "/gearouthub": "\u26ED", + "/gearhles": "\u26EE", + "/maplighthouse": "\u26EF", + "/mountain": "\u26F0", + "/umbrellaonground": "\u26F1", + "/fountain": "\u26F2", + "/flaginhole": "\u26F3", + "/ferry": "\u26F4", + "/sailboat": "\u26F5", + "/squarefourcorners": "\u26F6", + "/skier": "\u26F7", + "/iceskate": "\u26F8", + "/personball": "\u26F9", + "/tent": "\u26FA", + "/japanesebank": "\u26FB", + "/headstonegraveyard": "\u26FC", + "/fuelpump": "\u26FD", + "/consquareupblack": "\u26FE", + "/flaghorizontalmiddlestripeblackwhite": "\u26FF", + "/safetyscissorsblack": "\u2700", + "/scissorsupperblade": "\u2701", + "/scissorsblack": "\u2702", + "/scissorslowerblade": "\u2703", + "/scissorswhite": "\u2704", + "/checkwhiteheavy": "\u2705", + "/telephonelocationsign": "\u2706", + "/tapedrive": "\u2707", + "/airplane": "\u2708", + "/envelope": "\u2709", + "/raisedfist": "\u270A", + "/raisedh": "\u270B", + "/hvictory": "\u270C", + "/hwriting": "\u270D", + "/pencillowerright": "\u270E", + "/pencil": "\u270F", + "/pencilupperright": "\u2710", + "/nibwhite": "\u2711", + "/nibblack": "\u2712", + "/check": "\u2713", + "/checkheavy": "\u2714", + "/multiplicationx": "\u2715", + "/multiplicationxheavy": "\u2716", + "/ballotx": "\u2717", + "/ballotxheavy": "\u2718", + "/greekcrossoutlined": "\u2719", + "/greekcrossheavy": "\u271A", + "/crosscentreopen": "\u271B", + "/crosscentreopenheavy": "\u271C", + "/latincross": "\u271D", + "/latincrossshadowedwhite": "\u271E", + "/latincrossoutlined": "\u271F", + "/maltesecross": "\u2720", + "/starofdavid": "\u2721", + "/asteriskteardropfour": "\u2722", + "/asteriskballoonfour": "\u2723", + "/asteriskballoonheavyfour": "\u2724", + "/asteriskclubfour": "\u2725", + "/starpointedblackfour": "\u2726", + "/starpointedwhitefour": "\u2727", + "/sparkles": "\u2728", + "/staroutlinedstresswhite": "\u2729", + "/starcircledwhite": "\u272A", + "/starcentreopenblack": "\u272B", + "/starcentreblackwhite": "\u272C", + "/staroutlinedblack": "\u272D", + "/staroutlinedblackheavy": "\u272E", + "/starpinwheel": "\u272F", + "/starshadowedwhite": "\u2730", + "/asteriskheavy": "\u2731", + "/asteriskcentreopen": "\u2732", + "/spokedasteriskeight": "\u2733", + "/starpointedblackeight": "\u2734", + "/starpointedpinwheeleight": "\u2735", + "/starpointedblacksix": "\u2736", + "/compasstarpointedblackeight": "\u2737", + "/compasstarpointedblackheavyeight": "\u2738", + "/starpointedblacktwelve": "\u2739", + "/asteriskpointedsixteen": "\u273A", + "/asteriskteardrop": "\u273B", + "/asteriskteardropcentreopen": "\u273C", + "/asteriskteardropheavy": "\u273D", + "/florettepetalledblackwhitesix": "\u273E", + "/floretteblack": "\u273F", + "/florettewhite": "\u2740", + "/floretteoutlinedpetalledblackeight": "\u2741", + "/starcentreopenpointedcircledeight": "\u2742", + "/asteriskteardroppinwheelheavy": "\u2743", + "/snowflake": "\u2744", + "/snowflaketight": "\u2745", + "/chevronsnowflakeheavy": "\u2746", + "/sparkle": "\u2747", + "/sparkleheavy": "\u2748", + "/asteriskballoon": "\u2749", + "/asteriskteardroppropellereight": "\u274A", + "/asteriskteardroppropellerheavyeight": "\u274B", + "/cross": "\u274C", + "/circleshadowedwhite": "\u274D", + "/squaredcrossnegative": "\u274E", + "/squareshadowlowerrightwhite": "\u274F", + "/squareshadowupperrightwhite": "\u2750", + "/squarelowerrightshadowedwhite": "\u2751", + "/squareupperrightshadowedwhite": "\u2752", + "/questionblackornament": "\u2753", + "/questionwhiteornament": "\u2754", + "/exclamationwhiteornament": "\u2755", + "/diamondminusxblackwhite": "\u2756", + "/exclamationheavy": "\u2757", + "/verticalbarlight": "\u2758", + "/verticalbarmedium": "\u2759", + "/verticalbarheavy": "\u275A", + "/commaheavyturnedornament": "\u275B", + "/commaheavyornament": "\u275C", + "/commaheavydoubleturnedornament": "\u275D", + "/commaheavydoubleornament": "\u275E", + "/lowcommaheavyornament": "\u275F", + "/lowcommaheavydoubleornament": "\u2760", + "/curvedstemparagraphsignornament": "\u2761", + "/exclamationheavyornament": "\u2762", + "/heartexclamationheavyornament": "\u2763", + "/heartblackheavy": "\u2764", + "/heartbulletrotatedblackheavy": "\u2765", + "/floralheart": "\u2766", + "/floralheartbulletrotated": "\u2767", + "/parenthesisleftmediumornament": "\u2768", + "/parenthesisrightmediumornament": "\u2769", + "/parenthesisleftflattenedmediumornament": "\u276A", + "/parenthesisrightflattenedmediumornament": "\u276B", + "/bracketleftpointedanglemediumornament": "\u276C", + "/bracketrightpointedanglemediumornament": "\u276D", + "/quotationleftpointedangleheavyornament": "\u276E", + "/quotationrightpointedangleheavyornament": "\u276F", + "/bracketleftpointedangleheavyornament": "\u2770", + "/bracketrightpointedangleheavyornament": "\u2771", + "/bracketshellleftlightornament": "\u2772", + "/bracketshellrightlightornament": "\u2773", + "/curlybracketleftmediumornament": "\u2774", + "/curlybracketrightmediumornament": "\u2775", + "/onenegativecircled": "\u2776", + "/twonegativecircled": "\u2777", + "/threenegativecircled": "\u2778", + "/fournegativecircled": "\u2779", + "/fivenegativecircled": "\u277A", + "/sixnegativecircled": "\u277B", + "/sevennegativecircled": "\u277C", + "/eightnegativecircled": "\u277D", + "/ninenegativecircled": "\u277E", + "/tennegativecircled": "\u277F", + "/onesanscircled": "\u2780", + "/twosanscircled": "\u2781", + "/threesanscircled": "\u2782", + "/foursanscircled": "\u2783", + "/fivesanscircled": "\u2784", + "/sixsanscircled": "\u2785", + "/sevensanscircled": "\u2786", + "/eightsanscircled": "\u2787", + "/ninesanscircled": "\u2788", + "/tensanscircled": "\u2789", + "/onesansnegativecircled": "\u278A", + "/twosansnegativecircled": "\u278B", + "/threesansnegativecircled": "\u278C", + "/foursansnegativecircled": "\u278D", + "/fivesansnegativecircled": "\u278E", + "/sixsansnegativecircled": "\u278F", + "/sevensansnegativecircled": "\u2790", + "/eightsansnegativecircled": "\u2791", + "/ninesansnegativecircled": "\u2792", + "/tensansnegativecircled": "\u2793", + "/arrowrightwideheavy": "\u2794", + "/plussignheavy": "\u2795", + "/minussignheavy": "\u2796", + "/divisionsignheavy": "\u2797", + "/arrowheavySE": "\u2798", + "/arrowheavyNE": "\u279A", + "/arrowrightpointed": "\u279B", + "/arrowrightroundheavy": "\u279C", + "/arrowrighttriangle": "\u279D", + "/arrowrighttriangleheavy": "\u279E", + "/arrowrighttriangledashed": "\u279F", + "/arrowrighttriangledashedheavy": "\u27A0", + "/arrowrightblack": "\u27A1", + "/arrowheadrightthreeDtoplight": "\u27A2", + "/arrowheadrightthreeDbottomlight": "\u27A3", + "/arrowheadrightblack": "\u27A4", + "/arrowrightcurvedownblackheavy": "\u27A5", + "/arrowrightcurveupblackheavy": "\u27A6", + "/arrowrightsquatblack": "\u27A7", + "/arrowrightpointedblackheavy": "\u27A8", + "/arrowrightrightshadedwhite": "\u27A9", + "/arrowrightleftshadedwhite": "\u27AA", + "/arrowrightbacktiltedshadowedwhite": "\u27AB", + "/arrowrightfronttiltedshadowedwhite": "\u27AC", + "/arrowshadowrightlowerwhiteheavy": "\u27AD", + "/arrowshadowrightupperwhiteheavy": "\u27AE", + "/arrowshadowrightnotchedlowerwhite": "\u27AF", + "/curlyloop": "\u27B0", + "/arrowshadowrightnotchedupperwhite": "\u27B1", + "/arrowrightcircledwhiteheavy": "\u27B2", + "/arrowrightfeatheredwhite": "\u27B3", + "/arrowfeatheredblackSE": "\u27B4", + "/arrowrightfeatheredblack": "\u27B5", + "/arrowfeatheredblackNE": "\u27B6", + "/arrowfeatheredblackheavySE": "\u27B7", + "/arrowrightfeatheredblackheavy": "\u27B8", + "/arrowfeatheredblackheavyNE": "\u27B9", + "/arrowteardropright": "\u27BA", + "/arrowteardroprightheavy": "\u27BB", + "/arrowrightwedge": "\u27BC", + "/arrowrightwedgeheavy": "\u27BD", + "/arrowrightoutlinedopen": "\u27BE", + "/curlyloopdouble": "\u27BF", + "/threedimensionalangle": "\u27C0", + "/tricontainingtriwhiteanglesmall": "\u27C1", + "/opensubset": "\u27C3", + "/opensuperset": "\u27C4", + "/bagdelimitersshapeleft": "\u27C5", + "/bagdelimitersshaperight": "\u27C6", + "/ordotinside": "\u27C7", + "/solidussubsetreversepreceding": "\u27C8", + "/solidussupersetpreceding": "\u27C9", + "/verticalbarhorizontalstroke": "\u27CA", + "/risingdiagonal": "\u27CB", + "/longdivision": "\u27CC", + "/fallingdiagonal": "\u27CD", + "/dlogicalsquare": "\u27CE", + "/dlogicalorsquare": "\u27CF", + "/centreddotwhitediamond": "\u27D0", + "/andwithdot": "\u27D1", + "/elementopeningup": "\u27D2", + "/lowercornerdotright": "\u27D3", + "/cornerdotupleft": "\u27D4", + "/outerjoinleft": "\u27D5", + "/outerjoinright": "\u27D6", + "/outerjoinfull": "\u27D7", + "/largetackup": "\u27D8", + "/largetackdown": "\u27D9", + "/turnstileleftrightdbl": "\u27DA", + "/tackleftright": "\u27DB", + "/multimapleft": "\u27DC", + "/longtackright": "\u27DD", + "/longtackleft": "\u27DE", + "/tackcircleaboveup": "\u27DF", + "/lozengedividedbyrulehorizontal": "\u27E0", + "/convavediamondwhite": "\u27E1", + "/tickconvavediamondleftwhite": "\u27E2", + "/tickconvavediamondrightwhite": "\u27E3", + "/tickleftwhitesquare": "\u27E4", + "/tickrightwhitesquare": "\u27E5", + "/bracketwhitesquareleft": "\u27E6", + "/bracketwhitesquareright": "\u27E7", + "/bracketangleleft": "\u27E8", + "/bracketangleright": "\u27E9", + "/bracketangledblleft": "\u27EA", + "/bracketangledblright": "\u27EB", + "/bracketshellwhiteleft": "\u27EC", + "/bracketshellwhiteright": "\u27ED", + "/parenflatleft": "\u27EE", + "/parenflatright": "\u27EF", + "/brblank": "\u2800", + "/dots1": "\u2801", + "/dots2": "\u2802", + "/dots12": "\u2803", + "/dots3": "\u2804", + "/dots13": "\u2805", + "/dots23": "\u2806", + "/dots123": "\u2807", + "/dots4": "\u2808", + "/dots14": "\u2809", + "/dots24": "\u280A", + "/dots124": "\u280B", + "/dots34": "\u280C", + "/dots134": "\u280D", + "/dots234": "\u280E", + "/dots1234": "\u280F", + "/dots5": "\u2810", + "/dots15": "\u2811", + "/dots25": "\u2812", + "/dots125": "\u2813", + "/dots35": "\u2814", + "/dots135": "\u2815", + "/dots235": "\u2816", + "/dots1235": "\u2817", + "/dots45": "\u2818", + "/dots145": "\u2819", + "/dots245": "\u281A", + "/dots1245": "\u281B", + "/dots345": "\u281C", + "/dots1345": "\u281D", + "/dots2345": "\u281E", + "/dots12345": "\u281F", + "/dots6": "\u2820", + "/dots16": "\u2821", + "/dots26": "\u2822", + "/dots126": "\u2823", + "/dots36": "\u2824", + "/dots136": "\u2825", + "/dots236": "\u2826", + "/dots1236": "\u2827", + "/dots46": "\u2828", + "/dots146": "\u2829", + "/dots246": "\u282A", + "/dots1246": "\u282B", + "/dots346": "\u282C", + "/dots1346": "\u282D", + "/dots2346": "\u282E", + "/dots12346": "\u282F", + "/dots56": "\u2830", + "/dots156": "\u2831", + "/dots256": "\u2832", + "/dots1256": "\u2833", + "/dots356": "\u2834", + "/dots1356": "\u2835", + "/dots2356": "\u2836", + "/dots12356": "\u2837", + "/dots456": "\u2838", + "/dots1456": "\u2839", + "/dots2456": "\u283A", + "/dots12456": "\u283B", + "/dots3456": "\u283C", + "/dots13456": "\u283D", + "/dots23456": "\u283E", + "/dots123456": "\u283F", + "/dots7": "\u2840", + "/dots17": "\u2841", + "/dots27": "\u2842", + "/dots127": "\u2843", + "/dots37": "\u2844", + "/dots137": "\u2845", + "/dots237": "\u2846", + "/dots1237": "\u2847", + "/dots47": "\u2848", + "/dots147": "\u2849", + "/dots247": "\u284A", + "/dots1247": "\u284B", + "/dots347": "\u284C", + "/dots1347": "\u284D", + "/dots2347": "\u284E", + "/dots12347": "\u284F", + "/dots57": "\u2850", + "/dots157": "\u2851", + "/dots257": "\u2852", + "/dots1257": "\u2853", + "/dots357": "\u2854", + "/dots1357": "\u2855", + "/dots2357": "\u2856", + "/dots12357": "\u2857", + "/dots457": "\u2858", + "/dots1457": "\u2859", + "/dots2457": "\u285A", + "/dots12457": "\u285B", + "/dots3457": "\u285C", + "/dots13457": "\u285D", + "/dots23457": "\u285E", + "/dots123457": "\u285F", + "/dots67": "\u2860", + "/dots167": "\u2861", + "/dots267": "\u2862", + "/dots1267": "\u2863", + "/dots367": "\u2864", + "/dots1367": "\u2865", + "/dots2367": "\u2866", + "/dots12367": "\u2867", + "/dots467": "\u2868", + "/dots1467": "\u2869", + "/dots2467": "\u286A", + "/dots12467": "\u286B", + "/dots3467": "\u286C", + "/dots13467": "\u286D", + "/dots23467": "\u286E", + "/dots123467": "\u286F", + "/dots567": "\u2870", + "/dots1567": "\u2871", + "/dots2567": "\u2872", + "/dots12567": "\u2873", + "/dots3567": "\u2874", + "/dots13567": "\u2875", + "/dots23567": "\u2876", + "/dots123567": "\u2877", + "/dots4567": "\u2878", + "/dots14567": "\u2879", + "/dots24567": "\u287A", + "/dots124567": "\u287B", + "/dots34567": "\u287C", + "/dots134567": "\u287D", + "/dots234567": "\u287E", + "/dots1234567": "\u287F", + "/dots8": "\u2880", + "/dots18": "\u2881", + "/dots28": "\u2882", + "/dots128": "\u2883", + "/dots38": "\u2884", + "/dots138": "\u2885", + "/dots238": "\u2886", + "/dots1238": "\u2887", + "/dots48": "\u2888", + "/dots148": "\u2889", + "/dots248": "\u288A", + "/dots1248": "\u288B", + "/dots348": "\u288C", + "/dots1348": "\u288D", + "/dots2348": "\u288E", + "/dots12348": "\u288F", + "/dots58": "\u2890", + "/dots158": "\u2891", + "/dots258": "\u2892", + "/dots1258": "\u2893", + "/dots358": "\u2894", + "/dots1358": "\u2895", + "/dots2358": "\u2896", + "/dots12358": "\u2897", + "/dots458": "\u2898", + "/dots1458": "\u2899", + "/dots2458": "\u289A", + "/dots12458": "\u289B", + "/dots3458": "\u289C", + "/dots13458": "\u289D", + "/dots23458": "\u289E", + "/dots123458": "\u289F", + "/dots68": "\u28A0", + "/dots168": "\u28A1", + "/dots268": "\u28A2", + "/dots1268": "\u28A3", + "/dots368": "\u28A4", + "/dots1368": "\u28A5", + "/dots2368": "\u28A6", + "/dots12368": "\u28A7", + "/dots468": "\u28A8", + "/dots1468": "\u28A9", + "/dots2468": "\u28AA", + "/dots12468": "\u28AB", + "/dots3468": "\u28AC", + "/dots13468": "\u28AD", + "/dots23468": "\u28AE", + "/dots123468": "\u28AF", + "/dots568": "\u28B0", + "/dots1568": "\u28B1", + "/dots2568": "\u28B2", + "/dots12568": "\u28B3", + "/dots3568": "\u28B4", + "/dots13568": "\u28B5", + "/dots23568": "\u28B6", + "/dots123568": "\u28B7", + "/dots4568": "\u28B8", + "/dots14568": "\u28B9", + "/dots24568": "\u28BA", + "/dots124568": "\u28BB", + "/dots34568": "\u28BC", + "/dots134568": "\u28BD", + "/dots234568": "\u28BE", + "/dots1234568": "\u28BF", + "/dots78": "\u28C0", + "/dots178": "\u28C1", + "/dots278": "\u28C2", + "/dots1278": "\u28C3", + "/dots378": "\u28C4", + "/dots1378": "\u28C5", + "/dots2378": "\u28C6", + "/dots12378": "\u28C7", + "/dots478": "\u28C8", + "/dots1478": "\u28C9", + "/dots2478": "\u28CA", + "/dots12478": "\u28CB", + "/dots3478": "\u28CC", + "/dots13478": "\u28CD", + "/dots23478": "\u28CE", + "/dots123478": "\u28CF", + "/dots578": "\u28D0", + "/dots1578": "\u28D1", + "/dots2578": "\u28D2", + "/dots12578": "\u28D3", + "/dots3578": "\u28D4", + "/dots13578": "\u28D5", + "/dots23578": "\u28D6", + "/dots123578": "\u28D7", + "/dots4578": "\u28D8", + "/dots14578": "\u28D9", + "/dots24578": "\u28DA", + "/dots124578": "\u28DB", + "/dots34578": "\u28DC", + "/dots134578": "\u28DD", + "/dots234578": "\u28DE", + "/dots1234578": "\u28DF", + "/dots678": "\u28E0", + "/dots1678": "\u28E1", + "/dots2678": "\u28E2", + "/dots12678": "\u28E3", + "/dots3678": "\u28E4", + "/dots13678": "\u28E5", + "/dots23678": "\u28E6", + "/dots123678": "\u28E7", + "/dots4678": "\u28E8", + "/dots14678": "\u28E9", + "/dots24678": "\u28EA", + "/dots124678": "\u28EB", + "/dots34678": "\u28EC", + "/dots134678": "\u28ED", + "/dots234678": "\u28EE", + "/dots1234678": "\u28EF", + "/dots5678": "\u28F0", + "/dots15678": "\u28F1", + "/dots25678": "\u28F2", + "/dots125678": "\u28F3", + "/dots35678": "\u28F4", + "/dots135678": "\u28F5", + "/dots235678": "\u28F6", + "/dots1235678": "\u28F7", + "/dots45678": "\u28F8", + "/dots145678": "\u28F9", + "/dots245678": "\u28FA", + "/dots1245678": "\u28FB", + "/dots345678": "\u28FC", + "/dots1345678": "\u28FD", + "/dots2345678": "\u28FE", + "/dots12345678": "\u28FF", + "/rightwardsTwoheadedArrowWithVerticalStroke": "\u2900", + "/rightwardsTwoheadedArrowWithDoubleVerticalStroke": "\u2901", + "/leftwardsDoubleArrowWithVerticalStroke": "\u2902", + "/rightwardsDoubleArrowWithVerticalStroke": "\u2903", + "/leftRightDoubleArrowWithVerticalStroke": "\u2904", + "/rightwardsTwoheadedArrowFromBar": "\u2905", + "/leftwardsDoubleArrowFromBar": "\u2906", + "/rightwardsDoubleArrowFromBar": "\u2907", + "/downwardsArrowWithHorizontalStroke": "\u2908", + "/upwardsArrowWithHorizontalStroke": "\u2909", + "/upwardsTripleArrow": "\u290A", + "/downwardsTripleArrow": "\u290B", + "/leftwardsDoubleDashArrow": "\u290C", + "/rightwardsDoubleDashArrow": "\u290D", + "/leftwardsTripleDashArrow": "\u290E", + "/rightwardsTripleDashArrow": "\u290F", + "/rightwardsTwoheadedTripleDashArrow": "\u2910", + "/rightwardsArrowWithDottedStem": "\u2911", + "/upwardsArrowToBar": "\u2912", + "/downwardsArrowToBar": "\u2913", + "/rightwardsArrowWithTailWithVerticalStroke": "\u2914", + "/rightwardsArrowWithTailWithDoubleVerticalStroke": "\u2915", + "/rightwardsTwoheadedArrowWithTail": "\u2916", + "/rightwardsTwoheadedArrowWithTailWithVerticalStroke": "\u2917", + "/rightwardsTwoheadedArrowWithTailWithDoubleVerticalStroke": "\u2918", + "/leftwardsArrowtail": "\u2919", + "/rightwardsArrowtail": "\u291A", + "/leftwardsDoubleArrowtail": "\u291B", + "/rightwardsDoubleArrowtail": "\u291C", + "/leftwardsArrowToBlackDiamond": "\u291D", + "/rightwardsArrowToBlackDiamond": "\u291E", + "/leftwardsArrowFromBarToBlackDiamond": "\u291F", + "/rightwardsArrowFromBarToBlackDiamond": "\u2920", + "/northWestAndSouthEastArrow": "\u2921", + "/northEastAndSouthWestArrow": "\u2922", + "/northWestArrowWithHook": "\u2923", + "/northEastArrowWithHook": "\u2924", + "/southEastArrowWithHook": "\u2925", + "/southWestArrowWithHook": "\u2926", + "/northWestArrowAndNorthEastArrow": "\u2927", + "/northEastArrowAndSouthEastArrow": "\u2928", + "/southEastArrowAndSouthWestArrow": "\u2929", + "/southWestArrowAndNorthWestArrow": "\u292A", + "/risingDiagonalCrossingFallingDiagonal": "\u292B", + "/fallingDiagonalCrossingRisingDiagonal": "\u292C", + "/southEastArrowCrossingNorthEastArrow": "\u292D", + "/northEastArrowCrossingSouthEastArrow": "\u292E", + "/fallingDiagonalCrossingNorthEastArrow": "\u292F", + "/risingDiagonalCrossingSouthEastArrow": "\u2930", + "/northEastArrowCrossingNorthWestArrow": "\u2931", + "/northWestArrowCrossingNorthEastArrow": "\u2932", + "/waveArrowPointingDirectlyRight": "\u2933", + "/arrowPointingRightwardsThenCurvingUpwards": "\u2934", + "/arrowPointingRightwardsThenCurvingDownwards": "\u2935", + "/arrowPointingDownwardsThenCurvingLeftwards": "\u2936", + "/arrowPointingDownwardsThenCurvingRightwards": "\u2937", + "/rightsideArcClockwiseArrow": "\u2938", + "/leftsideArcAnticlockwiseArrow": "\u2939", + "/topArcAnticlockwiseArrow": "\u293A", + "/bottomArcAnticlockwiseArrow": "\u293B", + "/topArcClockwiseArrowWithMinus": "\u293C", + "/topArcAnticlockwiseArrowWithPlus": "\u293D", + "/lowerRightSemicircularClockwiseArrow": "\u293E", + "/lowerLeftSemicircularAnticlockwiseArrow": "\u293F", + "/anticlockwiseClosedCircleArrow": "\u2940", + "/clockwiseClosedCircleArrow": "\u2941", + "/rightwardsArrowAboveShortLeftwardsArrow": "\u2942", + "/leftwardsArrowAboveShortRightwardsArrow": "\u2943", + "/shortRightwardsArrowAboveLeftwardsArrow": "\u2944", + "/rightwardsArrowWithPlusBelow": "\u2945", + "/leftwardsArrowWithPlusBelow": "\u2946", + "/rightwardsArrowThroughX": "\u2947", + "/leftRightArrowThroughSmallCircle": "\u2948", + "/upwardsTwoheadedArrowFromSmallCircle": "\u2949", + "/leftBarbUpRightBarbDownHarpoon": "\u294A", + "/leftBarbDownRightBarbUpHarpoon": "\u294B", + "/upBarbRightDownBarbLeftHarpoon": "\u294C", + "/upBarbLeftDownBarbRightHarpoon": "\u294D", + "/leftBarbUpRightBarbUpHarpoon": "\u294E", + "/upBarbRightDownBarbRightHarpoon": "\u294F", + "/leftBarbDownRightBarbDownHarpoon": "\u2950", + "/upBarbLeftDownBarbLeftHarpoon": "\u2951", + "/leftwardsHarpoonWithBarbUpToBar": "\u2952", + "/rightwardsHarpoonWithBarbUpToBar": "\u2953", + "/upwardsHarpoonWithBarbRightToBar": "\u2954", + "/downwardsHarpoonWithBarbRightToBar": "\u2955", + "/leftwardsHarpoonWithBarbDownToBar": "\u2956", + "/rightwardsHarpoonWithBarbDownToBar": "\u2957", + "/upwardsHarpoonWithBarbLeftToBar": "\u2958", + "/downwardsHarpoonWithBarbLeftToBar": "\u2959", + "/leftwardsHarpoonWithBarbUpFromBar": "\u295A", + "/rightwardsHarpoonWithBarbUpFromBar": "\u295B", + "/upwardsHarpoonWithBarbRightFromBar": "\u295C", + "/downwardsHarpoonWithBarbRightFromBar": "\u295D", + "/leftwardsHarpoonWithBarbDownFromBar": "\u295E", + "/rightwardsHarpoonWithBarbDownFromBar": "\u295F", + "/upwardsHarpoonWithBarbLeftFromBar": "\u2960", + "/downwardsHarpoonWithBarbLeftFromBar": "\u2961", + "/leftwardsHarpoonWithBarbUpAboveLeftwardsHarpoonWithBarbDown": "\u2962", + "/upwardsHarpoonWithBarbLeftBesideUpwardsHarpoonWithBarbRight": "\u2963", + "/rightwardsHarpoonWithBarbUpAboveRightwardsHarpoonWithBarbDown": "\u2964", + "/downwardsHarpoonWithBarbLeftBesideDownwardsHarpoonWithBarbRight": "\u2965", + "/leftwardsHarpoonWithBarbUpAboveRightwardsHarpoonWithBarbUp": "\u2966", + "/leftwardsHarpoonWithBarbDownAboveRightwardsHarpoonWithBarbDown": "\u2967", + "/rightwardsHarpoonWithBarbUpAboveLeftwardsHarpoonWithBarbUp": "\u2968", + "/rightwardsHarpoonWithBarbDownAboveLeftwardsHarpoonWithBarbDown": "\u2969", + "/leftwardsHarpoonWithBarbUpAboveLongDash": "\u296A", + "/leftwardsHarpoonWithBarbDownBelowLongDash": "\u296B", + "/rightwardsHarpoonWithBarbUpAboveLongDash": "\u296C", + "/rightwardsHarpoonWithBarbDownBelowLongDash": "\u296D", + "/upwardsHarpoonWithBarbLeftBesideDownwardsHarpoonWithBarbRight": "\u296E", + "/downwardsHarpoonWithBarbLeftBesideUpwardsHarpoonWithBarbRight": "\u296F", + "/rightDoubleArrowWithRoundedHead": "\u2970", + "/equalsSignAboveRightwardsArrow": "\u2971", + "/tildeOperatorAboveRightwardsArrow": "\u2972", + "/leftwardsArrowAboveTildeOperator": "\u2973", + "/rightwardsArrowAboveTildeOperator": "\u2974", + "/rightwardsArrowAboveAlmostEqualTo": "\u2975", + "/lessthanAboveLeftwardsArrow": "\u2976", + "/leftwardsArrowThroughLessthan": "\u2977", + "/greaterthanAboveRightwardsArrow": "\u2978", + "/subsetAboveRightwardsArrow": "\u2979", + "/leftwardsArrowThroughSubset": "\u297A", + "/supersetAboveLeftwardsArrow": "\u297B", + "/leftFishTail": "\u297C", + "/rightFishTail": "\u297D", + "/upFishTail": "\u297E", + "/downFishTail": "\u297F", + "/tripleVerticalBarDelimiter": "\u2980", + "/zNotationSpot": "\u2981", + "/zNotationTypeColon": "\u2982", + "/leftWhiteCurlyBracket": "\u2983", + "/rightWhiteCurlyBracket": "\u2984", + "/leftWhiteParenthesis": "\u2985", + "/rightWhiteParenthesis": "\u2986", + "/zNotationLeftImageBracket": "\u2987", + "/zNotationRightImageBracket": "\u2988", + "/zNotationLeftBindingBracket": "\u2989", + "/zNotationRightBindingBracket": "\u298A", + "/leftSquareBracketWithUnderbar": "\u298B", + "/rightSquareBracketWithUnderbar": "\u298C", + "/leftSquareBracketWithTickInTopCorner": "\u298D", + "/rightSquareBracketWithTickInBottomCorner": "\u298E", + "/leftSquareBracketWithTickInBottomCorner": "\u298F", + "/rightSquareBracketWithTickInTopCorner": "\u2990", + "/leftAngleBracketWithDot": "\u2991", + "/rightAngleBracketWithDot": "\u2992", + "/leftArcLessthanBracket": "\u2993", + "/rightArcGreaterthanBracket": "\u2994", + "/doubleLeftArcGreaterthanBracket": "\u2995", + "/doubleRightArcLessthanBracket": "\u2996", + "/leftBlackTortoiseShellBracket": "\u2997", + "/rightBlackTortoiseShellBracket": "\u2998", + "/dottedFence": "\u2999", + "/verticalZigzagLine": "\u299A", + "/measuredAngleOpeningLeft": "\u299B", + "/rightAngleVariantWithSquare": "\u299C", + "/measuredRightAngleWithDot": "\u299D", + "/angleWithSInside": "\u299E", + "/acuteAngle": "\u299F", + "/sphericalAngleOpeningLeft": "\u29A0", + "/sphericalAngleOpeningUp": "\u29A1", + "/turnedAngle": "\u29A2", + "/reversedAngle": "\u29A3", + "/angleWithUnderbar": "\u29A4", + "/reversedAngleWithUnderbar": "\u29A5", + "/obliqueAngleOpeningUp": "\u29A6", + "/obliqueAngleOpeningDown": "\u29A7", + "/measuredAngleWithOpenArmEndingInArrowPointingUpAndRight": "\u29A8", + "/measuredAngleWithOpenArmEndingInArrowPointingUpAndLeft": "\u29A9", + "/measuredAngleWithOpenArmEndingInArrowPointingDownAndRight": "\u29AA", + "/measuredAngleWithOpenArmEndingInArrowPointingDownAndLeft": "\u29AB", + "/measuredAngleWithOpenArmEndingInArrowPointingRightAndUp": "\u29AC", + "/measuredAngleWithOpenArmEndingInArrowPointingLeftAndUp": "\u29AD", + "/measuredAngleWithOpenArmEndingInArrowPointingRightAndDown": "\u29AE", + "/measuredAngleWithOpenArmEndingInArrowPointingLeftAndDown": "\u29AF", + "/reversedEmptySet": "\u29B0", + "/emptySetWithOverbar": "\u29B1", + "/emptySetWithSmallCircleAbove": "\u29B2", + "/emptySetWithRightArrowAbove": "\u29B3", + "/emptySetWithLeftArrowAbove": "\u29B4", + "/circleWithHorizontalBar": "\u29B5", + "/circledVerticalBar": "\u29B6", + "/circledParallel": "\u29B7", + "/circledReverseSolidus": "\u29B8", + "/circledPerpendicular": "\u29B9", + "/circleDividedByHorizontalBarAndTopHalfDividedByVerticalBar": "\u29BA", + "/circleWithSuperimposedX": "\u29BB", + "/circledAnticlockwiserotatedDivisionSign": "\u29BC", + "/upArrowThroughCircle": "\u29BD", + "/circledWhiteBullet": "\u29BE", + "/circledBullet": "\u29BF", + "/circledLessthan": "\u29C0", + "/circledGreaterthan": "\u29C1", + "/circleWithSmallCircleToTheRight": "\u29C2", + "/circleWithTwoHorizontalStrokesToTheRight": "\u29C3", + "/squaredRisingDiagonalSlash": "\u29C4", + "/squaredFallingDiagonalSlash": "\u29C5", + "/squaredAsterisk": "\u29C6", + "/squaredSmallCircle": "\u29C7", + "/squaredSquare": "\u29C8", + "/twoJoinedSquares": "\u29C9", + "/triangleWithDotAbove": "\u29CA", + "/triangleWithUnderbar": "\u29CB", + "/sInTriangle": "\u29CC", + "/triangleWithSerifsAtBottom": "\u29CD", + "/rightTriangleAboveLeftTriangle": "\u29CE", + "/leftTriangleBesideVerticalBar": "\u29CF", + "/verticalBarBesideRightTriangle": "\u29D0", + "/bowtieWithLeftHalfBlack": "\u29D1", + "/bowtieWithRightHalfBlack": "\u29D2", + "/blackBowtie": "\u29D3", + "/timesWithLeftHalfBlack": "\u29D4", + "/timesWithRightHalfBlack": "\u29D5", + "/whiteHourglass": "\u29D6", + "/blackHourglass": "\u29D7", + "/leftWigglyFence": "\u29D8", + "/rightWigglyFence": "\u29D9", + "/leftDoubleWigglyFence": "\u29DA", + "/rightDoubleWigglyFence": "\u29DB", + "/incompleteInfinity": "\u29DC", + "/tieOverInfinity": "\u29DD", + "/infinityNegatedWithVerticalBar": "\u29DE", + "/doubleendedMultimap": "\u29DF", + "/squareWithContouredOutline": "\u29E0", + "/increasesAs": "\u29E1", + "/shuffleProduct": "\u29E2", + "/equalsSignAndSlantedParallel": "\u29E3", + "/equalsSignAndSlantedParallelWithTildeAbove": "\u29E4", + "/identicalToAndSlantedParallel": "\u29E5", + "/gleichStark": "\u29E6", + "/thermodynamic": "\u29E7", + "/downpointingTriangleWithLeftHalfBlack": "\u29E8", + "/downpointingTriangleWithRightHalfBlack": "\u29E9", + "/blackDiamondWithDownArrow": "\u29EA", + "/blackLozenge": "\u29EB", + "/whiteCircleWithDownArrow": "\u29EC", + "/blackCircleWithDownArrow": "\u29ED", + "/errorbarredWhiteSquare": "\u29EE", + "/errorbarredBlackSquare": "\u29EF", + "/errorbarredWhiteDiamond": "\u29F0", + "/errorbarredBlackDiamond": "\u29F1", + "/errorbarredWhiteCircle": "\u29F2", + "/errorbarredBlackCircle": "\u29F3", + "/ruledelayed": "\u29F4", + "/reverseSolidusOperator": "\u29F5", + "/solidusWithOverbar": "\u29F6", + "/reverseSolidusWithHorizontalStroke": "\u29F7", + "/bigSolidus": "\u29F8", + "/bigReverseSolidus": "\u29F9", + "/doublePlus": "\u29FA", + "/triplePlus": "\u29FB", + "/leftpointingCurvedAngleBracket": "\u29FC", + "/rightpointingCurvedAngleBracket": "\u29FD", + "/tiny": "\u29FE", + "/miny": "\u29FF", + "/naryCircledDotOperator": "\u2A00", + "/naryCircledPlusOperator": "\u2A01", + "/naryCircledTimesOperator": "\u2A02", + "/naryUnionOperatorWithDot": "\u2A03", + "/naryUnionOperatorWithPlus": "\u2A04", + "/narySquareIntersectionOperator": "\u2A05", + "/narySquareUnionOperator": "\u2A06", + "/twoLogicalAndOperator": "\u2A07", + "/twoLogicalOrOperator": "\u2A08", + "/naryTimesOperator": "\u2A09", + "/moduloTwoSum": "\u2A0A", + "/summationWithIntegral": "\u2A0B", + "/quadrupleIntegralOperator": "\u2A0C", + "/finitePartIntegral": "\u2A0D", + "/integralWithDoubleStroke": "\u2A0E", + "/integralAverageWithSlash": "\u2A0F", + "/circulationFunction": "\u2A10", + "/anticlockwiseIntegration": "\u2A11", + "/lineIntegrationWithRectangularPathAroundPole": "\u2A12", + "/lineIntegrationWithSemicircularPathAroundPole": "\u2A13", + "/lineIntegrationNotIncludingThePole": "\u2A14", + "/integralAroundAPointOperator": "\u2A15", + "/quaternionIntegralOperator": "\u2A16", + "/integralWithLeftwardsArrowWithHook": "\u2A17", + "/integralWithTimesSign": "\u2A18", + "/integralWithIntersection": "\u2A19", + "/integralWithUnion": "\u2A1A", + "/integralWithOverbar": "\u2A1B", + "/integralWithUnderbar": "\u2A1C", + "/join": "\u2A1D", + "/largeLeftTriangleOperator": "\u2A1E", + "/zNotationSchemaComposition": "\u2A1F", + "/zNotationSchemaPiping": "\u2A20", + "/zNotationSchemaProjection": "\u2A21", + "/plusSignWithSmallCircleAbove": "\u2A22", + "/plusSignWithCircumflexAccentAbove": "\u2A23", + "/plusSignWithTildeAbove": "\u2A24", + "/plusSignWithDotBelow": "\u2A25", + "/plusSignWithTildeBelow": "\u2A26", + "/plusSignWithSubscriptTwo": "\u2A27", + "/plusSignWithBlackTriangle": "\u2A28", + "/minusSignWithCommaAbove": "\u2A29", + "/minusSignWithDotBelow": "\u2A2A", + "/minusSignWithFallingDots": "\u2A2B", + "/minusSignWithRisingDots": "\u2A2C", + "/plusSignInLeftHalfCircle": "\u2A2D", + "/plusSignInRightHalfCircle": "\u2A2E", + "/vectorOrCrossProduct": "\u2A2F", + "/multiplicationSignWithDotAbove": "\u2A30", + "/multiplicationSignWithUnderbar": "\u2A31", + "/semidirectProductWithBottomClosed": "\u2A32", + "/smashProduct": "\u2A33", + "/multiplicationSignInLeftHalfCircle": "\u2A34", + "/multiplicationSignInRightHalfCircle": "\u2A35", + "/circledMultiplicationSignWithCircumflexAccent": "\u2A36", + "/multiplicationSignInDoubleCircle": "\u2A37", + "/circledDivisionSign": "\u2A38", + "/plusSignInTriangle": "\u2A39", + "/minusSignInTriangle": "\u2A3A", + "/multiplicationSignInTriangle": "\u2A3B", + "/interiorProduct": "\u2A3C", + "/righthandInteriorProduct": "\u2A3D", + "/zNotationRelationalComposition": "\u2A3E", + "/amalgamationOrCoproduct": "\u2A3F", + "/intersectionWithDot": "\u2A40", + "/unionWithMinusSign": "\u2A41", + "/unionWithOverbar": "\u2A42", + "/intersectionWithOverbar": "\u2A43", + "/intersectionWithLogicalAnd": "\u2A44", + "/unionWithLogicalOr": "\u2A45", + "/unionAboveIntersection": "\u2A46", + "/intersectionAboveUnion": "\u2A47", + "/unionAboveBarAboveIntersection": "\u2A48", + "/intersectionAboveBarAboveUnion": "\u2A49", + "/unionBesideAndJoinedWithUnion": "\u2A4A", + "/intersectionBesideAndJoinedWithIntersection": "\u2A4B", + "/closedUnionWithSerifs": "\u2A4C", + "/closedIntersectionWithSerifs": "\u2A4D", + "/doubleSquareIntersection": "\u2A4E", + "/doubleSquareUnion": "\u2A4F", + "/closedUnionWithSerifsAndSmashProduct": "\u2A50", + "/logicalAndWithDotAbove": "\u2A51", + "/logicalOrWithDotAbove": "\u2A52", + "/doubleLogicalAnd": "\u2A53", + "/doubleLogicalOr": "\u2A54", + "/twoIntersectingLogicalAnd": "\u2A55", + "/twoIntersectingLogicalOr": "\u2A56", + "/slopingLargeOr": "\u2A57", + "/slopingLargeAnd": "\u2A58", + "/logicalOrOverlappingLogicalAnd": "\u2A59", + "/logicalAndWithMiddleStem": "\u2A5A", + "/logicalOrWithMiddleStem": "\u2A5B", + "/logicalAndWithHorizontalDash": "\u2A5C", + "/logicalOrWithHorizontalDash": "\u2A5D", + "/logicalAndWithDoubleOverbar": "\u2A5E", + "/logicalAndWithUnderbar": "\u2A5F", + "/logicalAndWithDoubleUnderbar": "\u2A60", + "/smallVeeWithUnderbar": "\u2A61", + "/logicalOrWithDoubleOverbar": "\u2A62", + "/logicalOrWithDoubleUnderbar": "\u2A63", + "/zNotationDomainAntirestriction": "\u2A64", + "/zNotationRangeAntirestriction": "\u2A65", + "/equalsSignWithDotBelow": "\u2A66", + "/identicalWithDotAbove": "\u2A67", + "/tripleHorizontalBarWithDoubleVerticalStroke": "\u2A68", + "/tripleHorizontalBarWithTripleVerticalStroke": "\u2A69", + "/tildeOperatorWithDotAbove": "\u2A6A", + "/tildeOperatorWithRisingDots": "\u2A6B", + "/similarMinusSimilar": "\u2A6C", + "/congruentWithDotAbove": "\u2A6D", + "/equalsWithAsterisk": "\u2A6E", + "/almostEqualToWithCircumflexAccent": "\u2A6F", + "/approximatelyEqualOrEqualTo": "\u2A70", + "/equalsSignAbovePlusSign": "\u2A71", + "/plusSignAboveEqualsSign": "\u2A72", + "/equalsSignAboveTildeOperator": "\u2A73", + "/doubleColonEqual": "\u2A74", + "/twoConsecutiveEqualsSigns": "\u2A75", + "/threeConsecutiveEqualsSigns": "\u2A76", + "/equalsSignWithTwoDotsAboveAndTwoDotsBelow": "\u2A77", + "/equivalentWithFourDotsAbove": "\u2A78", + "/lessthanWithCircleInside": "\u2A79", + "/greaterthanWithCircleInside": "\u2A7A", + "/lessthanWithQuestionMarkAbove": "\u2A7B", + "/greaterthanWithQuestionMarkAbove": "\u2A7C", + "/lessthanOrSlantedEqualTo": "\u2A7D", + "/greaterthanOrSlantedEqualTo": "\u2A7E", + "/lessthanOrSlantedEqualToWithDotInside": "\u2A7F", + "/greaterthanOrSlantedEqualToWithDotInside": "\u2A80", + "/lessthanOrSlantedEqualToWithDotAbove": "\u2A81", + "/greaterthanOrSlantedEqualToWithDotAbove": "\u2A82", + "/lessthanOrSlantedEqualToWithDotAboveRight": "\u2A83", + "/greaterthanOrSlantedEqualToWithDotAboveLeft": "\u2A84", + "/lessthanOrApproximate": "\u2A85", + "/greaterthanOrApproximate": "\u2A86", + "/lessthanAndSinglelineNotEqualTo": "\u2A87", + "/greaterthanAndSinglelineNotEqualTo": "\u2A88", + "/lessthanAndNotApproximate": "\u2A89", + "/greaterthanAndNotApproximate": "\u2A8A", + "/lessthanAboveDoublelineEqualAboveGreaterthan": "\u2A8B", + "/greaterthanAboveDoublelineEqualAboveLessthan": "\u2A8C", + "/lessthanAboveSimilarOrEqual": "\u2A8D", + "/greaterthanAboveSimilarOrEqual": "\u2A8E", + "/lessthanAboveSimilarAboveGreaterthan": "\u2A8F", + "/greaterthanAboveSimilarAboveLessthan": "\u2A90", + "/lessthanAboveGreaterthanAboveDoublelineEqual": "\u2A91", + "/greaterthanAboveLessthanAboveDoublelineEqual": "\u2A92", + "/lessthanAboveSlantedEqualAboveGreaterthanAboveSlantedEqual": "\u2A93", + "/greaterthanAboveSlantedEqualAboveLessthanAboveSlantedEqual": "\u2A94", + "/slantedEqualToOrLessthan": "\u2A95", + "/slantedEqualToOrGreaterthan": "\u2A96", + "/slantedEqualToOrLessthanWithDotInside": "\u2A97", + "/slantedEqualToOrGreaterthanWithDotInside": "\u2A98", + "/doublelineEqualToOrLessthan": "\u2A99", + "/doublelineEqualToOrGreaterthan": "\u2A9A", + "/doublelineSlantedEqualToOrLessthan": "\u2A9B", + "/doublelineSlantedEqualToOrGreaterthan": "\u2A9C", + "/similarOrLessthan": "\u2A9D", + "/similarOrGreaterthan": "\u2A9E", + "/similarAboveLessthanAboveEqualsSign": "\u2A9F", + "/similarAboveGreaterthanAboveEqualsSign": "\u2AA0", + "/doubleNestedLessthan": "\u2AA1", + "/doubleNestedGreaterthan": "\u2AA2", + "/doubleNestedLessthanWithUnderbar": "\u2AA3", + "/greaterthanOverlappingLessthan": "\u2AA4", + "/greaterthanBesideLessthan": "\u2AA5", + "/lessthanClosedByCurve": "\u2AA6", + "/greaterthanClosedByCurve": "\u2AA7", + "/lessthanClosedByCurveAboveSlantedEqual": "\u2AA8", + "/greaterthanClosedByCurveAboveSlantedEqual": "\u2AA9", + "/smallerThan": "\u2AAA", + "/largerThan": "\u2AAB", + "/smallerThanOrEqualTo": "\u2AAC", + "/largerThanOrEqualTo": "\u2AAD", + "/equalsSignWithBumpyAbove": "\u2AAE", + "/precedesAboveSinglelineEqualsSign": "\u2AAF", + "/succeedsAboveSinglelineEqualsSign": "\u2AB0", + "/precedesAboveSinglelineNotEqualTo": "\u2AB1", + "/succeedsAboveSinglelineNotEqualTo": "\u2AB2", + "/precedesAboveEqualsSign": "\u2AB3", + "/succeedsAboveEqualsSign": "\u2AB4", + "/precedesAboveNotEqualTo": "\u2AB5", + "/succeedsAboveNotEqualTo": "\u2AB6", + "/precedesAboveAlmostEqualTo": "\u2AB7", + "/succeedsAboveAlmostEqualTo": "\u2AB8", + "/precedesAboveNotAlmostEqualTo": "\u2AB9", + "/succeedsAboveNotAlmostEqualTo": "\u2ABA", + "/doublePrecedes": "\u2ABB", + "/doubleSucceeds": "\u2ABC", + "/subsetWithDot": "\u2ABD", + "/supersetWithDot": "\u2ABE", + "/subsetWithPlusSignBelow": "\u2ABF", + "/supersetWithPlusSignBelow": "\u2AC0", + "/subsetWithMultiplicationSignBelow": "\u2AC1", + "/supersetWithMultiplicationSignBelow": "\u2AC2", + "/subsetOfOrEqualToWithDotAbove": "\u2AC3", + "/supersetOfOrEqualToWithDotAbove": "\u2AC4", + "/subsetOfAboveEqualsSign": "\u2AC5", + "/supersetOfAboveEqualsSign": "\u2AC6", + "/subsetOfAboveTildeOperator": "\u2AC7", + "/supersetOfAboveTildeOperator": "\u2AC8", + "/subsetOfAboveAlmostEqualTo": "\u2AC9", + "/supersetOfAboveAlmostEqualTo": "\u2ACA", + "/subsetOfAboveNotEqualTo": "\u2ACB", + "/supersetOfAboveNotEqualTo": "\u2ACC", + "/squareLeftOpenBoxOperator": "\u2ACD", + "/squareRightOpenBoxOperator": "\u2ACE", + "/closedSubset": "\u2ACF", + "/closedSuperset": "\u2AD0", + "/closedSubsetOrEqualTo": "\u2AD1", + "/closedSupersetOrEqualTo": "\u2AD2", + "/subsetAboveSuperset": "\u2AD3", + "/supersetAboveSubset": "\u2AD4", + "/subsetAboveSubset": "\u2AD5", + "/supersetAboveSuperset": "\u2AD6", + "/supersetBesideSubset": "\u2AD7", + "/supersetBesideAndJoinedByDashWithSubset": "\u2AD8", + "/elementOfOpeningDownwards": "\u2AD9", + "/pitchforkWithTeeTop": "\u2ADA", + "/transversalIntersection": "\u2ADB", + "/forking": "\u2ADC", + "/nonforking": "\u2ADD", + "/shortLeftTack": "\u2ADE", + "/shortDownTack": "\u2ADF", + "/shortUpTack": "\u2AE0", + "/perpendicularWithS": "\u2AE1", + "/verticalBarTripleRightTurnstile": "\u2AE2", + "/doubleVerticalBarLeftTurnstile": "\u2AE3", + "/verticalBarDoubleLeftTurnstile": "\u2AE4", + "/doubleVerticalBarDoubleLeftTurnstile": "\u2AE5", + "/longDashFromLeftMemberOfDoubleVertical": "\u2AE6", + "/shortDownTackWithOverbar": "\u2AE7", + "/shortUpTackWithUnderbar": "\u2AE8", + "/shortUpTackAboveShortDownTack": "\u2AE9", + "/doubleDownTack": "\u2AEA", + "/doubleUpTack": "\u2AEB", + "/doubleStrokeNotSign": "\u2AEC", + "/reversedDoubleStrokeNotSign": "\u2AED", + "/doesNotDivideWithReversedNegationSlash": "\u2AEE", + "/verticalLineWithCircleAbove": "\u2AEF", + "/verticalLineWithCircleBelow": "\u2AF0", + "/downTackWithCircleBelow": "\u2AF1", + "/parallelWithHorizontalStroke": "\u2AF2", + "/parallelWithTildeOperator": "\u2AF3", + "/tripleVerticalBarBinaryRelation": "\u2AF4", + "/tripleVerticalBarWithHorizontalStroke": "\u2AF5", + "/tripleColonOperator": "\u2AF6", + "/tripleNestedLessthan": "\u2AF7", + "/tripleNestedGreaterthan": "\u2AF8", + "/doublelineSlantedLessthanOrEqualTo": "\u2AF9", + "/doublelineSlantedGreaterthanOrEqualTo": "\u2AFA", + "/tripleSolidusBinaryRelation": "\u2AFB", + "/largeTripleVerticalBarOperator": "\u2AFC", + "/doubleSolidusOperator": "\u2AFD", + "/whiteVerticalBar": "\u2AFE", + "/naryWhiteVerticalBar": "\u2AFF", + "/northEastWhiteArrow": "\u2B00", + "/northWestWhiteArrow": "\u2B01", + "/southEastWhiteArrow": "\u2B02", + "/southWestWhiteArrow": "\u2B03", + "/leftRightWhiteArrow": "\u2B04", + "/leftwardsBlackArrow": "\u2B05", + "/upwardsBlackArrow": "\u2B06", + "/downwardsBlackArrow": "\u2B07", + "/northEastBlackArrow": "\u2B08", + "/northWestBlackArrow": "\u2B09", + "/southEastBlackArrow": "\u2B0A", + "/southWestBlackArrow": "\u2B0B", + "/leftRightBlackArrow": "\u2B0C", + "/upDownBlackArrow": "\u2B0D", + "/rightwardsArrowWithTipDownwards": "\u2B0E", + "/rightwardsArrowWithTipUpwards": "\u2B0F", + "/leftwardsArrowWithTipDownwards": "\u2B10", + "/leftwardsArrowWithTipUpwards": "\u2B11", + "/squareWithTopHalfBlack": "\u2B12", + "/squareWithBottomHalfBlack": "\u2B13", + "/squareWithUpperRightDiagonalHalfBlack": "\u2B14", + "/squareWithLowerLeftDiagonalHalfBlack": "\u2B15", + "/diamondWithLeftHalfBlack": "\u2B16", + "/diamondWithRightHalfBlack": "\u2B17", + "/diamondWithTopHalfBlack": "\u2B18", + "/diamondWithBottomHalfBlack": "\u2B19", + "/dottedSquare": "\u2B1A", + "/blackLargeSquare": "\u2B1B", + "/whiteLargeSquare": "\u2B1C", + "/blackVerySmallSquare": "\u2B1D", + "/whiteVerySmallSquare": "\u2B1E", + "/blackPentagon": "\u2B1F", + "/whitePentagon": "\u2B20", + "/whiteHexagon": "\u2B21", + "/blackHexagon": "\u2B22", + "/horizontalBlackHexagon": "\u2B23", + "/blackLargeCircle": "\u2B24", + "/blackMediumDiamond": "\u2B25", + "/whiteMediumDiamond": "\u2B26", + "/blackMediumLozenge": "\u2B27", + "/whiteMediumLozenge": "\u2B28", + "/blackSmallDiamond": "\u2B29", + "/blackSmallLozenge": "\u2B2A", + "/whiteSmallLozenge": "\u2B2B", + "/blackHorizontalEllipse": "\u2B2C", + "/whiteHorizontalEllipse": "\u2B2D", + "/blackVerticalEllipse": "\u2B2E", + "/whiteVerticalEllipse": "\u2B2F", + "/leftArrowWithSmallCircle": "\u2B30", + "/threeLeftwardsArrows": "\u2B31", + "/leftArrowWithCircledPlus": "\u2B32", + "/longLeftwardsSquiggleArrow": "\u2B33", + "/leftwardsTwoheadedArrowWithVerticalStroke": "\u2B34", + "/leftwardsTwoheadedArrowWithDoubleVerticalStroke": "\u2B35", + "/leftwardsTwoheadedArrowFromBar": "\u2B36", + "/leftwardsTwoheadedTripleDashArrow": "\u2B37", + "/leftwardsArrowWithDottedStem": "\u2B38", + "/leftwardsArrowWithTailWithVerticalStroke": "\u2B39", + "/leftwardsArrowWithTailWithDoubleVerticalStroke": "\u2B3A", + "/leftwardsTwoheadedArrowWithTail": "\u2B3B", + "/leftwardsTwoheadedArrowWithTailWithVerticalStroke": "\u2B3C", + "/leftwardsTwoheadedArrowWithTailWithDoubleVerticalStroke": "\u2B3D", + "/leftwardsArrowThroughX": "\u2B3E", + "/waveArrowPointingDirectlyLeft": "\u2B3F", + "/equalsSignAboveLeftwardsArrow": "\u2B40", + "/reverseTildeOperatorAboveLeftwardsArrow": "\u2B41", + "/leftwardsArrowAboveReverseAlmostEqualTo": "\u2B42", + "/rightwardsArrowThroughGreaterthan": "\u2B43", + "/rightwardsArrowThroughSuperset": "\u2B44", + "/leftwardsQuadrupleArrow": "\u2B45", + "/rightwardsQuadrupleArrow": "\u2B46", + "/reverseTildeOperatorAboveRightwardsArrow": "\u2B47", + "/rightwardsArrowAboveReverseAlmostEqualTo": "\u2B48", + "/tildeOperatorAboveLeftwardsArrow": "\u2B49", + "/leftwardsArrowAboveAlmostEqualTo": "\u2B4A", + "/leftwardsArrowAboveReverseTildeOperator": "\u2B4B", + "/rightwardsArrowAboveReverseTildeOperator": "\u2B4C", + "/downwardsTriangleheadedZigzagArrow": "\u2B4D", + "/shortSlantedNorthArrow": "\u2B4E", + "/shortBackslantedSouthArrow": "\u2B4F", + "/whiteMediumStar": "\u2B50", + "/blackSmallStar": "\u2B51", + "/whiteSmallStar": "\u2B52", + "/blackRightpointingPentagon": "\u2B53", + "/whiteRightpointingPentagon": "\u2B54", + "/heavyLargeCircle": "\u2B55", + "/heavyOvalWithOvalInside": "\u2B56", + "/heavyCircleWithCircleInside": "\u2B57", + "/heavyCircle": "\u2B58", + "/heavyCircledSaltire": "\u2B59", + "/slantedNorthArrowWithHookedHead": "\u2B5A", + "/backslantedSouthArrowWithHookedTail": "\u2B5B", + "/slantedNorthArrowWithHorizontalTail": "\u2B5C", + "/backslantedSouthArrowWithHorizontalTail": "\u2B5D", + "/bentArrowPointingDownwardsThenNorthEast": "\u2B5E", + "/shortBentArrowPointingDownwardsThenNorthEast": "\u2B5F", + "/leftwardsTriangleheadedArrow": "\u2B60", + "/upwardsTriangleheadedArrow": "\u2B61", + "/rightwardsTriangleheadedArrow": "\u2B62", + "/downwardsTriangleheadedArrow": "\u2B63", + "/leftRightTriangleheadedArrow": "\u2B64", + "/upDownTriangleheadedArrow": "\u2B65", + "/northWestTriangleheadedArrow": "\u2B66", + "/northEastTriangleheadedArrow": "\u2B67", + "/southEastTriangleheadedArrow": "\u2B68", + "/southWestTriangleheadedArrow": "\u2B69", + "/leftwardsTriangleheadedDashedArrow": "\u2B6A", + "/upwardsTriangleheadedDashedArrow": "\u2B6B", + "/rightwardsTriangleheadedDashedArrow": "\u2B6C", + "/downwardsTriangleheadedDashedArrow": "\u2B6D", + "/clockwiseTriangleheadedOpenCircleArrow": "\u2B6E", + "/anticlockwiseTriangleheadedOpenCircleArrow": "\u2B6F", + "/leftwardsTriangleheadedArrowToBar": "\u2B70", + "/upwardsTriangleheadedArrowToBar": "\u2B71", + "/rightwardsTriangleheadedArrowToBar": "\u2B72", + "/downwardsTriangleheadedArrowToBar": "\u2B73", + "/northWestTriangleheadedArrowToBar": "\u2B76", + "/northEastTriangleheadedArrowToBar": "\u2B77", + "/southEastTriangleheadedArrowToBar": "\u2B78", + "/southWestTriangleheadedArrowToBar": "\u2B79", + "/leftwardsTriangleheadedArrowWithDoubleHorizontalStroke": "\u2B7A", + "/upwardsTriangleheadedArrowWithDoubleHorizontalStroke": "\u2B7B", + "/rightwardsTriangleheadedArrowWithDoubleHorizontalStroke": "\u2B7C", + "/downwardsTriangleheadedArrowWithDoubleHorizontalStroke": "\u2B7D", + "/horizontalTabKey": "\u2B7E", + "/verticalTabKey": "\u2B7F", + "/leftwardsTriangleheadedArrowOverRightwardsTriangleheadedArrow": "\u2B80", + "/upwardsTriangleheadedArrowLeftwardsOfDownwardsTriangleheadedArrow": "\u2B81", + "/rightwardsTriangleheadedArrowOverLeftwardsTriangleheadedArrow": "\u2B82", + "/downwardsTriangleheadedArrowLeftwardsOfUpwardsTriangleheadedArrow": "\u2B83", + "/leftwardsTriangleheadedPairedArrows": "\u2B84", + "/upwardsTriangleheadedPairedArrows": "\u2B85", + "/rightwardsTriangleheadedPairedArrows": "\u2B86", + "/downwardsTriangleheadedPairedArrows": "\u2B87", + "/leftwardsBlackCircledWhiteArrow": "\u2B88", + "/upwardsBlackCircledWhiteArrow": "\u2B89", + "/rightwardsBlackCircledWhiteArrow": "\u2B8A", + "/downwardsBlackCircledWhiteArrow": "\u2B8B", + "/anticlockwiseTriangleheadedRightUshapedArrow": "\u2B8C", + "/anticlockwiseTriangleheadedBottomUshapedArrow": "\u2B8D", + "/anticlockwiseTriangleheadedLeftUshapedArrow": "\u2B8E", + "/anticlockwiseTriangleheadedTopUshapedArrow": "\u2B8F", + "/returnLeft": "\u2B90", + "/returnRight": "\u2B91", + "/newlineLeft": "\u2B92", + "/newlineRight": "\u2B93", + "/fourCornerArrowsCirclingAnticlockwise": "\u2B94", + "/rightwardsBlackArrow": "\u2B95", + "/symbolForTypeAElectronics": "\u2B97", + "/threedToplightedLeftwardsEquilateralArrowhead": "\u2B98", + "/threedRightlightedUpwardsEquilateralArrowhead": "\u2B99", + "/threedToplightedRightwardsEquilateralArrowhead": "\u2B9A", + "/threedLeftlightedDownwardsEquilateralArrowhead": "\u2B9B", + "/blackLeftwardsEquilateralArrowhead": "\u2B9C", + "/blackUpwardsEquilateralArrowhead": "\u2B9D", + "/blackRightwardsEquilateralArrowhead": "\u2B9E", + "/blackDownwardsEquilateralArrowhead": "\u2B9F", + "/downwardsTriangleheadedArrowWithLongTipLeftwards": "\u2BA0", + "/downwardsTriangleheadedArrowWithLongTipRightwards": "\u2BA1", + "/upwardsTriangleheadedArrowWithLongTipLeftwards": "\u2BA2", + "/upwardsTriangleheadedArrowWithLongTipRightwards": "\u2BA3", + "/leftwardsTriangleheadedArrowWithLongTipUpwards": "\u2BA4", + "/rightwardsTriangleheadedArrowWithLongTipUpwards": "\u2BA5", + "/leftwardsTriangleheadedArrowWithLongTipDownwards": "\u2BA6", + "/rightwardsTriangleheadedArrowWithLongTipDownwards": "\u2BA7", + "/blackCurvedDownwardsAndLeftwardsArrow": "\u2BA8", + "/blackCurvedDownwardsAndRightwardsArrow": "\u2BA9", + "/blackCurvedUpwardsAndLeftwardsArrow": "\u2BAA", + "/blackCurvedUpwardsAndRightwardsArrow": "\u2BAB", + "/blackCurvedLeftwardsAndUpwardsArrow": "\u2BAC", + "/blackCurvedRightwardsAndUpwardsArrow": "\u2BAD", + "/blackCurvedLeftwardsAndDownwardsArrow": "\u2BAE", + "/blackCurvedRightwardsAndDownwardsArrow": "\u2BAF", + "/ribbonArrowDownLeft": "\u2BB0", + "/ribbonArrowDownRight": "\u2BB1", + "/ribbonArrowUpLeft": "\u2BB2", + "/ribbonArrowUpRight": "\u2BB3", + "/ribbonArrowLeftUp": "\u2BB4", + "/ribbonArrowRightUp": "\u2BB5", + "/ribbonArrowLeftDown": "\u2BB6", + "/ribbonArrowRightDown": "\u2BB7", + "/upwardsWhiteArrowFromBarWithHorizontalBar": "\u2BB8", + "/upArrowheadInARectangleBox": "\u2BB9", + "/overlappingWhiteSquares": "\u2BBA", + "/overlappingWhiteAndBlackSquares": "\u2BBB", + "/overlappingBlackSquares": "\u2BBC", + "/ballotBoxWithLightX": "\u2BBD", + "/circledX": "\u2BBE", + "/circledBoldX": "\u2BBF", + "/blackSquareCentred": "\u2BC0", + "/blackDiamondCentred": "\u2BC1", + "/turnedBlackPentagon": "\u2BC2", + "/horizontalBlackOctagon": "\u2BC3", + "/blackOctagon": "\u2BC4", + "/blackMediumUppointingTriangleCentred": "\u2BC5", + "/blackMediumDownpointingTriangleCentred": "\u2BC6", + "/blackMediumLeftpointingTriangleCentred": "\u2BC7", + "/blackMediumRightpointingTriangleCentred": "\u2BC8", + "/neptuneFormTwo": "\u2BC9", + "/topHalfBlackCircle": "\u2BCA", + "/bottomHalfBlackCircle": "\u2BCB", + "/lightFourPointedBlackCusp": "\u2BCC", + "/rotatedLightFourPointedBlackCusp": "\u2BCD", + "/whiteFourPointedCusp": "\u2BCE", + "/rotatedWhiteFourPointedCusp": "\u2BCF", + "/squarePositionIndicator": "\u2BD0", + "/uncertaintySign": "\u2BD1", + "/groupMark": "\u2BD2", + "/plutoFormTwo": "\u2BD3", + "/plutoFormThree": "\u2BD4", + "/plutoFormFour": "\u2BD5", + "/plutoFormFive": "\u2BD6", + "/transpluto": "\u2BD7", + "/proserpina": "\u2BD8", + "/astraea": "\u2BD9", + "/hygiea": "\u2BDA", + "/pholus": "\u2BDB", + "/nessus": "\u2BDC", + "/whiteMoonSelena": "\u2BDD", + "/blackDiamondOnCross": "\u2BDE", + "/trueLightMoonArta": "\u2BDF", + "/cupido": "\u2BE0", + "/hades": "\u2BE1", + "/zeus": "\u2BE2", + "/kronos": "\u2BE3", + "/apollon": "\u2BE4", + "/admetos": "\u2BE5", + "/vulcanus": "\u2BE6", + "/poseidon": "\u2BE7", + "/leftHalfBlackStar": "\u2BE8", + "/rightHalfBlackStar": "\u2BE9", + "/starWithLeftHalfBlack": "\u2BEA", + "/starWithRightHalfBlack": "\u2BEB", + "/leftwardsTwoheadedArrowWithTriangleArrowheads": "\u2BEC", + "/upwardsTwoheadedArrowWithTriangleArrowheads": "\u2BED", + "/rightwardsTwoheadedArrowWithTriangleArrowheads": "\u2BEE", + "/downwardsTwoheadedArrowWithTriangleArrowheads": "\u2BEF", + "/erisFormOne": "\u2BF0", + "/erisFormTwo": "\u2BF1", + "/sedna": "\u2BF2", + "/russianAstrologicalSymbolVigintile": "\u2BF3", + "/russianAstrologicalSymbolNovile": "\u2BF4", + "/russianAstrologicalSymbolQuintile": "\u2BF5", + "/russianAstrologicalSymbolBinovile": "\u2BF6", + "/russianAstrologicalSymbolSentagon": "\u2BF7", + "/russianAstrologicalSymbolTredecile": "\u2BF8", + "/equalsSignWithInfinityBelow": "\u2BF9", + "/unitedSymbol": "\u2BFA", + "/separatedSymbol": "\u2BFB", + "/doubledSymbol": "\u2BFC", + "/passedSymbol": "\u2BFD", + "/reversedRightAngle": "\u2BFE", + "/hellschreiberPauseSymbol": "\u2BFF", + "/Azu": "\u2C00", + "/Buky": "\u2C01", + "/Vede": "\u2C02", + "/Glagoli": "\u2C03", + "/Dobro": "\u2C04", + "/Yestu": "\u2C05", + "/Zhivete": "\u2C06", + "/Dzelo": "\u2C07", + "/Zemlja": "\u2C08", + "/Izhe": "\u2C09", + "/Initializhe": "\u2C0A", + "/glag:I": "\u2C0B", + "/Djervi": "\u2C0C", + "/Kako": "\u2C0D", + "/Ljudije": "\u2C0E", + "/Myslite": "\u2C0F", + "/Nashi": "\u2C10", + "/Onu": "\u2C11", + "/Pokoji": "\u2C12", + "/Ritsi": "\u2C13", + "/Slovo": "\u2C14", + "/Tvrido": "\u2C15", + "/Uku": "\u2C16", + "/Fritu": "\u2C17", + "/Heru": "\u2C18", + "/Otu": "\u2C19", + "/Pe": "\u2C1A", + "/Shta": "\u2C1B", + "/Tsi": "\u2C1C", + "/Chrivi": "\u2C1D", + "/Sha": "\u2C1E", + "/Yeru": "\u2C1F", + "/Yeri": "\u2C20", + "/Yati": "\u2C21", + "/Spideryha": "\u2C22", + "/Yu": "\u2C23", + "/Smallyus": "\u2C24", + "/Smallyuswithtail": "\u2C25", + "/Yo": "\u2C26", + "/Iotatedsmallyus": "\u2C27", + "/Bigyus": "\u2C28", + "/Iotatedbigyus": "\u2C29", + "/Fita": "\u2C2A", + "/Izhitsa": "\u2C2B", + "/Shtapic": "\u2C2C", + "/Trokutastia": "\u2C2D", + "/Latinatemyslite": "\u2C2E", + "/Caudatechrivi": "\u2C2F", + "/azu": "\u2C30", + "/buky": "\u2C31", + "/vede": "\u2C32", + "/glagoli": "\u2C33", + "/dobro": "\u2C34", + "/yestu": "\u2C35", + "/zhivete": "\u2C36", + "/dzelo": "\u2C37", + "/zemlja": "\u2C38", + "/izhe": "\u2C39", + "/initializhe": "\u2C3A", + "/glag:i": "\u2C3B", + "/djervi": "\u2C3C", + "/kako": "\u2C3D", + "/ljudije": "\u2C3E", + "/myslite": "\u2C3F", + "/nashi": "\u2C40", + "/onu": "\u2C41", + "/pokoji": "\u2C42", + "/ritsi": "\u2C43", + "/slovo": "\u2C44", + "/tvrido": "\u2C45", + "/uku": "\u2C46", + "/fritu": "\u2C47", + "/heru": "\u2C48", + "/otu": "\u2C49", + "/shta": "\u2C4B", + "/tsi": "\u2C4C", + "/chrivi": "\u2C4D", + "/sha": "\u2C4E", + "/yeru": "\u2C4F", + "/yeri": "\u2C50", + "/yati": "\u2C51", + "/spideryha": "\u2C52", + "/glag:yu": "\u2C53", + "/smallyus": "\u2C54", + "/smallyuswithtail": "\u2C55", + "/yo": "\u2C56", + "/iotatedsmallyus": "\u2C57", + "/bigyus": "\u2C58", + "/iotatedbigyus": "\u2C59", + "/fita": "\u2C5A", + "/izhitsa": "\u2C5B", + "/shtapic": "\u2C5C", + "/trokutastia": "\u2C5D", + "/latinatemyslite": "\u2C5E", + "/caudatechrivi": "\u2C5F", + "/Ldblbar": "\u2C60", + "/ldblbar": "\u2C61", + "/Lmiddletilde": "\u2C62", + "/Pstroke": "\u2C63", + "/Rtail": "\u2C64", + "/astroke": "\u2C65", + "/tstroke": "\u2C66", + "/Hdescender": "\u2C67", + "/hdescender": "\u2C68", + "/Kdescender": "\u2C69", + "/kdescender": "\u2C6A", + "/Zdescender": "\u2C6B", + "/zdescender": "\u2C6C", + "/lt:Alpha": "\u2C6D", + "/Mhook": "\u2C6E", + "/Aturned": "\u2C6F", + "/lt:Alphaturned": "\u2C70", + "/vrighthook": "\u2C71", + "/Whook": "\u2C72", + "/whook": "\u2C73", + "/vcurl": "\u2C74", + "/Hhalf": "\u2C75", + "/hhalf": "\u2C76", + "/phitailless": "\u2C77", + "/enotch": "\u2C78", + "/rtailturned": "\u2C79", + "/olowringinside": "\u2C7A", + "/Esmallturned": "\u2C7B", + "/j.inferior": "\u2C7C", + "/Vmod": "\u2C7D", + "/Sswashtail": "\u2C7E", + "/Zswashtail": "\u2C7F", + "/anGeok": "\u2D00", + "/banGeok": "\u2D01", + "/ganGeok": "\u2D02", + "/donGeok": "\u2D03", + "/enGeok": "\u2D04", + "/vinGeok": "\u2D05", + "/zenGeok": "\u2D06", + "/tanGeok": "\u2D07", + "/inGeok": "\u2D08", + "/kanGeok": "\u2D09", + "/lasGeok": "\u2D0A", + "/manGeok": "\u2D0B", + "/narGeok": "\u2D0C", + "/onGeok": "\u2D0D", + "/parGeok": "\u2D0E", + "/zharGeok": "\u2D0F", + "/raeGeok": "\u2D10", + "/sanGeok": "\u2D11", + "/tarGeok": "\u2D12", + "/unGeok": "\u2D13", + "/pharGeok": "\u2D14", + "/kharGeok": "\u2D15", + "/ghanGeok": "\u2D16", + "/qarGeok": "\u2D17", + "/shinGeok": "\u2D18", + "/chinGeok": "\u2D19", + "/canGeok": "\u2D1A", + "/jilGeok": "\u2D1B", + "/cilGeok": "\u2D1C", + "/charGeok": "\u2D1D", + "/xanGeok": "\u2D1E", + "/jhanGeok": "\u2D1F", + "/haeGeok": "\u2D20", + "/heGeok": "\u2D21", + "/hieGeok": "\u2D22", + "/weGeok": "\u2D23", + "/harGeok": "\u2D24", + "/hoeGeok": "\u2D25", + "/ynGeok": "\u2D27", + "/aenGeok": "\u2D2D", + "/ethi:loa": "\u2D80", + "/ethi:moa": "\u2D81", + "/ethi:roa": "\u2D82", + "/ethi:soa": "\u2D83", + "/ethi:shoa": "\u2D84", + "/ethi:boa": "\u2D85", + "/ethi:toa": "\u2D86", + "/ethi:coa": "\u2D87", + "/ethi:noa": "\u2D88", + "/ethi:nyoa": "\u2D89", + "/ethi:oaglottal": "\u2D8A", + "/ethi:zoa": "\u2D8B", + "/ethi:doa": "\u2D8C", + "/ethi:ddoa": "\u2D8D", + "/ethi:joa": "\u2D8E", + "/ethi:thoa": "\u2D8F", + "/ethi:choa": "\u2D90", + "/ethi:phoa": "\u2D91", + "/ethi:poa": "\u2D92", + "/ethi:ggwa": "\u2D93", + "/ethi:ggwi": "\u2D94", + "/ethi:ggwee": "\u2D95", + "/ethi:ggwe": "\u2D96", + "/ethi:ssa": "\u2DA0", + "/ethi:ssu": "\u2DA1", + "/ethi:ssi": "\u2DA2", + "/ethi:ssaa": "\u2DA3", + "/ethi:ssee": "\u2DA4", + "/ethi:sse": "\u2DA5", + "/ethi:sso": "\u2DA6", + "/ethi:cca": "\u2DA8", + "/ethi:ccu": "\u2DA9", + "/ethi:cci": "\u2DAA", + "/ethi:ccaa": "\u2DAB", + "/ethi:ccee": "\u2DAC", + "/ethi:cce": "\u2DAD", + "/ethi:cco": "\u2DAE", + "/ethi:zza": "\u2DB0", + "/ethi:zzu": "\u2DB1", + "/ethi:zzi": "\u2DB2", + "/ethi:zzaa": "\u2DB3", + "/ethi:zzee": "\u2DB4", + "/ethi:zze": "\u2DB5", + "/ethi:zzo": "\u2DB6", + "/ethi:ccha": "\u2DB8", + "/ethi:cchu": "\u2DB9", + "/ethi:cchi": "\u2DBA", + "/ethi:cchaa": "\u2DBB", + "/ethi:cchee": "\u2DBC", + "/ethi:cche": "\u2DBD", + "/ethi:ccho": "\u2DBE", + "/ethi:qya": "\u2DC0", + "/ethi:qyu": "\u2DC1", + "/ethi:qyi": "\u2DC2", + "/ethi:qyaa": "\u2DC3", + "/ethi:qyee": "\u2DC4", + "/ethi:qye": "\u2DC5", + "/ethi:qyo": "\u2DC6", + "/ethi:kya": "\u2DC8", + "/ethi:kyu": "\u2DC9", + "/ethi:kyi": "\u2DCA", + "/ethi:kyaa": "\u2DCB", + "/ethi:kyee": "\u2DCC", + "/ethi:kye": "\u2DCD", + "/ethi:kyo": "\u2DCE", + "/ethi:xya": "\u2DD0", + "/ethi:xyu": "\u2DD1", + "/ethi:xyi": "\u2DD2", + "/ethi:xyaa": "\u2DD3", + "/ethi:xyee": "\u2DD4", + "/ethi:xye": "\u2DD5", + "/ethi:xyo": "\u2DD6", + "/ethi:gya": "\u2DD8", + "/ethi:gyu": "\u2DD9", + "/ethi:gyi": "\u2DDA", + "/ethi:gyaa": "\u2DDB", + "/ethi:gyee": "\u2DDC", + "/ethi:gye": "\u2DDD", + "/ethi:gyo": "\u2DDE", + "/anglemarkersubstitutionright": "\u2E00", + "/anglemarkerdottedsubstitutionright": "\u2E01", + "/bracketsubstitutionleft": "\u2E02", + "/bracketsubstitutionright": "\u2E03", + "/bracketdottedsubstitutionleft": "\u2E04", + "/bracketdottedsubstitutionright": "\u2E05", + "/markerraisedinterpolation": "\u2E06", + "/markerdottedraisedinterpolation": "\u2E07", + "/markerdottedtransposition": "\u2E08", + "/brackettranspositionleft": "\u2E09", + "/brackettranspositionright": "\u2E0A", + "/squareraised": "\u2E0B", + "/bracketraisedleft": "\u2E0C", + "/bracketraisedright": "\u2E0D", + "/coroniseditorial": "\u2E0E", + "/paragraphos": "\u2E0F", + "/paragraphosforked": "\u2E10", + "/paragraphosforkedreversed": "\u2E11", + "/hypodiastole": "\u2E12", + "/obelosdotted": "\u2E13", + "/ancoradown": "\u2E14", + "/ancoraup": "\u2E15", + "/angledottedright": "\u2E16", + "/hyphendbloblique": "\u2E17", + "/interrobanginverted": "\u2E18", + "/palmbranch": "\u2E19", + "/hyphendieresis": "\u2E1A", + "/tildering": "\u2E1B", + "/bracketparaphraselowleft": "\u2E1C", + "/bracketparaphraselowright": "\u2E1D", + "/tildedotaccent": "\u2E1E", + "/tildedotbelow": "\u2E1F", + "/barquillverticalleft": "\u2E20", + "/barquillverticalright": "\u2E21", + "/brackethalftopleft": "\u2E22", + "/brackethalftopright": "\u2E23", + "/brackethalfbottomleft": "\u2E24", + "/brackethalfbottomright": "\u2E25", + "/ubracketleft": "\u2E26", + "/ubracketright": "\u2E27", + "/parendblleft": "\u2E28", + "/parendblright": "\u2E29", + "/twodotsoveronedot": "\u2E2A", + "/onedotovertwodots": "\u2E2B", + "/dotsquarefour": "\u2E2C", + "/fivedot": "\u2E2D", + "/questionreversed": "\u2E2E", + "/tildevertical": "\u2E2F", + "/pointring": "\u2E30", + "/wordseparatormiddledot": "\u2E31", + "/turnedcomma": "\u2E32", + "/dotraised": "\u2E33", + "/commaraised": "\u2E34", + "/turnedsemicolon": "\u2E35", + "/daggerwithguardleft": "\u2E36", + "/daggerwithguardright": "\u2E37", + "/turneddagger": "\u2E38", + "/sectionsignhalftop": "\u2E39", + "/emdashdbl": "\u2E3A", + "/emdashtpl": "\u2E3B", + "/stenographicfullstop": "\u2E3C", + "/sixdotsvertical": "\u2E3D", + "/wigglylinevertical": "\u2E3E", + "/capitulum": "\u2E3F", + "/hyphendbl": "\u2E40", + "/commareversed": "\u2E41", + "/quotedbllowreversed": "\u2E42", + "/dashwithupturnleft": "\u2E43", + "/suspensiondbl": "\u2E44", + "/kavykainvertedlow": "\u2E45", + "/kavykawithkavykaaboveinvertedlow": "\u2E46", + "/kavykalow": "\u2E47", + "/kavykawithdotlow": "\u2E48", + "/stackedcommadbl": "\u2E49", + "/solidusdotted": "\u2E4A", + "/tripledagger": "\u2E4B", + "/medievalcomma": "\u2E4C", + "/paragraphus": "\u2E4D", + "/punctuselevatus": "\u2E4E", + "/cornishversedivider": "\u2E4F", + "/crosspattywithcrossbarright": "\u2E50", + "/crosspattywithcrossbarleft": "\u2E51", + "/tironiansigncapitalet": "\u2E52", + "/medievalexclamation": "\u2E53", + "/medievalquestion": "\u2E54", + "/squarebracketwithstrokeleft": "\u2E55", + "/squarebracketwithstrokeright": "\u2E56", + "/squarebracketwithstrokedblleft": "\u2E57", + "/squarebracketwithstrokedblright": "\u2E58", + "/parenhalftopleft": "\u2E59", + "/parenhalftopright": "\u2E5A", + "/parenhalfbottomleft": "\u2E5B", + "/parenhalfbottomright": "\u2E5C", + "/hyphenoblique": "\u2E5D", + "/whitesquarebracketleft": "\u301A", + "/whitesquarebracketright": "\u301B", + "/lowquotedblprime": "\u301F", + "/ideographicleveltonemark": "\u302A", + "/ideographicrisingtonemark": "\u302B", + "/ideographicdepartingtonemark": "\u302C", + "/ideographicenteringtonemark": "\u302D", + "/hangulsingledottonemark": "\u302E", + "/hanguldottonemarkdbl": "\u302F", + "/wavydash": "\u3030", + "/verticalkanarepeatmark": "\u3031", + "/verticalkanarepeatwithvoicedsoundmark": "\u3032", + "/verticalkanarepeatmarkupperhalf": "\u3033", + "/verticalkanarepeatwithvoicedsoundmarkupperhalf": "\u3034", + "/verticalkanarepeatmarklowerhalf": "\u3035", + "/ideographictelegraphlinefeedseparatorsymbol": "\u3037", + "/tenhangzhou": "\u3038", + "/thirtyhangzhou": "\u303A", + "/verticalideographiciterationmark": "\u303B", + "/masumark": "\u303C", + "/partalternationmark": "\u303D", + "/ideographicvariationindicator": "\u303E", + "/ideographichalffillspace": "\u303F", + "/hira:asmall": "\u3041", + "/hira:a": "\u3042", + "/hira:ismall": "\u3043", + "/hira:i": "\u3044", + "/hira:usmall": "\u3045", + "/hira:u": "\u3046", + "/hira:esmall": "\u3047", + "/hira:e": "\u3048", + "/hira:osmall": "\u3049", + "/hira:o": "\u304A", + "/hira:ka": "\u304B", + "/hira:ga": "\u304C", + "/hira:ki": "\u304D", + "/hira:gi": "\u304E", + "/hira:ku": "\u304F", + "/hira:gu": "\u3050", + "/hira:ke": "\u3051", + "/hira:ge": "\u3052", + "/hira:ko": "\u3053", + "/hira:go": "\u3054", + "/hira:sa": "\u3055", + "/hira:za": "\u3056", + "/hira:si": "\u3057", + "/hira:zi": "\u3058", + "/hira:su": "\u3059", + "/hira:zu": "\u305A", + "/hira:se": "\u305B", + "/hira:ze": "\u305C", + "/hira:so": "\u305D", + "/hira:zo": "\u305E", + "/hira:ta": "\u305F", + "/hira:da": "\u3060", + "/hira:ti": "\u3061", + "/hira:di": "\u3062", + "/hira:tusmall": "\u3063", + "/hira:tu": "\u3064", + "/hira:du": "\u3065", + "/hira:te": "\u3066", + "/hira:de": "\u3067", + "/hira:to": "\u3068", + "/hira:do": "\u3069", + "/hira:na": "\u306A", + "/hira:ni": "\u306B", + "/hira:nu": "\u306C", + "/hira:ne": "\u306D", + "/hira:no": "\u306E", + "/hira:ha": "\u306F", + "/hira:ba": "\u3070", + "/hira:pa": "\u3071", + "/hira:hi": "\u3072", + "/hira:bi": "\u3073", + "/hira:pi": "\u3074", + "/hira:hu": "\u3075", + "/hira:bu": "\u3076", + "/hira:pu": "\u3077", + "/hira:he": "\u3078", + "/hira:be": "\u3079", + "/hira:pe": "\u307A", + "/hira:ho": "\u307B", + "/hira:bo": "\u307C", + "/hira:po": "\u307D", + "/hira:ma": "\u307E", + "/hira:mi": "\u307F", + "/hira:mu": "\u3080", + "/hira:me": "\u3081", + "/hira:mo": "\u3082", + "/hira:yasmall": "\u3083", + "/hira:ya": "\u3084", + "/hira:yusmall": "\u3085", + "/hira:yu": "\u3086", + "/hira:yosmall": "\u3087", + "/hira:yo": "\u3088", + "/hira:ra": "\u3089", + "/hira:ri": "\u308A", + "/hira:ru": "\u308B", + "/hira:re": "\u308C", + "/hira:ro": "\u308D", + "/hira:wasmall": "\u308E", + "/hira:wa": "\u308F", + "/hira:wi": "\u3090", + "/hira:we": "\u3091", + "/hira:wo": "\u3092", + "/hira:n": "\u3093", + "/hira:vu": "\u3094", + "/hira:kasmall": "\u3095", + "/hira:kesmall": "\u3096", + "/hira:voicedmarkkanacmb": "\u3099", + "/hira:semivoicedmarkkanacmb": "\u309A", + "/hira:voicedmarkkana": "\u309B", + "/hira:semivoicedmarkkana": "\u309C", + "/hira:iterationhiragana": "\u309D", + "/hira:voicediterationhiragana": "\u309E", + "/hira:digraphyori": "\u309F", + "/kata:doublehyphenkana": "\u30A0", + "/kata:asmall": "\u30A1", + "/kata:a": "\u30A2", + "/kata:ismall": "\u30A3", + "/kata:i": "\u30A4", + "/kata:usmall": "\u30A5", + "/kata:u": "\u30A6", + "/kata:esmall": "\u30A7", + "/kata:e": "\u30A8", + "/kata:osmall": "\u30A9", + "/kata:o": "\u30AA", + "/kata:ka": "\u30AB", + "/kata:ga": "\u30AC", + "/kata:ki": "\u30AD", + "/kata:gi": "\u30AE", + "/kata:ku": "\u30AF", + "/kata:gu": "\u30B0", + "/kata:ke": "\u30B1", + "/kata:ge": "\u30B2", + "/kata:ko": "\u30B3", + "/kata:go": "\u30B4", + "/kata:sa": "\u30B5", + "/kata:za": "\u30B6", + "/kata:si": "\u30B7", + "/kata:zi": "\u30B8", + "/kata:su": "\u30B9", + "/kata:zu": "\u30BA", + "/kata:se": "\u30BB", + "/kata:ze": "\u30BC", + "/kata:so": "\u30BD", + "/kata:zo": "\u30BE", + "/kata:ta": "\u30BF", + "/kata:da": "\u30C0", + "/kata:ti": "\u30C1", + "/kata:di": "\u30C2", + "/kata:tusmall": "\u30C3", + "/kata:tu": "\u30C4", + "/kata:du": "\u30C5", + "/kata:te": "\u30C6", + "/kata:de": "\u30C7", + "/kata:to": "\u30C8", + "/kata:do": "\u30C9", + "/kata:na": "\u30CA", + "/kata:ni": "\u30CB", + "/kata:nu": "\u30CC", + "/kata:ne": "\u30CD", + "/kata:no": "\u30CE", + "/kata:ha": "\u30CF", + "/kata:ba": "\u30D0", + "/kata:pa": "\u30D1", + "/kata:hi": "\u30D2", + "/kata:bi": "\u30D3", + "/kata:pi": "\u30D4", + "/kata:hu": "\u30D5", + "/kata:bu": "\u30D6", + "/kata:pu": "\u30D7", + "/kata:he": "\u30D8", + "/kata:be": "\u30D9", + "/kata:pe": "\u30DA", + "/kata:ho": "\u30DB", + "/kata:bo": "\u30DC", + "/kata:po": "\u30DD", + "/kata:ma": "\u30DE", + "/kata:mi": "\u30DF", + "/kata:mu": "\u30E0", + "/kata:me": "\u30E1", + "/kata:mo": "\u30E2", + "/kata:yasmall": "\u30E3", + "/kata:ya": "\u30E4", + "/kata:yusmall": "\u30E5", + "/kata:yu": "\u30E6", + "/kata:yosmall": "\u30E7", + "/kata:yo": "\u30E8", + "/kata:ra": "\u30E9", + "/kata:ri": "\u30EA", + "/kata:ru": "\u30EB", + "/kata:re": "\u30EC", + "/kata:ro": "\u30ED", + "/kata:wasmall": "\u30EE", + "/kata:wa": "\u30EF", + "/kata:wi": "\u30F0", + "/kata:we": "\u30F1", + "/kata:wo": "\u30F2", + "/kata:n": "\u30F3", + "/kata:vu": "\u30F4", + "/kata:kasmall": "\u30F5", + "/kata:kesmall": "\u30F6", + "/kata:va": "\u30F7", + "/kata:vi": "\u30F8", + "/kata:ve": "\u30F9", + "/kata:vo": "\u30FA", + "/kata:middledot": "\u30FB", + "/kata:prolongedkana": "\u30FC", + "/kata:iteration": "\u30FD", + "/kata:voicediteration": "\u30FE", + "/kata:digraphkoto": "\u30FF", + "/bopo:b": "\u3105", + "/bopo:p": "\u3106", + "/bopo:m": "\u3107", + "/bopo:f": "\u3108", + "/bopo:d": "\u3109", + "/bopo:t": "\u310A", + "/bopo:n": "\u310B", + "/bopo:l": "\u310C", + "/bopo:g": "\u310D", + "/bopo:k": "\u310E", + "/bopo:h": "\u310F", + "/bopo:j": "\u3110", + "/bopo:q": "\u3111", + "/bopo:x": "\u3112", + "/bopo:zh": "\u3113", + "/bopo:ch": "\u3114", + "/bopo:sh": "\u3115", + "/bopo:r": "\u3116", + "/bopo:z": "\u3117", + "/bopo:c": "\u3118", + "/bopo:s": "\u3119", + "/bopo:a": "\u311A", + "/bopo:o": "\u311B", + "/bopo:e": "\u311C", + "/bopo:eh": "\u311D", + "/bopo:ai": "\u311E", + "/bopo:ei": "\u311F", + "/bopo:au": "\u3120", + "/bopo:ou": "\u3121", + "/bopo:an": "\u3122", + "/bopo:en": "\u3123", + "/bopo:ang": "\u3124", + "/bopo:eng": "\u3125", + "/bopo:er": "\u3126", + "/bopo:i": "\u3127", + "/bopo:u": "\u3128", + "/bopo:iu": "\u3129", + "/bopo:v": "\u312A", + "/bopo:ng": "\u312B", + "/bopo:gn": "\u312C", + "/bopo:ih": "\u312D", + "/bopo:owithdotabove": "\u312E", + "/bopo:nn": "\u312F", + "/ko:kiyeok": "\u3131", + "/ko:ssangkiyeok": "\u3132", + "/ko:kiyeoksios": "\u3133", + "/ko:nieun": "\u3134", + "/ko:nieuncieuc": "\u3135", + "/ko:nieunhieuh": "\u3136", + "/ko:tikeut": "\u3137", + "/ko:ssangtikeut": "\u3138", + "/ko:rieul": "\u3139", + "/ko:rieulkiyeok": "\u313A", + "/ko:rieulmieum": "\u313B", + "/ko:rieulpieup": "\u313C", + "/ko:rieulsios": "\u313D", + "/ko:rieulthieuth": "\u313E", + "/ko:rieulphieuph": "\u313F", + "/ko:rieulhieuh": "\u3140", + "/ko:mieum": "\u3141", + "/ko:pieup": "\u3142", + "/ko:ssangpieup": "\u3143", + "/ko:pieupsios": "\u3144", + "/ko:sios": "\u3145", + "/ko:ssangsios": "\u3146", + "/ko:ieung": "\u3147", + "/ko:cieuc": "\u3148", + "/ko:ssangcieuc": "\u3149", + "/ko:chieuch": "\u314A", + "/ko:khieukh": "\u314B", + "/ko:thieuth": "\u314C", + "/ko:phieuph": "\u314D", + "/ko:hieuh": "\u314E", + "/ko:a": "\u314F", + "/ko:ae": "\u3150", + "/ko:ya": "\u3151", + "/ko:yae": "\u3152", + "/ko:eo": "\u3153", + "/ko:e": "\u3154", + "/ko:yeo": "\u3155", + "/ko:ye": "\u3156", + "/ko:o": "\u3157", + "/ko:wa": "\u3158", + "/ko:wae": "\u3159", + "/ko:oe": "\u315A", + "/ko:yo": "\u315B", + "/ko:u": "\u315C", + "/ko:weo": "\u315D", + "/ko:we": "\u315E", + "/ko:wi": "\u315F", + "/ko:yu": "\u3160", + "/ko:eu": "\u3161", + "/ko:yi": "\u3162", + "/ko:i": "\u3163", + "/ko:filler": "\u3164", + "/ko:ssangnieun": "\u3165", + "/ko:nieuntikeut": "\u3166", + "/ko:nieunsios": "\u3167", + "/ko:nieunpansios": "\u3168", + "/ko:rieulkiyeoksios": "\u3169", + "/ko:rieultikeut": "\u316A", + "/ko:rieulpieupsios": "\u316B", + "/ko:rieulpansios": "\u316C", + "/ko:rieulyeorinhieuh": "\u316D", + "/ko:mieumpieup": "\u316E", + "/ko:mieumsios": "\u316F", + "/ko:mieumpansios": "\u3170", + "/ko:kapyeounmieum": "\u3171", + "/ko:pieupkiyeok": "\u3172", + "/ko:pieuptikeut": "\u3173", + "/ko:pieupsioskiyeok": "\u3174", + "/ko:pieupsiostikeut": "\u3175", + "/ko:pieupcieuc": "\u3176", + "/ko:pieupthieuth": "\u3177", + "/ko:kapyeounpieup": "\u3178", + "/ko:kapyeounssangpieup": "\u3179", + "/ko:sioskiyeok": "\u317A", + "/ko:siosnieun": "\u317B", + "/ko:siostikeut": "\u317C", + "/ko:siospieup": "\u317D", + "/ko:sioscieuc": "\u317E", + "/ko:pansios": "\u317F", + "/ko:ssangieung": "\u3180", + "/ko:yesieung": "\u3181", + "/ko:yesieungsios": "\u3182", + "/ko:yesieungpansios": "\u3183", + "/ko:kapyeounphieuph": "\u3184", + "/ko:ssanghieuh": "\u3185", + "/ko:yeorinhieuh": "\u3186", + "/ko:yoya": "\u3187", + "/ko:yoyae": "\u3188", + "/ko:yoi": "\u3189", + "/ko:yuyeo": "\u318A", + "/ko:yuye": "\u318B", + "/ko:yui": "\u318C", + "/ko:araea": "\u318D", + "/ko:araeae": "\u318E", + "/hangulkiyeokparen": "\u3200", + "/hangulnieunparen": "\u3201", + "/hangultikeutparen": "\u3202", + "/hangulrieulparen": "\u3203", + "/hangulmieumparen": "\u3204", + "/hangulpieupparen": "\u3205", + "/hangulsiosparen": "\u3206", + "/hangulieungparen": "\u3207", + "/hangulcieucparen": "\u3208", + "/hangulchieuchparen": "\u3209", + "/hangulkhieukhparen": "\u320A", + "/hangulthieuthparen": "\u320B", + "/hangulphieuphparen": "\u320C", + "/hangulhieuhparen": "\u320D", + "/hangulkiyeokaparen": "\u320E", + "/hangulnieunaparen": "\u320F", + "/hangultikeutaparen": "\u3210", + "/hangulrieulaparen": "\u3211", + "/hangulmieumaparen": "\u3212", + "/hangulpieupaparen": "\u3213", + "/hangulsiosaparen": "\u3214", + "/hangulieungaparen": "\u3215", + "/hangulcieucaparen": "\u3216", + "/hangulchieuchaparen": "\u3217", + "/hangulkhieukhaparen": "\u3218", + "/hangulthieuthaparen": "\u3219", + "/hangulphieuphaparen": "\u321A", + "/hangulhieuhaparen": "\u321B", + "/hangulcieucuparen": "\u321C", + "/ojeonparen": "\u321D", + "/ohuparen": "\u321E", + "/moonideographicparen": "\u322A", + "/fireideographicparen": "\u322B", + "/waterideographicparen": "\u322C", + "/woodideographicparen": "\u322D", + "/metalideographicparen": "\u322E", + "/earthideographicparen": "\u322F", + "/sunideographicparen": "\u3230", + "/stockideographicparen": "\u3231", + "/haveideographicparen": "\u3232", + "/societyideographicparen": "\u3233", + "/nameideographicparen": "\u3234", + "/specialideographicparen": "\u3235", + "/financialideographicparen": "\u3236", + "/congratulationideographicparen": "\u3237", + "/laborideographicparen": "\u3238", + "/representideographicparen": "\u3239", + "/callideographicparen": "\u323A", + "/studyideographicparen": "\u323B", + "/superviseideographicparen": "\u323C", + "/enterpriseideographicparen": "\u323D", + "/resourceideographicparen": "\u323E", + "/allianceideographicparen": "\u323F", + "/festivalideographicparen": "\u3240", + "/restideographicparen": "\u3241", + "/selfideographicparen": "\u3242", + "/reachideographicparen": "\u3243", + "/questionideographiccircled": "\u3244", + "/kindergartenideographiccircled": "\u3245", + "/schoolideographiccircled": "\u3246", + "/kotoideographiccircled": "\u3247", + "/tencirclesquare": "\u3248", + "/twentycirclesquare": "\u3249", + "/thirtycirclesquare": "\u324A", + "/fortycirclesquare": "\u324B", + "/fiftycirclesquare": "\u324C", + "/sixtycirclesquare": "\u324D", + "/seventycirclesquare": "\u324E", + "/eightycirclesquare": "\u324F", + "/partnership": "\u3250", + "/twentyonecircle": "\u3251", + "/twentytwocircle": "\u3252", + "/twentythreecircle": "\u3253", + "/twentyfourcircle": "\u3254", + "/twentyfivecircle": "\u3255", + "/twentysixcircle": "\u3256", + "/twentysevencircle": "\u3257", + "/twentyeightcircle": "\u3258", + "/twentyninecircle": "\u3259", + "/thirtycircle": "\u325A", + "/thirtyonecircle": "\u325B", + "/thirtytwocircle": "\u325C", + "/thirtythreecircle": "\u325D", + "/thirtyfourcircle": "\u325E", + "/thirtyfivecircle": "\u325F", + "/chamkocircle": "\u327C", + "/jueuicircle": "\u327D", + "/ieungucirclekorean": "\u327E", + "/oneideographiccircled": "\u3280", + "/twoideographiccircled": "\u3281", + "/threeideographiccircled": "\u3282", + "/fourideographiccircled": "\u3283", + "/fiveideographiccircled": "\u3284", + "/sixideographiccircled": "\u3285", + "/sevenideographiccircled": "\u3286", + "/eightideographiccircled": "\u3287", + "/nineideographiccircled": "\u3288", + "/tenideographiccircled": "\u3289", + "/moonideographiccircled": "\u328A", + "/fireideographiccircled": "\u328B", + "/waterideographiccircled": "\u328C", + "/woodideographiccircled": "\u328D", + "/metalideographiccircled": "\u328E", + "/earthideographiccircled": "\u328F", + "/sunideographiccircled": "\u3290", + "/stockideographiccircled": "\u3291", + "/haveideographiccircled": "\u3292", + "/societyideographiccircled": "\u3293", + "/nameideographiccircled": "\u3294", + "/specialideographiccircled": "\u3295", + "/financialideographiccircled": "\u3296", + "/congratulationideographiccircled": "\u3297", + "/laborideographiccircled": "\u3298", + "/secretideographiccircled": "\u3299", + "/maleideographiccircled": "\u329A", + "/femaleideographiccircled": "\u329B", + "/suitableideographiccircled": "\u329C", + "/excellentideographiccircled": "\u329D", + "/printideographiccircled": "\u329E", + "/attentionideographiccircled": "\u329F", + "/itemideographiccircled": "\u32A0", + "/restideographiccircled": "\u32A1", + "/copyideographiccircled": "\u32A2", + "/correctideographiccircled": "\u32A3", + "/highideographiccircled": "\u32A4", + "/centreideographiccircled": "\u32A5", + "/lowideographiccircled": "\u32A6", + "/leftideographiccircled": "\u32A7", + "/rightideographiccircled": "\u32A8", + "/medicineideographiccircled": "\u32A9", + "/religionideographiccircled": "\u32AA", + "/studyideographiccircled": "\u32AB", + "/superviseideographiccircled": "\u32AC", + "/enterpriseideographiccircled": "\u32AD", + "/resourceideographiccircled": "\u32AE", + "/allianceideographiccircled": "\u32AF", + "/nightideographiccircled": "\u32B0", + "/thirtysixcircle": "\u32B1", + "/thirtysevencircle": "\u32B2", + "/thirtyeightcircle": "\u32B3", + "/thirtyninecircle": "\u32B4", + "/fortycircle": "\u32B5", + "/fortyonecircle": "\u32B6", + "/fortytwocircle": "\u32B7", + "/fortythreecircle": "\u32B8", + "/fortyfourcircle": "\u32B9", + "/fortyfivecircle": "\u32BA", + "/fortysixcircle": "\u32BB", + "/fortysevencircle": "\u32BC", + "/fortyeightcircle": "\u32BD", + "/fortyninecircle": "\u32BE", + "/fiftycircle": "\u32BF", + "/januarytelegraph": "\u32C0", + "/februarytelegraph": "\u32C1", + "/marchtelegraph": "\u32C2", + "/apriltelegraph": "\u32C3", + "/maytelegraph": "\u32C4", + "/junetelegraph": "\u32C5", + "/julytelegraph": "\u32C6", + "/augusttelegraph": "\u32C7", + "/septembertelegraph": "\u32C8", + "/octobertelegraph": "\u32C9", + "/novembertelegraph": "\u32CA", + "/decembertelegraph": "\u32CB", + "/Hgfullwidth": "\u32CC", + "/ergfullwidth": "\u32CD", + "/eVfullwidth": "\u32CE", + "/LTDfullwidth": "\u32CF", + "/acirclekatakana": "\u32D0", + "/icirclekatakana": "\u32D1", + "/ucirclekatakana": "\u32D2", + "/ecirclekatakana": "\u32D3", + "/ocirclekatakana": "\u32D4", + "/kacirclekatakana": "\u32D5", + "/kicirclekatakana": "\u32D6", + "/kucirclekatakana": "\u32D7", + "/kecirclekatakana": "\u32D8", + "/kocirclekatakana": "\u32D9", + "/sacirclekatakana": "\u32DA", + "/sicirclekatakana": "\u32DB", + "/sucirclekatakana": "\u32DC", + "/secirclekatakana": "\u32DD", + "/socirclekatakana": "\u32DE", + "/tacirclekatakana": "\u32DF", + "/ticirclekatakana": "\u32E0", + "/tucirclekatakana": "\u32E1", + "/tecirclekatakana": "\u32E2", + "/tocirclekatakana": "\u32E3", + "/nacirclekatakana": "\u32E4", + "/nicirclekatakana": "\u32E5", + "/nucirclekatakana": "\u32E6", + "/necirclekatakana": "\u32E7", + "/nocirclekatakana": "\u32E8", + "/hacirclekatakana": "\u32E9", + "/hicirclekatakana": "\u32EA", + "/hucirclekatakana": "\u32EB", + "/hecirclekatakana": "\u32EC", + "/hocirclekatakana": "\u32ED", + "/macirclekatakana": "\u32EE", + "/micirclekatakana": "\u32EF", + "/mucirclekatakana": "\u32F0", + "/mecirclekatakana": "\u32F1", + "/mocirclekatakana": "\u32F2", + "/yacirclekatakana": "\u32F3", + "/yucirclekatakana": "\u32F4", + "/yocirclekatakana": "\u32F5", + "/racirclekatakana": "\u32F6", + "/ricirclekatakana": "\u32F7", + "/rucirclekatakana": "\u32F8", + "/recirclekatakana": "\u32F9", + "/rocirclekatakana": "\u32FA", + "/wacirclekatakana": "\u32FB", + "/wicirclekatakana": "\u32FC", + "/wecirclekatakana": "\u32FD", + "/wocirclekatakana": "\u32FE", + "/squareeranamereiwa": "\u32FF", + "/aruhuasquare": "\u3301", + "/anpeasquare": "\u3302", + "/iningusquare": "\u3304", + "/uonsquare": "\u3306", + "/esukuudosquare": "\u3307", + "/eekaasquare": "\u3308", + "/onsusquare": "\u3309", + "/oomusquare": "\u330A", + "/kairisquare": "\u330B", + "/karattosquare": "\u330C", + "/garonsquare": "\u330E", + "/ganmasquare": "\u330F", + "/gigasquare": "\u3310", + "/giniisquare": "\u3311", + "/kyuriisquare": "\u3312", + "/girudaasquare": "\u3313", + "/kirowattosquare": "\u3317", + "/guramutonsquare": "\u3319", + "/kuruzeirosquare": "\u331A", + "/kuroonesquare": "\u331B", + "/keesusquare": "\u331C", + "/korunasquare": "\u331D", + "/saikurusquare": "\u331F", + "/santiimusquare": "\u3320", + "/siringusquare": "\u3321", + "/daasusquare": "\u3324", + "/desisquare": "\u3325", + "/nanosquare": "\u3328", + "/nottosquare": "\u3329", + "/paatusquare": "\u332C", + "/baarerusquare": "\u332D", + "/piasutorusquare": "\u332E", + "/pikurusquare": "\u332F", + "/pikosquare": "\u3330", + "/huaraddosquare": "\u3332", + "/bussyerusquare": "\u3334", + "/huransquare": "\u3335", + "/pesosquare": "\u3337", + "/penihisquare": "\u3338", + "/pensusquare": "\u333A", + "/beetasquare": "\u333C", + "/pointosquare": "\u333D", + "/borutosquare": "\u333E", + "/honsquare": "\u333F", + "/pondosquare": "\u3340", + "/hoorusquare": "\u3341", + "/maikurosquare": "\u3343", + "/mairusquare": "\u3344", + "/mahhasquare": "\u3345", + "/marukusquare": "\u3346", + "/mikuronsquare": "\u3348", + "/megasquare": "\u334B", + "/megatonsquare": "\u334C", + "/yaarusquare": "\u334F", + "/yuansquare": "\u3350", + "/rirasquare": "\u3352", + "/rupiisquare": "\u3353", + "/ruuburusquare": "\u3354", + "/remusquare": "\u3355", + "/rentogensquare": "\u3356", + "/ideographictelegraphsymbolforhourzero": "\u3358", + "/ideographictelegraphsymbolforhourone": "\u3359", + "/ideographictelegraphsymbolforhourtwo": "\u335A", + "/ideographictelegraphsymbolforhourthree": "\u335B", + "/ideographictelegraphsymbolforhourfour": "\u335C", + "/ideographictelegraphsymbolforhourfive": "\u335D", + "/ideographictelegraphsymbolforhoursix": "\u335E", + "/ideographictelegraphsymbolforhourseven": "\u335F", + "/ideographictelegraphsymbolforhoureight": "\u3360", + "/ideographictelegraphsymbolforhournine": "\u3361", + "/ideographictelegraphsymbolforhourten": "\u3362", + "/ideographictelegraphsymbolforhoureleven": "\u3363", + "/ideographictelegraphsymbolforhourtwelve": "\u3364", + "/ideographictelegraphsymbolforhourthirteen": "\u3365", + "/ideographictelegraphsymbolforhourfourteen": "\u3366", + "/ideographictelegraphsymbolforhourfifteen": "\u3367", + "/ideographictelegraphsymbolforhoursixteen": "\u3368", + "/ideographictelegraphsymbolforhourseventeen": "\u3369", + "/ideographictelegraphsymbolforhoureighteen": "\u336A", + "/ideographictelegraphsymbolforhournineteen": "\u336B", + "/ideographictelegraphsymbolforhourtwenty": "\u336C", + "/ideographictelegraphsymbolforhourtwentyone": "\u336D", + "/ideographictelegraphsymbolforhourtwentytwo": "\u336E", + "/ideographictelegraphsymbolforhourtwentythree": "\u336F", + "/ideographictelegraphsymbolforhourtwentyfour": "\u3370", + "/hpafullwidth": "\u3371", + "/dafullwidth": "\u3372", + "/aufullwidth": "\u3373", + "/barfullwidth": "\u3374", + "/ovfullwidth": "\u3375", + "/pcfullwidth": "\u3376", + "/dmfullwidth": "\u3377", + "/dm2fullwidth": "\u3378", + "/dm3fullwidth": "\u3379", + "/iufullwidth": "\u337A", + "/eranameheiseisquare": "\u337B", + "/eranamesyouwasquare": "\u337C", + "/eranametaisyousquare": "\u337D", + "/eranamemeizisquare": "\u337E", + "/paampsfullwidth": "\u3380", + "/nafullwidth": "\u3381", + "/muafullwidth": "\u3382", + "/mafullwidth": "\u3383", + "/kafullwidth": "\u3384", + "/kbfullwidth": "\u3385", + "/mbfullwidth": "\u3386", + "/gbfullwidth": "\u3387", + "/calfullwidth": "\u3388", + "/kcalfullwidth": "\u3389", + "/pffullwidth": "\u338A", + "/nffullwidth": "\u338B", + "/muffullwidth": "\u338C", + "/mugfullwidth": "\u338D", + "/mgfullwidth": "\u338E", + "/kgfullwidth": "\u338F", + "/hzfullwidth": "\u3390", + "/khzfullwidth": "\u3391", + "/mhzfullwidth": "\u3392", + "/ghzfullwidth": "\u3393", + "/thzfullwidth": "\u3394", + "/mulfullwidth": "\u3395", + "/mlfullwidth": "\u3396", + "/dlfullwidth": "\u3397", + "/klfullwidth": "\u3398", + "/fmfullwidth": "\u3399", + "/nmfullwidth": "\u339A", + "/mumfullwidth": "\u339B", + "/mmfullwidth": "\u339C", + "/cmfullwidth": "\u339D", + "/kmfullwidth": "\u339E", + "/mm2fullwidth": "\u339F", + "/cm2fullwidth": "\u33A0", + "/m2fullwidth": "\u33A1", + "/km2fullwidth": "\u33A2", + "/mm3fullwidth": "\u33A3", + "/cm3fullwidth": "\u33A4", + "/m3fullwidth": "\u33A5", + "/km3fullwidth": "\u33A6", + "/moversfullwidth": "\u33A7", + "/movers2fullwidth": "\u33A8", + "/pafullwidth": "\u33A9", + "/kpafullwidth": "\u33AA", + "/mpafullwidth": "\u33AB", + "/gpafullwidth": "\u33AC", + "/radfullwidth": "\u33AD", + "/radoversfullwidth": "\u33AE", + "/radovers2fullwidth": "\u33AF", + "/psfullwidth": "\u33B0", + "/nsfullwidth": "\u33B1", + "/musfullwidth": "\u33B2", + "/msfullwidth": "\u33B3", + "/pvfullwidth": "\u33B4", + "/nvfullwidth": "\u33B5", + "/muvfullwidth": "\u33B6", + "/mvfullwidth": "\u33B7", + "/kvfullwidth": "\u33B8", + "/mvmegafullwidth": "\u33B9", + "/pwfullwidth": "\u33BA", + "/nwfullwidth": "\u33BB", + "/muwfullwidth": "\u33BC", + "/mwfullwidth": "\u33BD", + "/kwfullwidth": "\u33BE", + "/mwmegafullwidth": "\u33BF", + "/kohmfullwidth": "\u33C0", + "/mohmfullwidth": "\u33C1", + "/amfullwidth": "\u33C2", + "/bqfullwidth": "\u33C3", + "/ccfullwidth": "\u33C4", + "/cdfullwidth": "\u33C5", + "/coverkgfullwidth": "\u33C6", + "/cofullwidth": "\u33C7", + "/dbfullwidth": "\u33C8", + "/gyfullwidth": "\u33C9", + "/hafullwidth": "\u33CA", + "/hpfullwidth": "\u33CB", + "/infullwidth": "\u33CC", + "/kkfullwidth": "\u33CD", + "/kmcapitalfullwidth": "\u33CE", + "/ktfullwidth": "\u33CF", + "/lmfullwidth": "\u33D0", + "/lnfullwidth": "\u33D1", + "/logfullwidth": "\u33D2", + "/lxfullwidth": "\u33D3", + "/mbsmallfullwidth": "\u33D4", + "/milfullwidth": "\u33D5", + "/molfullwidth": "\u33D6", + "/phfullwidth": "\u33D7", + "/pmfullwidth": "\u33D8", + "/ppmfullwidth": "\u33D9", + "/prfullwidth": "\u33DA", + "/srfullwidth": "\u33DB", + "/svfullwidth": "\u33DC", + "/wbfullwidth": "\u33DD", + "/vovermfullwidth": "\u33DE", + "/aovermfullwidth": "\u33DF", + "/dayonetelegraph": "\u33E0", + "/daytwotelegraph": "\u33E1", + "/daythreetelegraph": "\u33E2", + "/dayfourtelegraph": "\u33E3", + "/dayfivetelegraph": "\u33E4", + "/daysixtelegraph": "\u33E5", + "/dayseventelegraph": "\u33E6", + "/dayeighttelegraph": "\u33E7", + "/dayninetelegraph": "\u33E8", + "/daytentelegraph": "\u33E9", + "/dayeleventelegraph": "\u33EA", + "/daytwelvetelegraph": "\u33EB", + "/daythirteentelegraph": "\u33EC", + "/dayfourteentelegraph": "\u33ED", + "/dayfifteentelegraph": "\u33EE", + "/daysixteentelegraph": "\u33EF", + "/dayseventeentelegraph": "\u33F0", + "/dayeighteentelegraph": "\u33F1", + "/daynineteentelegraph": "\u33F2", + "/daytwentytelegraph": "\u33F3", + "/daytwentyonetelegraph": "\u33F4", + "/daytwentytwotelegraph": "\u33F5", + "/daytwentythreetelegraph": "\u33F6", + "/daytwentyfourtelegraph": "\u33F7", + "/daytwentyfivetelegraph": "\u33F8", + "/daytwentysixtelegraph": "\u33F9", + "/daytwentyseventelegraph": "\u33FA", + "/daytwentyeighttelegraph": "\u33FB", + "/daytwentyninetelegraph": "\u33FC", + "/daythirtytelegraph": "\u33FD", + "/daythirtyonetelegraph": "\u33FE", + "/galsquare": "\u33FF", + "/stresstonemod": "\uA720", + "/stresslowtonemod": "\uA721", + "/Egyptalef": "\uA722", + "/egyptalef": "\uA723", + "/Egyptain": "\uA724", + "/egyptain": "\uA725", + "/Heng": "\uA726", + "/heng": "\uA727", + "/Tz": "\uA728", + "/tz": "\uA729", + "/Tresillo": "\uA72A", + "/tresillo": "\uA72B", + "/Cuatrillo": "\uA72C", + "/cuatrillo": "\uA72D", + "/Cuatrillocomma": "\uA72E", + "/cuatrillocomma": "\uA72F", + "/AA": "\uA732", + "/aa": "\uA733", + "/AO": "\uA734", + "/ao": "\uA735", + "/AU": "\uA736", + "/au": "\uA737", + "/AV": "\uA738", + "/av": "\uA739", + "/AVhorizontalbar": "\uA73A", + "/avhorizontalbar": "\uA73B", + "/AY": "\uA73C", + "/ay": "\uA73D", + "/Cdotreversed": "\uA73E", + "/cdotreversed": "\uA73F", + "/Kstroke": "\uA740", + "/kstroke": "\uA741", + "/Kdiagonalstroke": "\uA742", + "/kdiagonalstroke": "\uA743", + "/Kstrokediagonalstroke": "\uA744", + "/kstrokediagonalstroke": "\uA745", + "/Lbroken": "\uA746", + "/lbroken": "\uA747", + "/Lstroke": "\uA748", + "/lstroke": "\uA749", + "/Ostroke": "\uA74A", + "/ostroke": "\uA74B", + "/Oloop": "\uA74C", + "/oloop": "\uA74D", + "/OO": "\uA74E", + "/oo": "\uA74F", + "/Pstrokedescender": "\uA750", + "/pstrokedescender": "\uA751", + "/Pflourish": "\uA752", + "/pflourish": "\uA753", + "/Ptail": "\uA754", + "/ptail": "\uA755", + "/Qstrokedescender": "\uA756", + "/qstrokedescender": "\uA757", + "/Qdiagonalstroke": "\uA758", + "/qdiagonalstroke": "\uA759", + "/Rrotunda": "\uA75A", + "/rrotunda": "\uA75B", + "/Rumrotunda": "\uA75C", + "/rumrotunda": "\uA75D", + "/Vdiagonalstroke": "\uA75E", + "/vdiagonalstroke": "\uA75F", + "/Vy": "\uA760", + "/vy": "\uA761", + "/Visigothicz": "\uA762", + "/visigothicz": "\uA763", + "/Thornstroke": "\uA764", + "/thornstroke": "\uA765", + "/Thornstrokedescender": "\uA766", + "/thornstrokedescender": "\uA767", + "/Vend": "\uA768", + "/vend": "\uA769", + "/Et": "\uA76A", + "/et": "\uA76B", + "/Is": "\uA76C", + "/is": "\uA76D", + "/Con": "\uA76E", + "/con": "\uA76F", + "/usmod": "\uA770", + "/dum": "\uA771", + "/lum": "\uA772", + "/mum": "\uA773", + "/num": "\uA774", + "/rum": "\uA775", + "/Rumsmall": "\uA776", + "/tum": "\uA777", + "/um": "\uA778", + "/Dinsular": "\uA779", + "/dinsular": "\uA77A", + "/Finsular": "\uA77B", + "/finsular": "\uA77C", + "/Ginsular": "\uA77D", + "/Ginsularturned": "\uA77E", + "/ginsularturned": "\uA77F", + "/Lturned": "\uA780", + "/lturned": "\uA781", + "/Rinsular": "\uA782", + "/rinsular": "\uA783", + "/Sinsular": "\uA784", + "/sinsular": "\uA785", + "/Tinsular": "\uA786", + "/tinsular": "\uA787", + "/circumflexlow": "\uA788", + "/colonmod": "\uA789", + "/shortequalsmod": "\uA78A", + "/Saltillo": "\uA78B", + "/saltillo": "\uA78C", + "/Hturned": "\uA78D", + "/lbeltretroflex": "\uA78E", + "/sinologicaldot": "\uA78F", + "/Ndescender": "\uA790", + "/ndescender": "\uA791", + "/Cbar": "\uA792", + "/cbar": "\uA793", + "/cpalatalhook": "\uA794", + "/hpalatalhook": "\uA795", + "/Bflourish": "\uA796", + "/bflourish": "\uA797", + "/Fstroke": "\uA798", + "/fstroke": "\uA799", + "/Volapukae": "\uA79A", + "/volapukae": "\uA79B", + "/Volapukoe": "\uA79C", + "/volapukoe": "\uA79D", + "/Volapukue": "\uA79E", + "/volapukue": "\uA79F", + "/Gobliquestroke": "\uA7A0", + "/gobliquestroke": "\uA7A1", + "/Kobliquestroke": "\uA7A2", + "/kobliquestroke": "\uA7A3", + "/Nobliquestroke": "\uA7A4", + "/nobliquestroke": "\uA7A5", + "/Robliquestroke": "\uA7A6", + "/robliquestroke": "\uA7A7", + "/Sobliquestroke": "\uA7A8", + "/sobliquestroke": "\uA7A9", + "/Hhook": "\uA7AA", + "/Ereversedopen": "\uA7AB", + "/Scriptg": "\uA7AC", + "/Lbelt": "\uA7AD", + "/Kturned": "\uA7B0", + "/Tturned": "\uA7B1", + "/Jcrossed-tail": "\uA7B2", + "/ltgr:Chi": "\uA7B3", + "/ltgr:Beta": "\uA7B4", + "/ltgr:beta": "\uA7B5", + "/ltgr:Omega": "\uA7B6", + "/ltgr:omega": "\uA7B7", + "/Ustroke": "\uA7B8", + "/ustroke": "\uA7B9", + "/Glottala": "\uA7BA", + "/glottala": "\uA7BB", + "/Glottali": "\uA7BC", + "/glottali": "\uA7BD", + "/Glottalu": "\uA7BE", + "/glottalu": "\uA7BF", + "/Oldpolisho": "\uA7C0", + "/oldpolisho": "\uA7C1", + "/Anglicanaw": "\uA7C2", + "/anglicanaw": "\uA7C3", + "/Cpalatalhook": "\uA7C4", + "/Shook": "\uA7C5", + "/Zpalatalhook": "\uA7C6", + "/Dshortstroke": "\uA7C7", + "/dshortstroke": "\uA7C8", + "/Sshortstroke": "\uA7C9", + "/sshortstroke": "\uA7CA", + "/Ramshorn": "\uA7CB", + "/Sdiagonalstroke": "\uA7CC", + "/sdiagonalstroke": "\uA7CD", + "/Ginsularclosed": "\uA7D0", + "/ginsularclosed": "\uA7D1", + "/doublethorn": "\uA7D3", + "/doublewynn": "\uA7D5", + "/Middlescotss": "\uA7D6", + "/middlescotss": "\uA7D7", + "/Sigmoids": "\uA7D8", + "/sigmoids": "\uA7D9", + "/ltgr:Lambda": "\uA7DA", + "/ltgr:lambda": "\uA7DB", + "/Lambdastroke": "\uA7DC", + "/Cmod": "\uA7F2", + "/Fmod": "\uA7F3", + "/Qmod": "\uA7F4", + "/Halfhreversed": "\uA7F5", + "/halfhreversed": "\uA7F6", + "/iepigraphicsideways": "\uA7F7", + "/Hstrokemod": "\uA7F8", + "/ligatureoemod": "\uA7F9", + "/Mturnedsmall": "\uA7FA", + "/freversedepigraphic": "\uA7FB", + "/preversedepigraphic": "\uA7FC", + "/mepigraphicinverted": "\uA7FD", + "/iaepigraphic": "\uA7FE", + "/archaicmepigraphic": "\uA7FF", + "/panyangga": "\uA980", + "/cecak": "\uA981", + "/layar": "\uA982", + "/wignyan": "\uA983", + "/java:a": "\uA984", + "/ikawi": "\uA985", + "/java:i": "\uA986", + "/ii": "\uA987", + "/java:u": "\uA988", + "/pacerek": "\uA989", + "/ngalelet": "\uA98A", + "/ngaleletraswadi": "\uA98B", + "/java:e": "\uA98C", + "/ai": "\uA98D", + "/java:o": "\uA98E", + "/ka": "\uA98F", + "/kasasak": "\uA990", + "/kamurda": "\uA991", + "/ga": "\uA992", + "/gamurda": "\uA993", + "/nga": "\uA994", + "/ca": "\uA995", + "/camurda": "\uA996", + "/ja": "\uA997", + "/nyamurda": "\uA998", + "/jamahaprana": "\uA999", + "/nya": "\uA99A", + "/tta": "\uA99B", + "/ttamahaprana": "\uA99C", + "/dda": "\uA99D", + "/ddamahaprana": "\uA99E", + "/namurda": "\uA99F", + "/ta": "\uA9A0", + "/tamurda": "\uA9A1", + "/da": "\uA9A2", + "/damahaprana": "\uA9A3", + "/na": "\uA9A4", + "/pa": "\uA9A5", + "/pamurda": "\uA9A6", + "/ba": "\uA9A7", + "/bamurda": "\uA9A8", + "/ma": "\uA9A9", + "/ya": "\uA9AA", + "/ra": "\uA9AB", + "/raagung": "\uA9AC", + "/la": "\uA9AD", + "/wa": "\uA9AE", + "/samurda": "\uA9AF", + "/samahaprana": "\uA9B0", + "/sa": "\uA9B1", + "/ha": "\uA9B2", + "/cecaktelu": "\uA9B3", + "/tarungvowel": "\uA9B4", + "/tolongvowel": "\uA9B5", + "/wuluvowel": "\uA9B6", + "/wulumelikvowel": "\uA9B7", + "/sukuvowel": "\uA9B8", + "/sukumendutvowel": "\uA9B9", + "/talingvowel": "\uA9BA", + "/dirgamurevowel": "\uA9BB", + "/pepetvowel": "\uA9BC", + "/keretconsonant": "\uA9BD", + "/pengkalconsonant": "\uA9BE", + "/cakraconsonant": "\uA9BF", + "/pangkon": "\uA9C0", + "/rerengganleft": "\uA9C1", + "/rerengganright": "\uA9C2", + "/andappada": "\uA9C3", + "/madyapada": "\uA9C4", + "/luhurpada": "\uA9C5", + "/windupada": "\uA9C6", + "/pangkatpada": "\uA9C7", + "/lingsapada": "\uA9C8", + "/lungsipada": "\uA9C9", + "/adegpada": "\uA9CA", + "/adegadegpada": "\uA9CB", + "/piselehpada": "\uA9CC", + "/turnedpiselehpada": "\uA9CD", + "/pangrangkep": "\uA9CF", + "/java:zero": "\uA9D0", + "/java:one": "\uA9D1", + "/java:two": "\uA9D2", + "/java:three": "\uA9D3", + "/java:four": "\uA9D4", + "/java:five": "\uA9D5", + "/java:six": "\uA9D6", + "/java:seven": "\uA9D7", + "/java:eight": "\uA9D8", + "/java:nine": "\uA9D9", + "/tirtatumetespada": "\uA9DE", + "/isen-isenpada": "\uA9DF", + "/ethi:tthu": "\uAB01", + "/ethi:tthi": "\uAB02", + "/ethi:tthaa": "\uAB03", + "/ethi:tthee": "\uAB04", + "/ethi:tthe": "\uAB05", + "/ethi:ttho": "\uAB06", + "/ethi:ddhu": "\uAB09", + "/ethi:ddhi": "\uAB0A", + "/ethi:ddhaa": "\uAB0B", + "/ethi:ddhee": "\uAB0C", + "/ethi:ddhe": "\uAB0D", + "/ethi:ddho": "\uAB0E", + "/ethi:dzu": "\uAB11", + "/ethi:dzi": "\uAB12", + "/ethi:dzaa": "\uAB13", + "/ethi:dzee": "\uAB14", + "/ethi:dze": "\uAB15", + "/ethi:dzo": "\uAB16", + "/ethi:cchha": "\uAB20", + "/ethi:cchhu": "\uAB21", + "/ethi:cchhi": "\uAB22", + "/ethi:cchhaa": "\uAB23", + "/ethi:cchhee": "\uAB24", + "/ethi:cchhe": "\uAB25", + "/ethi:cchho": "\uAB26", + "/ethi:bba": "\uAB28", + "/ethi:bbu": "\uAB29", + "/ethi:bbi": "\uAB2A", + "/ethi:bbaa": "\uAB2B", + "/ethi:bbee": "\uAB2C", + "/ethi:bbe": "\uAB2D", + "/ethi:bbo": "\uAB2E", + "/redalphabar": "\uAB30", + "/areversedschwa": "\uAB31", + "/efractur": "\uAB32", + "/redebar": "\uAB33", + "/eflourish": "\uAB34", + "/flenis": "\uAB35", + "/gtailscript": "\uAB36", + "/llazyinverteds": "\uAB37", + "/lmiddledbltilde": "\uAB38", + "/lmiddlering": "\uAB39", + "/mtail": "\uAB3A", + "/ntail": "\uAB3B", + "/engtail": "\uAB3C", + "/ofractur": "\uAB3D", + "/ofracturstroke": "\uAB3E", + "/ostrokeopen": "\uAB3F", + "/oeinverted": "\uAB40", + "/oestroketurned": "\uAB41", + "/oehorizontalstroketurned": "\uAB42", + "/ooopenturned": "\uAB43", + "/ooopenstroketurned": "\uAB44", + "/stirrupr": "\uAB45", + "/Rrightlegsmall": "\uAB46", + "/routhandle": "\uAB47", + "/rdbl": "\uAB48", + "/rtail": "\uAB49", + "/rtaildbl": "\uAB4A", + "/rscript": "\uAB4B", + "/rringscript": "\uAB4C", + "/baselineesh": "\uAB4D", + "/urightlegshort": "\uAB4E", + "/urightlegbarshort": "\uAB4F", + "/ui": "\uAB50", + "/uiturned": "\uAB51", + "/ulefthook": "\uAB52", + "/ltgr:chi": "\uAB53", + "/chilowrightring": "\uAB54", + "/chilowleftserif": "\uAB55", + "/xlowrightring": "\uAB56", + "/xlongleftleg": "\uAB57", + "/xlongleftlegandlowrightring": "\uAB58", + "/xlongleftlegserif": "\uAB59", + "/yrightlegshort": "\uAB5A", + "/breveinvertedmod": "\uAB5B", + "/hengmod": "\uAB5C", + "/llazyinvertedsmod": "\uAB5D", + "/lmiddletildemod": "\uAB5E", + "/ulefthookmod": "\uAB5F", + "/sakhayat": "\uAB60", + "/iotifiede": "\uAB61", + "/oeopen": "\uAB62", + "/uo": "\uAB63", + "/alphainverted": "\uAB64", + "/Omegasmall": "\uAB65", + "/dzdigraphretroflexhook": "\uAB66", + "/tsdigraphretroflexhook": "\uAB67", + "/rmiddletildeturned": "\uAB68", + "/wturnedmod": "\uAB69", + "/modLetterLeftTack": "\uAB6A", + "/modLetterRightTack": "\uAB6B", + "/chrk:asmall": "\uAB70", + "/chrk:esmall": "\uAB71", + "/chrk:ismall": "\uAB72", + "/chrk:osmall": "\uAB73", + "/chrk:usmall": "\uAB74", + "/chrk:vsmall": "\uAB75", + "/chrk:gasmall": "\uAB76", + "/chrk:kasmall": "\uAB77", + "/chrk:gesmall": "\uAB78", + "/chrk:gismall": "\uAB79", + "/chrk:gosmall": "\uAB7A", + "/chrk:gusmall": "\uAB7B", + "/chrk:gvsmall": "\uAB7C", + "/chrk:hasmall": "\uAB7D", + "/chrk:hesmall": "\uAB7E", + "/chrk:hismall": "\uAB7F", + "/chrk:hosmall": "\uAB80", + "/chrk:husmall": "\uAB81", + "/chrk:hvsmall": "\uAB82", + "/chrk:lasmall": "\uAB83", + "/chrk:lesmall": "\uAB84", + "/chrk:lismall": "\uAB85", + "/chrk:losmall": "\uAB86", + "/chrk:lusmall": "\uAB87", + "/chrk:lvsmall": "\uAB88", + "/chrk:masmall": "\uAB89", + "/chrk:mesmall": "\uAB8A", + "/chrk:mismall": "\uAB8B", + "/chrk:mosmall": "\uAB8C", + "/chrk:musmall": "\uAB8D", + "/chrk:nasmall": "\uAB8E", + "/chrk:hnasmall": "\uAB8F", + "/chrk:nahsmall": "\uAB90", + "/chrk:nesmall": "\uAB91", + "/chrk:nismall": "\uAB92", + "/chrk:nosmall": "\uAB93", + "/chrk:nusmall": "\uAB94", + "/chrk:nvsmall": "\uAB95", + "/chrk:quasmall": "\uAB96", + "/chrk:quesmall": "\uAB97", + "/chrk:quismall": "\uAB98", + "/chrk:quosmall": "\uAB99", + "/chrk:quusmall": "\uAB9A", + "/chrk:quvsmall": "\uAB9B", + "/chrk:sasmall": "\uAB9C", + "/chrk:ssmall": "\uAB9D", + "/chrk:sesmall": "\uAB9E", + "/chrk:sismall": "\uAB9F", + "/chrk:sosmall": "\uABA0", + "/chrk:susmall": "\uABA1", + "/chrk:svsmall": "\uABA2", + "/chrk:dasmall": "\uABA3", + "/chrk:tasmall": "\uABA4", + "/chrk:desmall": "\uABA5", + "/chrk:tesmall": "\uABA6", + "/chrk:dismall": "\uABA7", + "/chrk:tismall": "\uABA8", + "/chrk:dosmall": "\uABA9", + "/chrk:dusmall": "\uABAA", + "/chrk:dvsmall": "\uABAB", + "/chrk:dlasmall": "\uABAC", + "/chrk:tlasmall": "\uABAD", + "/chrk:tlesmall": "\uABAE", + "/chrk:tlismall": "\uABAF", + "/chrk:tlosmall": "\uABB0", + "/chrk:tlusmall": "\uABB1", + "/chrk:tlvsmall": "\uABB2", + "/chrk:tsasmall": "\uABB3", + "/chrk:tsesmall": "\uABB4", + "/chrk:tsismall": "\uABB5", + "/chrk:tsosmall": "\uABB6", + "/chrk:tsusmall": "\uABB7", + "/chrk:tsvsmall": "\uABB8", + "/chrk:wasmall": "\uABB9", + "/chrk:wesmall": "\uABBA", + "/chrk:wismall": "\uABBB", + "/chrk:wosmall": "\uABBC", + "/chrk:wusmall": "\uABBD", + "/chrk:wvsmall": "\uABBE", + "/chrk:yasmall": "\uABBF", + "/f_f": "\uFB00", + "/f_f_i": "\uFB03", + "/f_f_l": "\uFB04", + "/longs_t": "\uFB05", + "/s_t": "\uFB06", + "/men_nowarmn": "\uFB13", + "/men_echarmn": "\uFB14", + "/men_iniarmn": "\uFB15", + "/vew_nowarmn": "\uFB16", + "/men_xeharmn": "\uFB17", + "/yodwithhiriq:hb": "\uFB1D", + "/varikajudeospanish:hb": "\uFB1E", + "/yod_yod_patah:hb": "\uFB1F", + "/ayinalt:hb": "\uFB20", + "/alefwide:hb": "\uFB21", + "/daletwide:hb": "\uFB22", + "/hewide:hb": "\uFB23", + "/kafwide:hb": "\uFB24", + "/lamedwide:hb": "\uFB25", + "/finalmemwide:hb": "\uFB26", + "/reshwide:hb": "\uFB27", + "/tavwide:hb": "\uFB28", + "/plussignalt:hb": "\uFB29", + "/shinwithshinDot:hb": "\uFB2A", + "/shinwithsinDot:hb": "\uFB2B", + "/shinwithdageshandshinDot:hb": "\uFB2C", + "/shinwithdageshandsinDot:hb": "\uFB2D", + "/alefwithpatah:hb": "\uFB2E", + "/alefwithqamats:hb": "\uFB2F", + "/alefwithmapiq:hb": "\uFB30", + "/betwithdagesh:hb": "\uFB31", + "/gimelwithdagesh:hb": "\uFB32", + "/daletwithdagesh:hb": "\uFB33", + "/hewithmapiq:hb": "\uFB34", + "/vavwithdagesh:hb": "\uFB35", + "/zayinwithdagesh:hb": "\uFB36", + "/tetwithdagesh:hb": "\uFB38", + "/yodwithdagesh:hb": "\uFB39", + "/finalkafwithdagesh:hb": "\uFB3A", + "/kafwithdagesh:hb": "\uFB3B", + "/lamedwithdagesh:hb": "\uFB3C", + "/memwithdagesh:hb": "\uFB3E", + "/nunwithdagesh:hb": "\uFB40", + "/samekhwithdagesh:hb": "\uFB41", + "/finalpewithdagesh:hb": "\uFB43", + "/pewithdagesh:hb": "\uFB44", + "/tsadiwithdagesh:hb": "\uFB46", + "/qofwithdagesh:hb": "\uFB47", + "/reshwithdagesh:hb": "\uFB48", + "/shinwithdagesh:hb": "\uFB49", + "/tavwithdagesh:hb": "\uFB4A", + "/vavwithholam:hb": "\uFB4B", + "/betwithrafe:hb": "\uFB4C", + "/kafwithrafe:hb": "\uFB4D", + "/pewithrafe:hb": "\uFB4E", + "/ligaturealeflamed:hb": "\uFB4F", + "/alefwasla.isol": "\uFB50", + "/alefwasla.fina": "\uFB51", + "/beeh.isol": "\uFB52", + "/beeh.fina": "\uFB53", + "/beeh.init": "\uFB54", + "/beeh.medi": "\uFB55", + "/peh.isol": "\uFB56", + "/peh.fina": "\uFB57", + "/peh.init": "\uFB58", + "/peh.medi": "\uFB59", + "/beheh.isol": "\uFB5A", + "/beheh.fina": "\uFB5B", + "/beheh.init": "\uFB5C", + "/beheh.medi": "\uFB5D", + "/tteheh.isol": "\uFB5E", + "/tteheh.fina": "\uFB5F", + "/tteheh.init": "\uFB60", + "/tteheh.medi": "\uFB61", + "/teheh.isol": "\uFB62", + "/teheh.fina": "\uFB63", + "/teheh.init": "\uFB64", + "/teheh.medi": "\uFB65", + "/tteh.isol": "\uFB66", + "/tteh.fina": "\uFB67", + "/tteh.init": "\uFB68", + "/tteh.medi": "\uFB69", + "/veh.isol": "\uFB6A", + "/veh.fina": "\uFB6B", + "/veh.init": "\uFB6C", + "/veh.medi": "\uFB6D", + "/peheh.isol": "\uFB6E", + "/peheh.fina": "\uFB6F", + "/peheh.init": "\uFB70", + "/peheh.medi": "\uFB71", + "/dyeh.isol": "\uFB72", + "/dyeh.fina": "\uFB73", + "/dyeh.init": "\uFB74", + "/dyeh.medi": "\uFB75", + "/nyeh.isol": "\uFB76", + "/nyeh.fina": "\uFB77", + "/nyeh.init": "\uFB78", + "/nyeh.medi": "\uFB79", + "/tcheh.isol": "\uFB7A", + "/tcheh.fina": "\uFB7B", + "/tcheh.init": "\uFB7C", + "/tcheh.medi": "\uFB7D", + "/tcheheh.isol": "\uFB7E", + "/tcheheh.fina": "\uFB7F", + "/tcheheh.init": "\uFB80", + "/tcheheh.medi": "\uFB81", + "/ddahal.isol": "\uFB82", + "/ddahal.fina": "\uFB83", + "/dahal.isol": "\uFB84", + "/dahal.fina": "\uFB85", + "/dul.isol": "\uFB86", + "/dul.fina": "\uFB87", + "/ddal.isol": "\uFB88", + "/ddal.fina": "\uFB89", + "/jeh.isol": "\uFB8A", + "/jeh.fina": "\uFB8B", + "/rreh.isol": "\uFB8C", + "/rreh.fina": "\uFB8D", + "/keheh.isol": "\uFB8E", + "/keheh.fina": "\uFB8F", + "/keheh.init": "\uFB90", + "/keheh.medi": "\uFB91", + "/gaf.isol": "\uFB92", + "/gaf.fina": "\uFB93", + "/gaf.init": "\uFB94", + "/gaf.medi": "\uFB95", + "/gueh.isol": "\uFB96", + "/gueh.fina": "\uFB97", + "/gueh.init": "\uFB98", + "/gueh.medi": "\uFB99", + "/ngoeh.isol": "\uFB9A", + "/ngoeh.fina": "\uFB9B", + "/ngoeh.init": "\uFB9C", + "/ngoeh.medi": "\uFB9D", + "/noonghunna.isol": "\uFB9E", + "/noonghunna.fina": "\uFB9F", + "/rnoon.isol": "\uFBA0", + "/rnoon.fina": "\uFBA1", + "/rnoon.init": "\uFBA2", + "/rnoon.medi": "\uFBA3", + "/hehyeh.isol": "\uFBA4", + "/hehyeh.fina": "\uFBA5", + "/hehgoal.isol": "\uFBA6", + "/hehgoal.fina": "\uFBA7", + "/hehgoal.init": "\uFBA8", + "/hehgoal.medi": "\uFBA9", + "/hehdoachashmee.isol": "\uFBAA", + "/hehdoachashmee.fina": "\uFBAB", + "/hehdoachashmee.init": "\uFBAC", + "/hehdoachashmee.medi": "\uFBAD", + "/yehbarree.isol": "\uFBAE", + "/yehbarree.fina": "\uFBAF", + "/yehbarreehamza.isol": "\uFBB0", + "/yehbarreehamza.fina": "\uFBB1", + "/symboldotabove": "\uFBB2", + "/symboldotbelow": "\uFBB3", + "/symboltwodotsabove": "\uFBB4", + "/symboltwodotsbelow": "\uFBB5", + "/symbolabovethreedotsabove": "\uFBB6", + "/symbolbelowthreedotsabove": "\uFBB7", + "/symbolpointingabovedownthreedotsabove": "\uFBB8", + "/symbolpointingbelowdownthreedotsabove": "\uFBB9", + "/symbolfourdotsabove": "\uFBBA", + "/symbolfourdotsbelow": "\uFBBB", + "/symboldoubleverticalbarbelow": "\uFBBC", + "/symboltwodotsverticallyabove": "\uFBBD", + "/symboltwodotsverticallybelow": "\uFBBE", + "/symbolring": "\uFBBF", + "/symboltahabovesmall": "\uFBC0", + "/symboltahbelowsmall": "\uFBC1", + "/symbolabovewasla": "\uFBC2", + "/ng.isol": "\uFBD3", + "/ng.fina": "\uFBD4", + "/ng.init": "\uFBD5", + "/ng.medi": "\uFBD6", + "/u.isol": "\uFBD7", + "/u.fina": "\uFBD8", + "/oe.isol": "\uFBD9", + "/oe.fina": "\uFBDA", + "/yu.isol": "\uFBDB", + "/yu.fina": "\uFBDC", + "/uhamza.isol": "\uFBDD", + "/ve.isol": "\uFBDE", + "/ve.fina": "\uFBDF", + "/oekirghiz.isol": "\uFBE0", + "/oekirghiz.fina": "\uFBE1", + "/yukirghiz.isol": "\uFBE2", + "/yukirghiz.fina": "\uFBE3", + "/e.isol": "\uFBE4", + "/e.fina": "\uFBE5", + "/e.init": "\uFBE6", + "/e.medi": "\uFBE7", + "/uighurkazakhkirghizalefmaksura.init": "\uFBE8", + "/uighurkazakhkirghizalefmaksura.medi": "\uFBE9", + "/yeh.init_hamzaabove.medi_alef.fina": "\uFBEA", + "/yeh.medi_hamzaabove.medi_alef.fina": "\uFBEB", + "/yeh.init_hamzaabove.medi_ae.fina": "\uFBEC", + "/yeh.medi_hamzaabove.medi_ae.fina": "\uFBED", + "/yeh.init_hamzaabove.medi_waw.fina": "\uFBEE", + "/yeh.medi_hamzaabove.medi_waw.fina": "\uFBEF", + "/yeh.init_hamzaabove.medi_u.fina": "\uFBF0", + "/yeh.medi_hamzaabove.medi_u.fina": "\uFBF1", + "/yeh.init_hamzaabove.medi_oe.fina": "\uFBF2", + "/yeh.medi_hamzaabove.medi_oe.fina": "\uFBF3", + "/yeh.init_hamzaabove.medi_yu.fina": "\uFBF4", + "/yeh.medi_hamzaabove.medi_yu.fina": "\uFBF5", + "/yeh.init_hamzaabove.medi_e.fina": "\uFBF6", + "/yeh.medi_hamzaabove.medi_e.fina": "\uFBF7", + "/yeh.init_hamzaabove.medi_e.medi": "\uFBF8", + "/uighurkirghizyeh.init_hamzaabove.medi_alefmaksura.fina": "\uFBF9", + "/uighurkirghizyeh.medi_hamzaabove.medi_alefmaksura.fina": "\uFBFA", + "/uighurkirghizyeh.init_hamzaabove.medi_alefmaksura.medi": "\uFBFB", + "/yehfarsi.isol": "\uFBFC", + "/yehfarsi.fina": "\uFBFD", + "/yehfarsi.init": "\uFBFE", + "/yehfarsi.medi": "\uFBFF", + "/yeh.init_hamzaabove.medi_jeem.fina": "\uFC00", + "/yeh.init_hamzaabove.medi_hah.fina": "\uFC01", + "/yeh.init_hamzaabove.medi_meem.fina": "\uFC02", + "/yeh.init_hamzaabove.medi_alefmaksura.fina": "\uFC03", + "/yeh.init_hamzaabove.medi_yeh.fina": "\uFC04", + "/beh.init_jeem.fina": "\uFC05", + "/beh.init_hah.fina": "\uFC06", + "/beh.init_khah.fina": "\uFC07", + "/beh.init_meem.fina": "\uFC08", + "/beh.init_alefmaksura.fina": "\uFC09", + "/beh.init_yeh.fina": "\uFC0A", + "/teh.init_jeem.fina": "\uFC0B", + "/teh.init_hah.fina": "\uFC0C", + "/teh.init_khah.fina": "\uFC0D", + "/teh.init_meem.fina": "\uFC0E", + "/teh.init_alefmaksura.fina": "\uFC0F", + "/teh.init_yeh.fina": "\uFC10", + "/theh.init_jeem.fina": "\uFC11", + "/theh.init_meem.fina": "\uFC12", + "/theh.init_alefmaksura.fina": "\uFC13", + "/theh.init_yeh.fina": "\uFC14", + "/jeem.init_hah.fina": "\uFC15", + "/jeem.init_meem.fina": "\uFC16", + "/hah.init_jeem.fina": "\uFC17", + "/hah.init_meem.fina": "\uFC18", + "/khah.init_jeem.fina": "\uFC19", + "/khah.init_hah.fina": "\uFC1A", + "/khah.init_meem.fina": "\uFC1B", + "/seen.init_jeem.fina": "\uFC1C", + "/seen.init_hah.fina": "\uFC1D", + "/seen.init_khah.fina": "\uFC1E", + "/seen.init_meem.fina": "\uFC1F", + "/sad.init_hah.fina": "\uFC20", + "/sad.init_meem.fina": "\uFC21", + "/dad.init_jeem.fina": "\uFC22", + "/dad.init_hah.fina": "\uFC23", + "/dad.init_khah.fina": "\uFC24", + "/dad.init_meem.fina": "\uFC25", + "/tah.init_hah.fina": "\uFC26", + "/tah.init_meem.fina": "\uFC27", + "/zah.init_meem.fina": "\uFC28", + "/ain.init_jeem.fina": "\uFC29", + "/ain.init_meem.fina": "\uFC2A", + "/ghain.init_jeem.fina": "\uFC2B", + "/ghain.init_meem.fina": "\uFC2C", + "/feh.init_jeem.fina": "\uFC2D", + "/feh.init_hah.fina": "\uFC2E", + "/feh.init_khah.fina": "\uFC2F", + "/feh.init_meem.fina": "\uFC30", + "/feh.init_alefmaksura.fina": "\uFC31", + "/feh.init_yeh.fina": "\uFC32", + "/qaf.init_hah.fina": "\uFC33", + "/qaf.init_meem.fina": "\uFC34", + "/qaf.init_alefmaksura.fina": "\uFC35", + "/qaf.init_yeh.fina": "\uFC36", + "/kaf.init_alef.fina": "\uFC37", + "/kaf.init_jeem.fina": "\uFC38", + "/kaf.init_hah.fina": "\uFC39", + "/kaf.init_khah.fina": "\uFC3A", + "/kaf.init_lam.fina": "\uFC3B", + "/kaf.init_meem.fina": "\uFC3C", + "/kaf.init_alefmaksura.fina": "\uFC3D", + "/kaf.init_yeh.fina": "\uFC3E", + "/lam.init_jeem.fina": "\uFC3F", + "/lam.init_hah.fina": "\uFC40", + "/lam.init_khah.fina": "\uFC41", + "/lam.init_meem.fina": "\uFC42", + "/lam.init_alefmaksura.fina": "\uFC43", + "/lam.init_yeh.fina": "\uFC44", + "/meem.init_jeem.fina": "\uFC45", + "/meem.init_hah.fina": "\uFC46", + "/meem.init_khah.fina": "\uFC47", + "/meem.init_meem.fina": "\uFC48", + "/meem.init_alefmaksura.fina": "\uFC49", + "/meem.init_yeh.fina": "\uFC4A", + "/noon.init_jeem.fina": "\uFC4B", + "/noon.init_hah.fina": "\uFC4C", + "/noon.init_khah.fina": "\uFC4D", + "/noon.init_meem.fina": "\uFC4E", + "/noon.init_alefmaksura.fina": "\uFC4F", + "/noon.init_yeh.fina": "\uFC50", + "/heh.init_jeem.fina": "\uFC51", + "/heh.init_meem.fina": "\uFC52", + "/heh.init_alefmaksura.fina": "\uFC53", + "/heh.init_yeh.fina": "\uFC54", + "/yeh.init_jeem.fina": "\uFC55", + "/yeh.init_hah.fina": "\uFC56", + "/yeh.init_khah.fina": "\uFC57", + "/yeh.init_meem.fina": "\uFC58", + "/yeh.init_alefmaksura.fina": "\uFC59", + "/yeh.init_yeh.fina": "\uFC5A", + "/thal.init_superscriptalef.fina": "\uFC5B", + "/reh.init_superscriptalef.fina": "\uFC5C", + "/alefmaksura.init_superscriptalef.fina": "\uFC5D", + "/shaddaDammatanIsol": "\uFC5E", + "/shaddaKasratanIsol": "\uFC5F", + "/shaddaFathaIsol": "\uFC60", + "/shaddaDammaIsol": "\uFC61", + "/shaddaKasraIsol": "\uFC62", + "/shaddaAlefIsol": "\uFC63", + "/yeh.medi_hamzaabove.medi_reh.fina": "\uFC64", + "/yeh.medi_hamzaabove.medi_zain.fina": "\uFC65", + "/yeh.medi_hamzaabove.medi_meem.fina": "\uFC66", + "/yeh.medi_hamzaabove.medi_noon.fina": "\uFC67", + "/yeh.medi_hamzaabove.medi_alefmaksura.fina": "\uFC68", + "/yeh.medi_hamzaabove.medi_yeh.fina": "\uFC69", + "/beh.medi_reh.fina": "\uFC6A", + "/beh.medi_zain.fina": "\uFC6B", + "/beh.medi_meem.fina": "\uFC6C", + "/beh.medi_noon.fina": "\uFC6D", + "/beh.medi_alefmaksura.fina": "\uFC6E", + "/beh.medi_yeh.fina": "\uFC6F", + "/teh.medi_reh.fina": "\uFC70", + "/teh.medi_zain.fina": "\uFC71", + "/teh.medi_meem.fina": "\uFC72", + "/teh.medi_noon.fina": "\uFC73", + "/teh.medi_alefmaksura.fina": "\uFC74", + "/teh.medi_yeh.fina": "\uFC75", + "/theh.medi_reh.fina": "\uFC76", + "/theh.medi_zain.fina": "\uFC77", + "/theh.medi_meem.fina": "\uFC78", + "/theh.medi_noon.fina": "\uFC79", + "/theh.medi_alefmaksura.fina": "\uFC7A", + "/theh.medi_yeh.fina": "\uFC7B", + "/feh.medi_alefmaksura.fina": "\uFC7C", + "/feh.medi_yeh.fina": "\uFC7D", + "/qaf.medi_alefmaksura.fina": "\uFC7E", + "/qaf.medi_yeh.fina": "\uFC7F", + "/kaf.medi_alef.fina": "\uFC80", + "/kaf.medi_lam.fina": "\uFC81", + "/kaf.medi_meem.fina": "\uFC82", + "/kaf.medi_alefmaksura.fina": "\uFC83", + "/kaf.medi_yeh.fina": "\uFC84", + "/lam.medi_meem.fina": "\uFC85", + "/lam.medi_alefmaksura.fina": "\uFC86", + "/lam.medi_yeh.fina": "\uFC87", + "/meem.medi_alef.fina": "\uFC88", + "/meem.medi_meem.fina": "\uFC89", + "/noon.medi_reh.fina": "\uFC8A", + "/noon.medi_zain.fina": "\uFC8B", + "/noon.medi_meem.fina": "\uFC8C", + "/noon.medi_noon.fina": "\uFC8D", + "/noon.medi_alefmaksura.fina": "\uFC8E", + "/noon.medi_yeh.fina": "\uFC8F", + "/alefmaksura.medi_superscriptalef.fina": "\uFC90", + "/yeh.medi_reh.fina": "\uFC91", + "/yeh.medi_zain.fina": "\uFC92", + "/yeh.medi_meem.fina": "\uFC93", + "/yeh.medi_noon.fina": "\uFC94", + "/yeh.medi_alefmaksura.fina": "\uFC95", + "/yeh.medi_yeh.fina": "\uFC96", + "/yeh.init_hamzaabove.medi_jeem.medi": "\uFC97", + "/yeh.init_hamzaabove.medi_hah.medi": "\uFC98", + "/yeh.init_hamzaabove.medi_khah.medi": "\uFC99", + "/yeh.init_hamzaabove.medi_meem.medi": "\uFC9A", + "/yeh.init_hamzaabove.medi_heh.medi": "\uFC9B", + "/beh.init_jeem.medi": "\uFC9C", + "/beh.init_hah.medi": "\uFC9D", + "/beh.init_khah.medi": "\uFC9E", + "/beh.init_meem.medi": "\uFC9F", + "/beh.init_heh.medi": "\uFCA0", + "/teh.init_jeem.medi": "\uFCA1", + "/teh.init_hah.medi": "\uFCA2", + "/teh.init_khah.medi": "\uFCA3", + "/teh.init_meem.medi": "\uFCA4", + "/teh.init_heh.medi": "\uFCA5", + "/theh.init_meem.medi": "\uFCA6", + "/jeem.init_hah.medi": "\uFCA7", + "/jeem.init_meem.medi": "\uFCA8", + "/hah.init_jeem.medi": "\uFCA9", + "/hah.init_meem.medi": "\uFCAA", + "/khah.init_jeem.medi": "\uFCAB", + "/khah.init_meem.medi": "\uFCAC", + "/seen.init_jeem.medi": "\uFCAD", + "/seen.init_hah.medi": "\uFCAE", + "/seen.init_khah.medi": "\uFCAF", + "/seen.init_meem.medi": "\uFCB0", + "/sad.init_hah.medi": "\uFCB1", + "/sad.init_khah.medi": "\uFCB2", + "/sad.init_meem.medi": "\uFCB3", + "/dad.init_jeem.medi": "\uFCB4", + "/dad.init_hah.medi": "\uFCB5", + "/dad.init_khah.medi": "\uFCB6", + "/dad.init_meem.medi": "\uFCB7", + "/tah.init_hah.medi": "\uFCB8", + "/zah.init_meem.medi": "\uFCB9", + "/ain.init_jeem.medi": "\uFCBA", + "/ain.init_meem.medi": "\uFCBB", + "/ghain.init_jeem.medi": "\uFCBC", + "/ghain.init_meem.medi": "\uFCBD", + "/feh.init_jeem.medi": "\uFCBE", + "/feh.init_hah.medi": "\uFCBF", + "/feh.init_khah.medi": "\uFCC0", + "/feh.init_meem.medi": "\uFCC1", + "/qaf.init_hah.medi": "\uFCC2", + "/qaf.init_meem.medi": "\uFCC3", + "/kaf.init_jeem.medi": "\uFCC4", + "/kaf.init_hah.medi": "\uFCC5", + "/kaf.init_khah.medi": "\uFCC6", + "/kaf.init_lam.medi": "\uFCC7", + "/kaf.init_meem.medi": "\uFCC8", + "/lam.init_jeem.medi": "\uFCC9", + "/lam.init_hah.medi": "\uFCCA", + "/lam.init_khah.medi": "\uFCCB", + "/lam.init_meem.medi": "\uFCCC", + "/lam.init_heh.medi": "\uFCCD", + "/meem.init_jeem.medi": "\uFCCE", + "/meem.init_hah.medi": "\uFCCF", + "/meem.init_khah.medi": "\uFCD0", + "/meem.init_meem.medi": "\uFCD1", + "/noon.init_jeem.medi": "\uFCD2", + "/noon.init_hah.medi": "\uFCD3", + "/noon.init_khah.medi": "\uFCD4", + "/noon.init_meem.medi": "\uFCD5", + "/noon.init_heh.medi": "\uFCD6", + "/heh.init_jeem.medi": "\uFCD7", + "/heh.init_meem.medi": "\uFCD8", + "/heh.init_superscriptalef.medi": "\uFCD9", + "/yeh.init_jeem.medi": "\uFCDA", + "/yeh.init_hah.medi": "\uFCDB", + "/yeh.init_khah.medi": "\uFCDC", + "/yeh.init_meem.medi": "\uFCDD", + "/yeh.init_heh.medi": "\uFCDE", + "/yeh.medi_hamzaabove.medi_meem.medi": "\uFCDF", + "/yeh.medi_hamzaabove.medi_heh.medi": "\uFCE0", + "/beh.medi_meem.medi": "\uFCE1", + "/beh.medi_heh.medi": "\uFCE2", + "/teh.medi_meem.medi": "\uFCE3", + "/teh.medi_heh.medi": "\uFCE4", + "/theh.medi_meem.medi": "\uFCE5", + "/theh.medi_heh.medi": "\uFCE6", + "/seen.medi_meem.medi": "\uFCE7", + "/seen.medi_heh.medi": "\uFCE8", + "/sheen.medi_meem.medi": "\uFCE9", + "/sheen.medi_heh.medi": "\uFCEA", + "/kaf.medi_lam.medi": "\uFCEB", + "/kaf.medi_meem.medi": "\uFCEC", + "/lam.medi_meem.medi": "\uFCED", + "/noon.medi_meem.medi": "\uFCEE", + "/noon.medi_heh.medi": "\uFCEF", + "/yeh.medi_meem.medi": "\uFCF0", + "/yeh.medi_heh.medi": "\uFCF1", + "/shaddaFathaMedi": "\uFCF2", + "/shaddaDammaMedi": "\uFCF3", + "/shaddaKasraMedi": "\uFCF4", + "/tah.init_alefmaksura.fina": "\uFCF5", + "/tah.init_yeh.fina": "\uFCF6", + "/ain.init_alefmaksura.fina": "\uFCF7", + "/ain.init_yeh.fina": "\uFCF8", + "/ghain.init_alefmaksura.fina": "\uFCF9", + "/ghain.init_yeh.fina": "\uFCFA", + "/seen.init_alefmaksura.fina": "\uFCFB", + "/seen.init_yeh.fina": "\uFCFC", + "/sheen.init_alefmaksura.fina": "\uFCFD", + "/sheen.init_yeh.fina": "\uFCFE", + "/hah.init_alefmaksura.fina": "\uFCFF", + "/hah.init_yeh.fina": "\uFD00", + "/jeem.init_alefmaksura.fina": "\uFD01", + "/jeem.init_yeh.fina": "\uFD02", + "/khah.init_alefmaksura.fina": "\uFD03", + "/khah.init_yeh.fina": "\uFD04", + "/sad.init_alefmaksura.fina": "\uFD05", + "/sad.init_yeh.fina": "\uFD06", + "/dad.init_alefmaksura.fina": "\uFD07", + "/dad.init_yeh.fina": "\uFD08", + "/sheen.init_jeem.fina": "\uFD09", + "/sheen.init_hah.fina": "\uFD0A", + "/sheen.init_khah.fina": "\uFD0B", + "/sheen.init_meem.fina": "\uFD0C", + "/sheen.init_reh.fina": "\uFD0D", + "/seen.init_reh.fina": "\uFD0E", + "/sad.init_reh.fina": "\uFD0F", + "/dad.init_reh.fina": "\uFD10", + "/tah.medi_alefmaksura.fina": "\uFD11", + "/tah.medi_yeh.fina": "\uFD12", + "/ain.medi_alefmaksura.fina": "\uFD13", + "/ain.medi_yeh.fina": "\uFD14", + "/ghain.medi_alefmaksura.fina": "\uFD15", + "/ghain.medi_yeh.fina": "\uFD16", + "/seen.medi_alefmaksura.fina": "\uFD17", + "/seen.medi_yeh.fina": "\uFD18", + "/sheen.medi_alefmaksura.fina": "\uFD19", + "/sheen.medi_yeh.fina": "\uFD1A", + "/hah.medi_alefmaksura.fina": "\uFD1B", + "/hah.medi_yeh.fina": "\uFD1C", + "/jeem.medi_alefmaksura.fina": "\uFD1D", + "/jeem.medi_yeh.fina": "\uFD1E", + "/khah.medi_alefmaksura.fina": "\uFD1F", + "/khah.medi_yeh.fina": "\uFD20", + "/sad.medi_alefmaksura.fina": "\uFD21", + "/sad.medi_yeh.fina": "\uFD22", + "/dad.medi_alefmaksura.fina": "\uFD23", + "/dad.medi_yeh.fina": "\uFD24", + "/sheen.medi_jeem.fina": "\uFD25", + "/sheen.medi_hah.fina": "\uFD26", + "/sheen.medi_khah.fina": "\uFD27", + "/sheen.medi_meem.fina": "\uFD28", + "/sheen.medi_reh.fina": "\uFD29", + "/seen.medi_reh.fina": "\uFD2A", + "/sad.medi_reh.fina": "\uFD2B", + "/dad.medi_reh.fina": "\uFD2C", + "/sheen.init_jeem.medi": "\uFD2D", + "/sheen.init_hah.medi": "\uFD2E", + "/sheen.init_khah.medi": "\uFD2F", + "/sheen.init_meem.medi": "\uFD30", + "/seen.init_heh.medi": "\uFD31", + "/sheen.init_heh.medi": "\uFD32", + "/tah.init_meem.medi": "\uFD33", + "/seen.medi_jeem.medi": "\uFD34", + "/seen.medi_hah.medi": "\uFD35", + "/seen.medi_khah.medi": "\uFD36", + "/sheen.medi_jeem.medi": "\uFD37", + "/sheen.medi_hah.medi": "\uFD38", + "/sheen.medi_khah.medi": "\uFD39", + "/tah.medi_meem.medi": "\uFD3A", + "/zah.medi_meem.medi": "\uFD3B", + "/alef.medi_fathatan.fina": "\uFD3C", + "/alef.init_fathatan.fina": "\uFD3D", + "/ornateleftparenthesis": "\uFD3E", + "/ornaterightparenthesis": "\uFD3F", + "/RahimahuAllaah": "\uFD40", + "/RadiAllaahuAnh": "\uFD41", + "/RadiAllaahuAnhaa": "\uFD42", + "/RadiAllaahuAnhum": "\uFD43", + "/RadiAllaahuAnhumaa": "\uFD44", + "/RadiAllaahuAnhunna": "\uFD45", + "/SallallaahuAlayhiWaAalih": "\uFD46", + "/AlayhiAsSalaam": "\uFD47", + "/AlayhimAsSalaam": "\uFD48", + "/AlayhimaaAsSalaam": "\uFD49", + "/AlayhiAsSalaatuWasSalaam": "\uFD4A", + "/QuddisaSirrah": "\uFD4B", + "/SallallahuAlayhiWaaaliheeWaSallam": "\uFD4C", + "/AlayhaaAsSalaam": "\uFD4D", + "/TabaarakaWaTaaalaa": "\uFD4E", + "/RahimahumAllaah": "\uFD4F", + "/teh.init_jeem.medi_meem.medi": "\uFD50", + "/teh.medi_hah.medi_jeem.fina": "\uFD51", + "/teh.init_hah.medi_jeem.medi": "\uFD52", + "/teh.init_hah.medi_meem.medi": "\uFD53", + "/teh.init_khah.medi_meem.medi": "\uFD54", + "/teh.init_meem.medi_jeem.medi": "\uFD55", + "/teh.init_meem.medi_hah.medi": "\uFD56", + "/teh.init_meem.medi_khah.medi": "\uFD57", + "/jeem.medi_meem.medi_hah.fina": "\uFD58", + "/jeem.init_meem.medi_hah.medi": "\uFD59", + "/hah.medi_meem.medi_yeh.fina": "\uFD5A", + "/hah.medi_meem.medi_alefmaksura.fina": "\uFD5B", + "/seen.init_hah.medi_jeem.medi": "\uFD5C", + "/seen.init_jeem.medi_hah.medi": "\uFD5D", + "/seen.medi_jeem.medi_alefmaksura.fina": "\uFD5E", + "/seen.medi_meem.medi_hah.fina": "\uFD5F", + "/seen.init_meem.medi_hah.medi": "\uFD60", + "/seen.init_meem.medi_jeem.medi": "\uFD61", + "/seen.medi_meem.medi_meem.fina": "\uFD62", + "/seen.init_meem.medi_meem.medi": "\uFD63", + "/sad.medi_hah.medi_hah.fina": "\uFD64", + "/sad.init_hah.medi_hah.medi": "\uFD65", + "/sad.medi_meem.medi_meem.fina": "\uFD66", + "/sheen.medi_hah.medi_meem.fina": "\uFD67", + "/sheen.init_hah.medi_meem.medi": "\uFD68", + "/sheen.medi_jeem.medi_yeh.fina": "\uFD69", + "/sheen.medi_meem.medi_khah.fina": "\uFD6A", + "/sheen.init_meem.medi_khah.medi": "\uFD6B", + "/sheen.medi_meem.medi_meem.fina": "\uFD6C", + "/sheen.init_meem.medi_meem.medi": "\uFD6D", + "/dad.medi_hah.medi_alefmaksura.fina": "\uFD6E", + "/dad.medi_khah.medi_meem.fina": "\uFD6F", + "/dad.init_khah.medi_meem.medi": "\uFD70", + "/tah.medi_meem.medi_hah.fina": "\uFD71", + "/tah.init_meem.medi_hah.medi": "\uFD72", + "/tah.init_meem.medi_meem.medi": "\uFD73", + "/tah.medi_meem.medi_yeh.fina": "\uFD74", + "/ain.medi_jeem.medi_meem.fina": "\uFD75", + "/ain.medi_meem.medi_meem.fina": "\uFD76", + "/ain.init_meem.medi_meem.medi": "\uFD77", + "/ain.medi_meem.medi_alefmaksura.fina": "\uFD78", + "/ghain.medi_meem.medi_meem.fina": "\uFD79", + "/ghain.medi_meem.medi_yeh.fina": "\uFD7A", + "/ghain.medi_meem.medi_alefmaksura.fina": "\uFD7B", + "/feh.medi_khah.medi_meem.fina": "\uFD7C", + "/feh.init_khah.medi_meem.medi": "\uFD7D", + "/qaf.medi_meem.medi_hah.fina": "\uFD7E", + "/qaf.medi_meem.medi_meem.fina": "\uFD7F", + "/lam.medi_hah.medi_meem.fina": "\uFD80", + "/lam.medi_hah.medi_yeh.fina": "\uFD81", + "/lam.medi_hah.medi_alefmaksura.fina": "\uFD82", + "/lam.init_jeem.medi_jeem.medi": "\uFD83", + "/lam.medi_jeem.medi_jeem.fina": "\uFD84", + "/lam.medi_khah.medi_meem.fina": "\uFD85", + "/lam.init_khah.medi_meem.medi": "\uFD86", + "/lam.medi_meem.medi_hah.fina": "\uFD87", + "/lam.init_meem.medi_hah.medi": "\uFD88", + "/meem.init_hah.medi_jeem.medi": "\uFD89", + "/meem.init_hah.medi_meem.medi": "\uFD8A", + "/meem.medi_hah.medi_yeh.fina": "\uFD8B", + "/meem.init_jeem.medi_hah.medi": "\uFD8C", + "/meem.init_jeem.medi_meem.medi": "\uFD8D", + "/meem.init_khah.medi_jeem.medi": "\uFD8E", + "/meem.init_khah.medi_meem.medi": "\uFD8F", + "/meem.init_jeem.medi_khah.medi": "\uFD92", + "/heh.init_meem.medi_jeem.medi": "\uFD93", + "/heh.init_meem.medi_meem.medi": "\uFD94", + "/noon.init_hah.medi_meem.medi": "\uFD95", + "/noon.medi_hah.medi_alefmaksura.fina": "\uFD96", + "/noon.medi_jeem.medi_meem.fina": "\uFD97", + "/noon.init_jeem.medi_meem.medi": "\uFD98", + "/noon.medi_jeem.medi_alefmaksura.fina": "\uFD99", + "/noon.medi_meem.medi_yeh.fina": "\uFD9A", + "/noon.medi_meem.medi_alefmaksura.fina": "\uFD9B", + "/yeh.medi_meem.medi_meem.fina": "\uFD9C", + "/yeh.init_meem.medi_meem.medi": "\uFD9D", + "/beh.medi_khah.medi_yeh.fina": "\uFD9E", + "/teh.medi_jeem.medi_yeh.fina": "\uFD9F", + "/teh.medi_jeem.medi_alefmaksura.fina": "\uFDA0", + "/teh.medi_khah.medi_yeh.fina": "\uFDA1", + "/teh.medi_khah.medi_alefmaksura.fina": "\uFDA2", + "/teh.medi_meem.medi_yeh.fina": "\uFDA3", + "/teh.medi_meem.medi_alefmaksura.fina": "\uFDA4", + "/jeem.medi_meem.medi_yeh.fina": "\uFDA5", + "/jeem.medi_hah.medi_alefmaksura.fina": "\uFDA6", + "/jeem.medi_meem.medi_alefmaksura.fina": "\uFDA7", + "/seen.medi_khah.medi_alefmaksura.fina": "\uFDA8", + "/sad.medi_hah.medi_yeh.fina": "\uFDA9", + "/sheen.medi_hah.medi_yeh.fina": "\uFDAA", + "/dad.medi_hah.medi_yeh.fina": "\uFDAB", + "/lam.medi_jeem.medi_yeh.fina": "\uFDAC", + "/lam.medi_meem.medi_yeh.fina": "\uFDAD", + "/yeh.medi_hah.medi_yeh.fina": "\uFDAE", + "/yeh.medi_jeem.medi_yeh.fina": "\uFDAF", + "/yeh.medi_meem.medi_yeh.fina": "\uFDB0", + "/meem.medi_meem.medi_yeh.fina": "\uFDB1", + "/qaf.medi_meem.medi_yeh.fina": "\uFDB2", + "/noon.medi_hah.medi_yeh.fina": "\uFDB3", + "/qaf.init_meem.medi_hah.medi": "\uFDB4", + "/lam.init_hah.medi_meem.medi": "\uFDB5", + "/ain.medi_meem.medi_yeh.fina": "\uFDB6", + "/kaf.medi_meem.medi_yeh.fina": "\uFDB7", + "/noon.init_jeem.medi_hah.medi": "\uFDB8", + "/meem.medi_khah.medi_yeh.fina": "\uFDB9", + "/lam.init_jeem.medi_meem.medi": "\uFDBA", + "/kaf.medi_meem.medi_meem.fina": "\uFDBB", + "/lam.medi_jeem.medi_meem.fina": "\uFDBC", + "/noon.medi_jeem.medi_hah.fina": "\uFDBD", + "/jeem.medi_hah.medi_yeh.fina": "\uFDBE", + "/hah.medi_jeem.medi_yeh.fina": "\uFDBF", + "/meem.medi_jeem.medi_yeh.fina": "\uFDC0", + "/feh.medi_meem.medi_yeh.fina": "\uFDC1", + "/beh.medi_hah.medi_yeh.fina": "\uFDC2", + "/kaf.init_meem.medi_meem.medi": "\uFDC3", + "/ain.init_jeem.medi_meem.medi": "\uFDC4", + "/sad.init_meem.medi_meem.medi": "\uFDC5", + "/seen.medi_khah.medi_yeh.fina": "\uFDC6", + "/noon.medi_jeem.medi_yeh.fina": "\uFDC7", + "/SalaamuhuAlaynaa": "\uFDCF", + "/SallaUsedAsKoranicStopSign": "\uFDF0", + "/QalaUsedAsKoranicStopSign": "\uFDF1", + "/Allah": "\uFDF2", + "/Akbar": "\uFDF3", + "/Mohammad": "\uFDF4", + "/Salam": "\uFDF5", + "/Rasoul": "\uFDF6", + "/Alayhe": "\uFDF7", + "/Wasallam": "\uFDF8", + "/Salla": "\uFDF9", + "/SallallahouAlayheWasallam": "\uFDFA", + "/Jallajalalouhou": "\uFDFB", + "/rial": "\uFDFC", + "/BismillahArRahmanArRaheem": "\uFDFD", + "/SubhaanahuWaTaaalaa": "\uFDFE", + "/AzzaWaJall": "\uFDFF", + "/vert:comma": "\uFE10", + "/vert:ideographiccomma": "\uFE11", + "/vert:ideographicfullstop": "\uFE12", + "/vert:colon": "\uFE13", + "/vert:semicolon": "\uFE14", + "/vert:exclam": "\uFE15", + "/vert:question": "\uFE16", + "/vert:bracketwhiteleft": "\uFE17", + "/vert:brakcetwhiteright": "\uFE18", + "/vert:ellipsishor": "\uFE19", + "/underscorewavyvertical": "\uFE34", + "/sesamedot": "\uFE45", + "/whitesesamedot": "\uFE46", + "/squarebracketleftvertical": "\uFE47", + "/squarebracketrightvertical": "\uFE48", + "/underscoredashed": "\uFE4D", + "/underscorecenterline": "\uFE4E", + "/ideographiccommasmall": "\uFE51", + "/emdashsmall": "\uFE58", + "/parenthesisleftsmall": "\uFE59", + "/parenthesisrightsmall": "\uFE5A", + "/backslashsmall": "\uFE68", + "/commercialatsmall": "\uFE6B", + "/fathatanIsol": "\uFE70", + "/tatweelFathatanAbove": "\uFE71", + "/dammatanIsol": "\uFE72", + "/kashidaFina": "\uFE73", + "/kasratanIsol": "\uFE74", + "/fathaIsol": "\uFE76", + "/fathaMedi": "\uFE77", + "/dammaIsol": "\uFE78", + "/dammaMedi": "\uFE79", + "/kasraIsol": "\uFE7A", + "/kasraMedi": "\uFE7B", + "/shaddaIsol": "\uFE7C", + "/shaddaMedi": "\uFE7D", + "/sukunIsol": "\uFE7E", + "/sukunMedi": "\uFE7F", + "/hamzaIsol": "\uFE80", + "/alefmadda.isol": "\uFE81", + "/alefmadda.fina": "\uFE82", + "/alefhamza.isol": "\uFE83", + "/alefhamza.fina": "\uFE84", + "/wawhamza.isol": "\uFE85", + "/wawhamza.fina": "\uFE86", + "/alefhamzabelow.isol": "\uFE87", + "/alefhamzabelow.fina": "\uFE88", + "/yehhamza.isol": "\uFE89", + "/yehhamza.fina": "\uFE8A", + "/yehhamza.init": "\uFE8B", + "/yehhamza.medi": "\uFE8C", + "/alef.isol": "\uFE8D", + "/alef.fina": "\uFE8E", + "/beh.isol": "\uFE8F", + "/beh.fina": "\uFE90", + "/beh.init": "\uFE91", + "/beh.medi": "\uFE92", + "/tehmarbuta.isol": "\uFE93", + "/tehmarbuta.fina": "\uFE94", + "/teh.isol": "\uFE95", + "/teh.fina": "\uFE96", + "/teh.init": "\uFE97", + "/teh.medi": "\uFE98", + "/theh.isol": "\uFE99", + "/theh.fina": "\uFE9A", + "/theh.init": "\uFE9B", + "/theh.medi": "\uFE9C", + "/jeem.isol": "\uFE9D", + "/jeem.fina": "\uFE9E", + "/jeem.init": "\uFE9F", + "/jeem.medi": "\uFEA0", + "/hah.isol": "\uFEA1", + "/hah.fina": "\uFEA2", + "/hah.init": "\uFEA3", + "/hah.medi": "\uFEA4", + "/khah.isol": "\uFEA5", + "/khah.fina": "\uFEA6", + "/khah.init": "\uFEA7", + "/khah.medi": "\uFEA8", + "/dal.isol": "\uFEA9", + "/dal.fina": "\uFEAA", + "/thal.isol": "\uFEAB", + "/thal.fina": "\uFEAC", + "/reh.isol": "\uFEAD", + "/reh.fina": "\uFEAE", + "/zain.isol": "\uFEAF", + "/zain.fina": "\uFEB0", + "/seen.isol": "\uFEB1", + "/seen.fina": "\uFEB2", + "/seen.init": "\uFEB3", + "/seen.medi": "\uFEB4", + "/sheen.isol": "\uFEB5", + "/sheen.fina": "\uFEB6", + "/sheen.init": "\uFEB7", + "/sheen.medi": "\uFEB8", + "/sad.isol": "\uFEB9", + "/sad.fina": "\uFEBA", + "/sad.init": "\uFEBB", + "/sad.medi": "\uFEBC", + "/dad.isol": "\uFEBD", + "/dad.fina": "\uFEBE", + "/dad.init": "\uFEBF", + "/dad.medi": "\uFEC0", + "/tah.isol": "\uFEC1", + "/tah.fina": "\uFEC2", + "/tah.init": "\uFEC3", + "/tah.medi": "\uFEC4", + "/zah.isol": "\uFEC5", + "/zah.fina": "\uFEC6", + "/zah.init": "\uFEC7", + "/zah.medi": "\uFEC8", + "/ain.isol": "\uFEC9", + "/ain.fina": "\uFECA", + "/ain.init": "\uFECB", + "/ain.medi": "\uFECC", + "/ghain.isol": "\uFECD", + "/ghain.fina": "\uFECE", + "/ghain.init": "\uFECF", + "/ghain.medi": "\uFED0", + "/feh.isol": "\uFED1", + "/feh.fina": "\uFED2", + "/feh.init": "\uFED3", + "/feh.medi": "\uFED4", + "/qaf.isol": "\uFED5", + "/qaf.fina": "\uFED6", + "/qaf.init": "\uFED7", + "/qaf.medi": "\uFED8", + "/kaf.isol": "\uFED9", + "/kaf.fina": "\uFEDA", + "/kaf.init": "\uFEDB", + "/kaf.medi": "\uFEDC", + "/lam.isol": "\uFEDD", + "/lam.fina": "\uFEDE", + "/lam.init": "\uFEDF", + "/lam.medi": "\uFEE0", + "/meem.isol": "\uFEE1", + "/meem.fina": "\uFEE2", + "/meem.init": "\uFEE3", + "/meem.medi": "\uFEE4", + "/noon.isol": "\uFEE5", + "/noon.fina": "\uFEE6", + "/noon.init": "\uFEE7", + "/noon.medi": "\uFEE8", + "/heh.isol": "\uFEE9", + "/heh.fina": "\uFEEA", + "/heh.init": "\uFEEB", + "/heh.medi": "\uFEEC", + "/waw.isol": "\uFEED", + "/waw.fina": "\uFEEE", + "/alefmaksura.isol": "\uFEEF", + "/alefmaksura.fina": "\uFEF0", + "/yeh.isol": "\uFEF1", + "/yeh.fina": "\uFEF2", + "/yeh.init": "\uFEF3", + "/yeh.medi": "\uFEF4", + "/lam.init_alef.medi_maddaabove.fina": "\uFEF5", + "/lam.medi_alef.medi_maddaabove.fina": "\uFEF6", + "/lam.init_alef.medi_hamzaabove.fina": "\uFEF7", + "/lam.medi_alef.medi_hamzaabove.fina": "\uFEF8", + "/lam.init_alef.medi_hamzabelow.fina": "\uFEF9", + "/lam.medi_alef.medi_hamzabelow.fina": "\uFEFA", + "/lam.init_alef.fina": "\uFEFB", + "/lam.medi_alef.fina": "\uFEFC", + "/zerowidthnobreakspace": "\uFEFF", + "/fwd:exclam": "\uFF01", + "/fwd:quotedbl": "\uFF02", + "/fwd:numbersign": "\uFF03", + "/fwd:dollar": "\uFF04", + "/fwd:percent": "\uFF05", + "/fwd:ampersand": "\uFF06", + "/fwd:quotesingle": "\uFF07", + "/fwd:parenthesisleft": "\uFF08", + "/fwd:parenthesisright": "\uFF09", + "/fwd:asterisk": "\uFF0A", + "/fwd:plus": "\uFF0B", + "/fwd:comma": "\uFF0C", + "/fwd:hyphen": "\uFF0D", + "/fwd:period": "\uFF0E", + "/fwd:slash": "\uFF0F", + "/fwd:zero": "\uFF10", + "/fwd:one": "\uFF11", + "/fwd:two": "\uFF12", + "/fwd:three": "\uFF13", + "/fwd:four": "\uFF14", + "/fwd:five": "\uFF15", + "/fwd:six": "\uFF16", + "/fwd:seven": "\uFF17", + "/fwd:eight": "\uFF18", + "/fwd:nine": "\uFF19", + "/fwd:colon": "\uFF1A", + "/fwd:semicolon": "\uFF1B", + "/fwd:less": "\uFF1C", + "/fwd:equal": "\uFF1D", + "/fwd:greater": "\uFF1E", + "/fwd:question": "\uFF1F", + "/fwd:at": "\uFF20", + "/fwd:A": "\uFF21", + "/fwd:B": "\uFF22", + "/fwd:C": "\uFF23", + "/fwd:D": "\uFF24", + "/fwd:E": "\uFF25", + "/fwd:F": "\uFF26", + "/fwd:G": "\uFF27", + "/fwd:H": "\uFF28", + "/fwd:I": "\uFF29", + "/fwd:J": "\uFF2A", + "/fwd:K": "\uFF2B", + "/fwd:L": "\uFF2C", + "/fwd:M": "\uFF2D", + "/fwd:N": "\uFF2E", + "/fwd:O": "\uFF2F", + "/fwd:P": "\uFF30", + "/fwd:Q": "\uFF31", + "/fwd:R": "\uFF32", + "/fwd:S": "\uFF33", + "/fwd:T": "\uFF34", + "/fwd:U": "\uFF35", + "/fwd:V": "\uFF36", + "/fwd:W": "\uFF37", + "/fwd:X": "\uFF38", + "/fwd:Y": "\uFF39", + "/fwd:Z": "\uFF3A", + "/fwd:bracketleft": "\uFF3B", + "/fwd:backslash": "\uFF3C", + "/fwd:bracketright": "\uFF3D", + "/fwd:asciicircum": "\uFF3E", + "/fwd:underscore": "\uFF3F", + "/fwd:grave": "\uFF40", + "/fwd:a": "\uFF41", + "/fwd:b": "\uFF42", + "/fwd:c": "\uFF43", + "/fwd:d": "\uFF44", + "/fwd:e": "\uFF45", + "/fwd:f": "\uFF46", + "/fwd:g": "\uFF47", + "/fwd:h": "\uFF48", + "/fwd:i": "\uFF49", + "/fwd:j": "\uFF4A", + "/fwd:k": "\uFF4B", + "/fwd:l": "\uFF4C", + "/fwd:m": "\uFF4D", + "/fwd:n": "\uFF4E", + "/fwd:o": "\uFF4F", + "/fwd:p": "\uFF50", + "/fwd:q": "\uFF51", + "/fwd:r": "\uFF52", + "/fwd:s": "\uFF53", + "/fwd:t": "\uFF54", + "/fwd:u": "\uFF55", + "/fwd:v": "\uFF56", + "/fwd:w": "\uFF57", + "/fwd:x": "\uFF58", + "/fwd:y": "\uFF59", + "/fwd:z": "\uFF5A", + "/fwd:braceleft": "\uFF5B", + "/fwd:bar": "\uFF5C", + "/fwd:braceright": "\uFF5D", + "/fwd:asciitilde": "\uFF5E", + "/fwd:leftwhiteparenthesis": "\uFF5F", + "/fwd:rightwhiteparenthesis": "\uFF60", + "/hwd:ideographicfullstop": "\uFF61", + "/hwd:leftcornerbracket": "\uFF62", + "/hwd:rightcornerbracket": "\uFF63", + "/hwd:ideographiccomma": "\uFF64", + "/hwd:kata:middledot": "\uFF65", + "/hwd:kata:wo": "\uFF66", + "/hwd:kata:asmall": "\uFF67", + "/hwd:kata:ismall": "\uFF68", + "/hwd:kata:usmall": "\uFF69", + "/hwd:kata:esmall": "\uFF6A", + "/hwd:kata:osmall": "\uFF6B", + "/hwd:kata:yasmall": "\uFF6C", + "/hwd:kata:yusmall": "\uFF6D", + "/hwd:kata:yosmall": "\uFF6E", + "/hwd:kata:tusmall": "\uFF6F", + "/hwd:kata:prolongedkana": "\uFF70", + "/hwd:kata:a": "\uFF71", + "/hwd:kata:i": "\uFF72", + "/hwd:kata:u": "\uFF73", + "/hwd:kata:e": "\uFF74", + "/hwd:kata:o": "\uFF75", + "/hwd:kata:ka": "\uFF76", + "/hwd:kata:ki": "\uFF77", + "/hwd:kata:ku": "\uFF78", + "/hwd:kata:ke": "\uFF79", + "/hwd:kata:ko": "\uFF7A", + "/hwd:kata:sa": "\uFF7B", + "/hwd:kata:si": "\uFF7C", + "/hwd:kata:su": "\uFF7D", + "/hwd:kata:se": "\uFF7E", + "/hwd:kata:so": "\uFF7F", + "/hwd:kata:ta": "\uFF80", + "/hwd:kata:ti": "\uFF81", + "/hwd:kata:tu": "\uFF82", + "/hwd:kata:te": "\uFF83", + "/hwd:kata:to": "\uFF84", + "/hwd:kata:na": "\uFF85", + "/hwd:kata:ni": "\uFF86", + "/hwd:kata:nu": "\uFF87", + "/hwd:kata:ne": "\uFF88", + "/hwd:kata:no": "\uFF89", + "/hwd:kata:ha": "\uFF8A", + "/hwd:kata:hi": "\uFF8B", + "/hwd:kata:hu": "\uFF8C", + "/hwd:kata:he": "\uFF8D", + "/hwd:kata:ho": "\uFF8E", + "/hwd:kata:ma": "\uFF8F", + "/hwd:kata:mi": "\uFF90", + "/hwd:kata:mu": "\uFF91", + "/hwd:kata:me": "\uFF92", + "/hwd:kata:mo": "\uFF93", + "/hwd:kata:ya": "\uFF94", + "/hwd:kata:yu": "\uFF95", + "/hwd:kata:yo": "\uFF96", + "/hwd:kata:ra": "\uFF97", + "/hwd:kata:ri": "\uFF98", + "/hwd:kata:ru": "\uFF99", + "/hwd:kata:re": "\uFF9A", + "/hwd:kata:ro": "\uFF9B", + "/hwd:kata:wa": "\uFF9C", + "/hwd:kata:n": "\uFF9D", + "/hwd:kata:voiced": "\uFF9E", + "/hwd:kata:semi-voiced": "\uFF9F", + "/hwd:hangulfiller": "\uFFA0", + "/hwd:kiyeok": "\uFFA1", + "/hwd:ssangkiyeok": "\uFFA2", + "/hwd:kiyeoksios": "\uFFA3", + "/hwd:nieun": "\uFFA4", + "/hwd:nieuncieuc": "\uFFA5", + "/hwd:nieunhieuh": "\uFFA6", + "/hwd:tikeut": "\uFFA7", + "/hwd:ssangtikeut": "\uFFA8", + "/hwd:rieul": "\uFFA9", + "/hwd:rieulkiyeok": "\uFFAA", + "/hwd:rieulmieum": "\uFFAB", + "/hwd:rieulpieup": "\uFFAC", + "/hwd:rieulsios": "\uFFAD", + "/hwd:rieulthieuth": "\uFFAE", + "/hwd:rieulphieuph": "\uFFAF", + "/hwd:rieulhieuh": "\uFFB0", + "/hwd:mieum": "\uFFB1", + "/hwd:pieup": "\uFFB2", + "/hwd:ssangpieup": "\uFFB3", + "/hwd:pieupsios": "\uFFB4", + "/hwd:sios": "\uFFB5", + "/hwd:ssangsios": "\uFFB6", + "/hwd:ieung": "\uFFB7", + "/hwd:cieuc": "\uFFB8", + "/hwd:ssangcieuc": "\uFFB9", + "/hwd:chieuch": "\uFFBA", + "/hwd:khieukh": "\uFFBB", + "/hwd:thieuth": "\uFFBC", + "/hwd:phieuph": "\uFFBD", + "/hwd:hieuh": "\uFFBE", + "/hwd:a": "\uFFC2", + "/hwd:ae": "\uFFC3", + "/hwd:ya": "\uFFC4", + "/hwd:yae": "\uFFC5", + "/hwd:eo": "\uFFC6", + "/hwd:e": "\uFFC7", + "/hwd:yeo": "\uFFCA", + "/hwd:ye": "\uFFCB", + "/hwd:o": "\uFFCC", + "/hwd:wa": "\uFFCD", + "/hwd:wae": "\uFFCE", + "/hwd:oe": "\uFFCF", + "/hwd:yo": "\uFFD2", + "/hwd:u": "\uFFD3", + "/hwd:weo": "\uFFD4", + "/hwd:we": "\uFFD5", + "/hwd:wi": "\uFFD6", + "/hwd:yu": "\uFFD7", + "/hwd:eu": "\uFFDA", + "/hwd:yi": "\uFFDB", + "/hwd:i": "\uFFDC", + "/fwd:centsign": "\uFFE0", + "/fwd:poundsign": "\uFFE1", + "/fwd:notsign": "\uFFE2", + "/fwd:macron": "\uFFE3", + "/fwd:brokenbar": "\uFFE4", + "/fwd:yensign": "\uFFE5", + "/fwd:wonsign": "\uFFE6", + "/hwd:formslightvertical": "\uFFE8", + "/hwd:leftwardsarrow": "\uFFE9", + "/hwd:upwardsarrow": "\uFFEA", + "/hwd:rightwardsarrow": "\uFFEB", + "/hwd:downwardsarrow": "\uFFEC", + "/hwd:blacksquare": "\uFFED", + "/hwd:whitecircle": "\uFFEE", + "/interlinearanchor": "\uFFF9", + "/interlinearseparator": "\uFFFA", + "/interlinearterminator": "\uFFFB", + "/replacementcharobj": "\uFFFC", + "/replacementchar": "\uFFFD", + "/modifierAasmall": "\U00010780", + "/modifierLetterSuperscriptTriangularColon": "\U00010781", + "/modifierLetterSuperscriptHalfTriangularColon": "\U00010782", + "/aemod": "\U00010783", + "/modifierBsmall": "\U00010784", + "/bWithHookmod": "\U00010785", + "/dzDigraphmod": "\U00010787", + "/dzDigraphWithRetroflexHookmod": "\U00010788", + "/dzDigraphWithCurlmod": "\U00010789", + "/dezhDigraphmod": "\U0001078A", + "/dWithTailmod": "\U0001078B", + "/dWithHookmod": "\U0001078C", + "/dWithHookAndTailmod": "\U0001078D", + "/reversedEmod": "\U0001078E", + "/closedReversedOpenEmod": "\U0001078F", + "/fengDigraphmod": "\U00010790", + "/ramsHornmod": "\U00010791", + "/modifierGsmall": "\U00010792", + "/gWithHookmod": "\U00010793", + "/modifierGWithHooksmall": "\U00010794", + "/hWithStrokemod": "\U00010795", + "/modifierHsmall": "\U00010796", + "/hengWithHookmod": "\U00010797", + "/dotlessJWithStrokeAndHookmod": "\U00010798", + "/lsDigraphmod": "\U00010799", + "/lzDigraphmod": "\U0001079A", + "/lWithBeltmod": "\U0001079B", + "/modifierLWithBeltsmall": "\U0001079C", + "/lWithRetroflexHookAndBeltmod": "\U0001079D", + "/lezhmod": "\U0001079E", + "/lezhWithRetroflexHookmod": "\U0001079F", + "/turnedYmod": "\U000107A0", + "/turnedYWithBeltmod": "\U000107A1", + "/oWithStrokemod": "\U000107A2", + "/modifierOesmall": "\U000107A3", + "/closedOmegamod": "\U000107A4", + "/qmod": "\U000107A5", + "/turnedRWithLongLegmod": "\U000107A6", + "/turnedRWithLongLegAndRetroflexHookmod": "\U000107A7", + "/rWithTailmod": "\U000107A8", + "/rWithFishhookmod": "\U000107A9", + "/modifierRsmall": "\U000107AA", + "/tcDigraphWithCurlmod": "\U000107AB", + "/tsDigraphmod": "\U000107AC", + "/tsDigraphWithRetroflexHookmod": "\U000107AD", + "/teshDigraphmod": "\U000107AE", + "/tWithRetroflexHookmod": "\U000107AF", + "/vWithRightHookmod": "\U000107B0", + "/modifierYsmall": "\U000107B2", + "/modifierLetterGlottalStopWithStroke": "\U000107B3", + "/modifierLetterReversedGlottalStopWithStroke": "\U000107B4", + "/modifierLetterBilabialClick": "\U000107B5", + "/modifierLetterDentalClick": "\U000107B6", + "/modifierLetterLateralClick": "\U000107B7", + "/modifierLetterAlveolarClick": "\U000107B8", + "/modifierLetterRetroflexClickWithRetroflexHook": "\U000107B9", + "/sWithCurlmod": "\U000107BA", + "/zanb:a": "\U00011A00", + "/zanb:vowelsigni": "\U00011A01", + "/zanb:vowelsignue": "\U00011A02", + "/zanb:vowelsignu": "\U00011A03", + "/zanb:vowelsigne": "\U00011A04", + "/zanb:vowelsignoe": "\U00011A05", + "/zanb:vowelsigno": "\U00011A06", + "/zanb:vowelsignai": "\U00011A07", + "/zanb:vowelsignau": "\U00011A08", + "/zanb:vowelsignreversedi": "\U00011A09", + "/zanb:vowellengthmark": "\U00011A0A", + "/zanb:ka": "\U00011A0B", + "/zanb:kha": "\U00011A0C", + "/zanb:ga": "\U00011A0D", + "/zanb:gha": "\U00011A0E", + "/zanb:nga": "\U00011A0F", + "/zanb:ca": "\U00011A10", + "/zanb:cha": "\U00011A11", + "/zanb:ja": "\U00011A12", + "/zanb:nya": "\U00011A13", + "/zanb:tta": "\U00011A14", + "/zanb:ttha": "\U00011A15", + "/zanb:dda": "\U00011A16", + "/zanb:ddha": "\U00011A17", + "/zanb:nna": "\U00011A18", + "/zanb:ta": "\U00011A19", + "/zanb:tha": "\U00011A1A", + "/zanb:da": "\U00011A1B", + "/zanb:dha": "\U00011A1C", + "/zanb:na": "\U00011A1D", + "/zanb:pa": "\U00011A1E", + "/zanb:pha": "\U00011A1F", + "/zanb:ba": "\U00011A20", + "/zanb:bha": "\U00011A21", + "/zanb:ma": "\U00011A22", + "/zanb:tsa": "\U00011A23", + "/zanb:tsha": "\U00011A24", + "/zanb:dza": "\U00011A25", + "/zanb:dzha": "\U00011A26", + "/zanb:zha": "\U00011A27", + "/zanb:za": "\U00011A28", + "/zanb:dashA": "\U00011A29", + "/zanb:ya": "\U00011A2A", + "/zanb:ra": "\U00011A2B", + "/zanb:la": "\U00011A2C", + "/zanb:va": "\U00011A2D", + "/zanb:sha": "\U00011A2E", + "/zanb:ssa": "\U00011A2F", + "/zanb:sa": "\U00011A30", + "/zanb:ha": "\U00011A31", + "/zanb:kssa": "\U00011A32", + "/zanb:finalconsonantmark": "\U00011A33", + "/zanb:signvirama": "\U00011A34", + "/zanb:signcandrabindu": "\U00011A35", + "/zanb:signcandrabinduwithornament": "\U00011A36", + "/zanb:signcandrawithornament": "\U00011A37", + "/zanb:signanusvara": "\U00011A38", + "/zanb:signvisarga": "\U00011A39", + "/zanb:raclusterinit": "\U00011A3A", + "/zanb:yaclusterfina": "\U00011A3B", + "/zanb:raclusterfina": "\U00011A3C", + "/zanb:laclusterfina": "\U00011A3D", + "/zanb:vaclusterfina": "\U00011A3E", + "/zanb:initialheadmark": "\U00011A3F", + "/zanb:closingheadmark": "\U00011A40", + "/zanb:marktsheg": "\U00011A41", + "/zanb:markshad": "\U00011A42", + "/zanb:markdoubleshad": "\U00011A43", + "/zanb:marklongtsheg": "\U00011A44", + "/zanb:initialheadmarkdbllined": "\U00011A45", + "/zanb:closingheadmarkdbllined": "\U00011A46", + "/zanb:subjoiner": "\U00011A47", + "/soyo:a": "\U00011A50", + "/soyo:iVowelSign": "\U00011A51", + "/soyo:ueVowelSign": "\U00011A52", + "/soyo:uVowelSign": "\U00011A53", + "/soyo:eVowelSign": "\U00011A54", + "/soyo:oVowelSign": "\U00011A55", + "/soyo:oeVowelSign": "\U00011A56", + "/soyo:aiVowelSign": "\U00011A57", + "/soyo:auVowelSign": "\U00011A58", + "/soyo:vocalicRVowelSign": "\U00011A59", + "/soyo:vocalicLVowelSign": "\U00011A5A", + "/soyo:vowelLengthMark": "\U00011A5B", + "/soyo:ka": "\U00011A5C", + "/soyo:kha": "\U00011A5D", + "/soyo:ga": "\U00011A5E", + "/soyo:gha": "\U00011A5F", + "/soyo:nga": "\U00011A60", + "/soyo:ca": "\U00011A61", + "/soyo:cha": "\U00011A62", + "/soyo:ja": "\U00011A63", + "/soyo:jha": "\U00011A64", + "/soyo:nya": "\U00011A65", + "/soyo:tta": "\U00011A66", + "/soyo:ttha": "\U00011A67", + "/soyo:dda": "\U00011A68", + "/soyo:ddha": "\U00011A69", + "/soyo:nna": "\U00011A6A", + "/soyo:ta": "\U00011A6B", + "/soyo:tha": "\U00011A6C", + "/soyo:da": "\U00011A6D", + "/soyo:dha": "\U00011A6E", + "/soyo:na": "\U00011A6F", + "/soyo:pa": "\U00011A70", + "/soyo:pha": "\U00011A71", + "/soyo:ba": "\U00011A72", + "/soyo:bha": "\U00011A73", + "/soyo:ma": "\U00011A74", + "/soyo:tsa": "\U00011A75", + "/soyo:tsha": "\U00011A76", + "/soyo:dza": "\U00011A77", + "/soyo:zha": "\U00011A78", + "/soyo:za": "\U00011A79", + "/soyo:-a": "\U00011A7A", + "/soyo:ya": "\U00011A7B", + "/soyo:ra": "\U00011A7C", + "/soyo:la": "\U00011A7D", + "/soyo:va": "\U00011A7E", + "/soyo:sha": "\U00011A7F", + "/soyo:ssa": "\U00011A80", + "/soyo:sa": "\U00011A81", + "/soyo:ha": "\U00011A82", + "/soyo:kssa": "\U00011A83", + "/soyo:signJihvamuliya": "\U00011A84", + "/soyo:signUpadhmaniya": "\U00011A85", + "/soyo:raClusterInitial": "\U00011A86", + "/soyo:laClusterInitial": "\U00011A87", + "/soyo:shaClusterInitial": "\U00011A88", + "/soyo:saClusterInitial": "\U00011A89", + "/soyo:finalConsonantSignG": "\U00011A8A", + "/soyo:finalConsonantSignK": "\U00011A8B", + "/soyo:finalConsonantSignNg": "\U00011A8C", + "/soyo:finalConsonantSignD": "\U00011A8D", + "/soyo:finalConsonantSignN": "\U00011A8E", + "/soyo:finalConsonantSignB": "\U00011A8F", + "/soyo:finalConsonantSignM": "\U00011A90", + "/soyo:finalConsonantSignR": "\U00011A91", + "/soyo:finalConsonantSignL": "\U00011A92", + "/soyo:finalConsonantSignSh": "\U00011A93", + "/soyo:finalConsonantSignS": "\U00011A94", + "/soyo:finalConsonantSign-a": "\U00011A95", + "/soyo:signAnusvara": "\U00011A96", + "/soyo:signVisarga": "\U00011A97", + "/soyo:geminationMark": "\U00011A98", + "/soyo:subjoiner": "\U00011A99", + "/soyo:markTsheg": "\U00011A9A", + "/soyo:markShad": "\U00011A9B", + "/soyo:markDoubleShad": "\U00011A9C", + "/soyo:markPluta": "\U00011A9D", + "/soyo:headMarkWithMoonAndSunAndTripleFlame": "\U00011A9E", + "/soyo:headMarkWithMoonAndSunAndFlame": "\U00011A9F", + "/soyo:headMarkWithMoonAndSun": "\U00011AA0", + "/soyo:terminalMark-1": "\U00011AA1", + "/soyo:terminalMark-2": "\U00011AA2", + "/Abold": "\U0001D400", + "/Bbold": "\U0001D401", + "/Cbold": "\U0001D402", + "/Dbold": "\U0001D403", + "/Ebold": "\U0001D404", + "/Fbold": "\U0001D405", + "/Gbold": "\U0001D406", + "/Hbold": "\U0001D407", + "/Ibold": "\U0001D408", + "/Jbold": "\U0001D409", + "/Kbold": "\U0001D40A", + "/Lbold": "\U0001D40B", + "/Mbold": "\U0001D40C", + "/Nbold": "\U0001D40D", + "/Obold": "\U0001D40E", + "/Pbold": "\U0001D40F", + "/Qbold": "\U0001D410", + "/Rbold": "\U0001D411", + "/Sbold": "\U0001D412", + "/Tbold": "\U0001D413", + "/Ubold": "\U0001D414", + "/Vbold": "\U0001D415", + "/Wbold": "\U0001D416", + "/Xbold": "\U0001D417", + "/Ybold": "\U0001D418", + "/Zbold": "\U0001D419", + "/abold": "\U0001D41A", + "/bbold": "\U0001D41B", + "/cbold": "\U0001D41C", + "/dbold": "\U0001D41D", + "/ebold": "\U0001D41E", + "/fbold": "\U0001D41F", + "/gbold": "\U0001D420", + "/hbold": "\U0001D421", + "/ibold": "\U0001D422", + "/jbold": "\U0001D423", + "/kbold": "\U0001D424", + "/lbold": "\U0001D425", + "/mbold": "\U0001D426", + "/nbold": "\U0001D427", + "/obold": "\U0001D428", + "/pbold": "\U0001D429", + "/qbold": "\U0001D42A", + "/rbold": "\U0001D42B", + "/sbold": "\U0001D42C", + "/tbold": "\U0001D42D", + "/ubold": "\U0001D42E", + "/vbold": "\U0001D42F", + "/wbold": "\U0001D430", + "/xbold": "\U0001D431", + "/ybold": "\U0001D432", + "/zbold": "\U0001D433", + "/Aitalic": "\U0001D434", + "/Bitalic": "\U0001D435", + "/Citalic": "\U0001D436", + "/Ditalic": "\U0001D437", + "/Eitalic": "\U0001D438", + "/Fitalic": "\U0001D439", + "/Gitalic": "\U0001D43A", + "/Hitalic": "\U0001D43B", + "/Iitalic": "\U0001D43C", + "/Jitalic": "\U0001D43D", + "/Kitalic": "\U0001D43E", + "/Litalic": "\U0001D43F", + "/Mitalic": "\U0001D440", + "/Nitalic": "\U0001D441", + "/Oitalic": "\U0001D442", + "/Pitalic": "\U0001D443", + "/Qitalic": "\U0001D444", + "/Ritalic": "\U0001D445", + "/Sitalic": "\U0001D446", + "/Titalic": "\U0001D447", + "/Uitalic": "\U0001D448", + "/Vitalic": "\U0001D449", + "/Witalic": "\U0001D44A", + "/Xitalic": "\U0001D44B", + "/Yitalic": "\U0001D44C", + "/Zitalic": "\U0001D44D", + "/aitalic": "\U0001D44E", + "/bitalic": "\U0001D44F", + "/citalic": "\U0001D450", + "/ditalic": "\U0001D451", + "/eitalic": "\U0001D452", + "/fitalic": "\U0001D453", + "/gitalic": "\U0001D454", + "/iitalic": "\U0001D456", + "/jitalic": "\U0001D457", + "/kitalic": "\U0001D458", + "/litalic": "\U0001D459", + "/mitalic": "\U0001D45A", + "/nitalic": "\U0001D45B", + "/oitalic": "\U0001D45C", + "/pitalic": "\U0001D45D", + "/qitalic": "\U0001D45E", + "/ritalic": "\U0001D45F", + "/sitalic": "\U0001D460", + "/titalic": "\U0001D461", + "/uitalic": "\U0001D462", + "/vitalic": "\U0001D463", + "/witalic": "\U0001D464", + "/xitalic": "\U0001D465", + "/yitalic": "\U0001D466", + "/zitalic": "\U0001D467", + "/Abolditalic": "\U0001D468", + "/Bbolditalic": "\U0001D469", + "/Cbolditalic": "\U0001D46A", + "/Dbolditalic": "\U0001D46B", + "/Ebolditalic": "\U0001D46C", + "/Fbolditalic": "\U0001D46D", + "/Gbolditalic": "\U0001D46E", + "/Hbolditalic": "\U0001D46F", + "/Ibolditalic": "\U0001D470", + "/Jbolditalic": "\U0001D471", + "/Kbolditalic": "\U0001D472", + "/Lbolditalic": "\U0001D473", + "/Mbolditalic": "\U0001D474", + "/Nbolditalic": "\U0001D475", + "/Obolditalic": "\U0001D476", + "/Pbolditalic": "\U0001D477", + "/Qbolditalic": "\U0001D478", + "/Rbolditalic": "\U0001D479", + "/Sbolditalic": "\U0001D47A", + "/Tbolditalic": "\U0001D47B", + "/Ubolditalic": "\U0001D47C", + "/Vbolditalic": "\U0001D47D", + "/Wbolditalic": "\U0001D47E", + "/Xbolditalic": "\U0001D47F", + "/Ybolditalic": "\U0001D480", + "/Zbolditalic": "\U0001D481", + "/abolditalic": "\U0001D482", + "/bbolditalic": "\U0001D483", + "/cbolditalic": "\U0001D484", + "/dbolditalic": "\U0001D485", + "/ebolditalic": "\U0001D486", + "/fbolditalic": "\U0001D487", + "/gbolditalic": "\U0001D488", + "/hbolditalic": "\U0001D489", + "/ibolditalic": "\U0001D48A", + "/jbolditalic": "\U0001D48B", + "/kbolditalic": "\U0001D48C", + "/lbolditalic": "\U0001D48D", + "/mbolditalic": "\U0001D48E", + "/nbolditalic": "\U0001D48F", + "/obolditalic": "\U0001D490", + "/pbolditalic": "\U0001D491", + "/qbolditalic": "\U0001D492", + "/rbolditalic": "\U0001D493", + "/sbolditalic": "\U0001D494", + "/tbolditalic": "\U0001D495", + "/ubolditalic": "\U0001D496", + "/vbolditalic": "\U0001D497", + "/wbolditalic": "\U0001D498", + "/xbolditalic": "\U0001D499", + "/ybolditalic": "\U0001D49A", + "/zbolditalic": "\U0001D49B", + "/Ascript": "\U0001D49C", + "/Cscript": "\U0001D49E", + "/Dscript": "\U0001D49F", + "/Gscript": "\U0001D4A2", + "/Jscript": "\U0001D4A5", + "/Kscript": "\U0001D4A6", + "/Nscript": "\U0001D4A9", + "/Oscript": "\U0001D4AA", + "/Pscript": "\U0001D4AB", + "/Qscript": "\U0001D4AC", + "/Sscript": "\U0001D4AE", + "/Tscript": "\U0001D4AF", + "/Uscript": "\U0001D4B0", + "/Vscript": "\U0001D4B1", + "/Wscript": "\U0001D4B2", + "/Xscript": "\U0001D4B3", + "/Yscript": "\U0001D4B4", + "/Zscript": "\U0001D4B5", + "/bscript": "\U0001D4B7", + "/cscript": "\U0001D4B8", + "/dscript": "\U0001D4B9", + "/fscript": "\U0001D4BB", + "/hscript": "\U0001D4BD", + "/iscript": "\U0001D4BE", + "/jscript": "\U0001D4BF", + "/kscript": "\U0001D4C0", + "/lscript": "\U0001D4C1", + "/mscript": "\U0001D4C2", + "/nscript": "\U0001D4C3", + "/pscript": "\U0001D4C5", + "/qscript": "\U0001D4C6", + "/math:rscript": "\U0001D4C7", + "/sscript": "\U0001D4C8", + "/tscript": "\U0001D4C9", + "/uscript": "\U0001D4CA", + "/vscript": "\U0001D4CB", + "/wscript": "\U0001D4CC", + "/xscript": "\U0001D4CD", + "/yscript": "\U0001D4CE", + "/zscript": "\U0001D4CF", + "/Aboldscript": "\U0001D4D0", + "/Bboldscript": "\U0001D4D1", + "/Cboldscript": "\U0001D4D2", + "/Dboldscript": "\U0001D4D3", + "/Eboldscript": "\U0001D4D4", + "/Fboldscript": "\U0001D4D5", + "/Gboldscript": "\U0001D4D6", + "/Hboldscript": "\U0001D4D7", + "/Iboldscript": "\U0001D4D8", + "/Jboldscript": "\U0001D4D9", + "/Kboldscript": "\U0001D4DA", + "/Lboldscript": "\U0001D4DB", + "/Mboldscript": "\U0001D4DC", + "/Nboldscript": "\U0001D4DD", + "/Oboldscript": "\U0001D4DE", + "/Pboldscript": "\U0001D4DF", + "/Qboldscript": "\U0001D4E0", + "/Rboldscript": "\U0001D4E1", + "/Sboldscript": "\U0001D4E2", + "/Tboldscript": "\U0001D4E3", + "/Uboldscript": "\U0001D4E4", + "/Vboldscript": "\U0001D4E5", + "/Wboldscript": "\U0001D4E6", + "/Xboldscript": "\U0001D4E7", + "/Yboldscript": "\U0001D4E8", + "/Zboldscript": "\U0001D4E9", + "/aboldscript": "\U0001D4EA", + "/bboldscript": "\U0001D4EB", + "/cboldscript": "\U0001D4EC", + "/dboldscript": "\U0001D4ED", + "/eboldscript": "\U0001D4EE", + "/fboldscript": "\U0001D4EF", + "/gboldscript": "\U0001D4F0", + "/hboldscript": "\U0001D4F1", + "/iboldscript": "\U0001D4F2", + "/jboldscript": "\U0001D4F3", + "/kboldscript": "\U0001D4F4", + "/lboldscript": "\U0001D4F5", + "/mboldscript": "\U0001D4F6", + "/nboldscript": "\U0001D4F7", + "/oboldscript": "\U0001D4F8", + "/pboldscript": "\U0001D4F9", + "/qboldscript": "\U0001D4FA", + "/rboldscript": "\U0001D4FB", + "/sboldscript": "\U0001D4FC", + "/tboldscript": "\U0001D4FD", + "/uboldscript": "\U0001D4FE", + "/vboldscript": "\U0001D4FF", + "/wboldscript": "\U0001D500", + "/xboldscript": "\U0001D501", + "/yboldscript": "\U0001D502", + "/zboldscript": "\U0001D503", + "/Afraktur": "\U0001D504", + "/Bfraktur": "\U0001D505", + "/Dfraktur": "\U0001D507", + "/Efraktur": "\U0001D508", + "/Ffraktur": "\U0001D509", + "/Gfraktur": "\U0001D50A", + "/Jfraktur": "\U0001D50D", + "/Kfraktur": "\U0001D50E", + "/Lfraktur": "\U0001D50F", + "/Mfraktur": "\U0001D510", + "/Nfraktur": "\U0001D511", + "/Ofraktur": "\U0001D512", + "/Pfraktur": "\U0001D513", + "/Qfraktur": "\U0001D514", + "/Sfraktur": "\U0001D516", + "/Tfraktur": "\U0001D517", + "/Ufraktur": "\U0001D518", + "/Vfraktur": "\U0001D519", + "/Wfraktur": "\U0001D51A", + "/Xfraktur": "\U0001D51B", + "/Yfraktur": "\U0001D51C", + "/afraktur": "\U0001D51E", + "/bfraktur": "\U0001D51F", + "/cfraktur": "\U0001D520", + "/dfraktur": "\U0001D521", + "/efraktur": "\U0001D522", + "/ffraktur": "\U0001D523", + "/gfraktur": "\U0001D524", + "/hfraktur": "\U0001D525", + "/ifraktur": "\U0001D526", + "/jfraktur": "\U0001D527", + "/kfraktur": "\U0001D528", + "/lfraktur": "\U0001D529", + "/mfraktur": "\U0001D52A", + "/nfraktur": "\U0001D52B", + "/ofraktur": "\U0001D52C", + "/pfraktur": "\U0001D52D", + "/qfraktur": "\U0001D52E", + "/rfraktur": "\U0001D52F", + "/sfraktur": "\U0001D530", + "/tfraktur": "\U0001D531", + "/ufraktur": "\U0001D532", + "/vfraktur": "\U0001D533", + "/wfraktur": "\U0001D534", + "/xfraktur": "\U0001D535", + "/yfraktur": "\U0001D536", + "/zfraktur": "\U0001D537", + "/Adblstruck": "\U0001D538", + "/Bdblstruck": "\U0001D539", + "/Ddblstruck": "\U0001D53B", + "/Edblstruck": "\U0001D53C", + "/Fdblstruck": "\U0001D53D", + "/Gdblstruck": "\U0001D53E", + "/Idblstruck": "\U0001D540", + "/Jdblstruck": "\U0001D541", + "/Kdblstruck": "\U0001D542", + "/Ldblstruck": "\U0001D543", + "/Mdblstruck": "\U0001D544", + "/Odblstruck": "\U0001D546", + "/Sdblstruck": "\U0001D54A", + "/Tdblstruck": "\U0001D54B", + "/Udblstruck": "\U0001D54C", + "/Vdblstruck": "\U0001D54D", + "/Wdblstruck": "\U0001D54E", + "/Xdblstruck": "\U0001D54F", + "/Ydblstruck": "\U0001D550", + "/adblstruck": "\U0001D552", + "/bdblstruck": "\U0001D553", + "/cdblstruck": "\U0001D554", + "/ddblstruck": "\U0001D555", + "/edblstruck": "\U0001D556", + "/fdblstruck": "\U0001D557", + "/gdblstruck": "\U0001D558", + "/hdblstruck": "\U0001D559", + "/idblstruck": "\U0001D55A", + "/jdblstruck": "\U0001D55B", + "/kdblstruck": "\U0001D55C", + "/ldblstruck": "\U0001D55D", + "/mdblstruck": "\U0001D55E", + "/ndblstruck": "\U0001D55F", + "/odblstruck": "\U0001D560", + "/pdblstruck": "\U0001D561", + "/qdblstruck": "\U0001D562", + "/rdblstruck": "\U0001D563", + "/sdblstruck": "\U0001D564", + "/tdblstruck": "\U0001D565", + "/udblstruck": "\U0001D566", + "/vdblstruck": "\U0001D567", + "/wdblstruck": "\U0001D568", + "/xdblstruck": "\U0001D569", + "/ydblstruck": "\U0001D56A", + "/zdblstruck": "\U0001D56B", + "/Aboldfraktur": "\U0001D56C", + "/Bboldfraktur": "\U0001D56D", + "/Cboldfraktur": "\U0001D56E", + "/Dboldfraktur": "\U0001D56F", + "/Eboldfraktur": "\U0001D570", + "/Fboldfraktur": "\U0001D571", + "/Gboldfraktur": "\U0001D572", + "/Hboldfraktur": "\U0001D573", + "/Iboldfraktur": "\U0001D574", + "/Jboldfraktur": "\U0001D575", + "/Kboldfraktur": "\U0001D576", + "/Lboldfraktur": "\U0001D577", + "/Mboldfraktur": "\U0001D578", + "/Nboldfraktur": "\U0001D579", + "/Oboldfraktur": "\U0001D57A", + "/Pboldfraktur": "\U0001D57B", + "/Qboldfraktur": "\U0001D57C", + "/Rboldfraktur": "\U0001D57D", + "/Sboldfraktur": "\U0001D57E", + "/Tboldfraktur": "\U0001D57F", + "/Uboldfraktur": "\U0001D580", + "/Vboldfraktur": "\U0001D581", + "/Wboldfraktur": "\U0001D582", + "/Xboldfraktur": "\U0001D583", + "/Yboldfraktur": "\U0001D584", + "/Zboldfraktur": "\U0001D585", + "/aboldfraktur": "\U0001D586", + "/bboldfraktur": "\U0001D587", + "/cboldfraktur": "\U0001D588", + "/dboldfraktur": "\U0001D589", + "/eboldfraktur": "\U0001D58A", + "/fboldfraktur": "\U0001D58B", + "/gboldfraktur": "\U0001D58C", + "/hboldfraktur": "\U0001D58D", + "/iboldfraktur": "\U0001D58E", + "/jboldfraktur": "\U0001D58F", + "/kboldfraktur": "\U0001D590", + "/lboldfraktur": "\U0001D591", + "/mboldfraktur": "\U0001D592", + "/nboldfraktur": "\U0001D593", + "/oboldfraktur": "\U0001D594", + "/pboldfraktur": "\U0001D595", + "/qboldfraktur": "\U0001D596", + "/rboldfraktur": "\U0001D597", + "/sboldfraktur": "\U0001D598", + "/tboldfraktur": "\U0001D599", + "/uboldfraktur": "\U0001D59A", + "/vboldfraktur": "\U0001D59B", + "/wboldfraktur": "\U0001D59C", + "/xboldfraktur": "\U0001D59D", + "/yboldfraktur": "\U0001D59E", + "/zboldfraktur": "\U0001D59F", + "/Asans": "\U0001D5A0", + "/Bsans": "\U0001D5A1", + "/Csans": "\U0001D5A2", + "/Dsans": "\U0001D5A3", + "/Esans": "\U0001D5A4", + "/Fsans": "\U0001D5A5", + "/Gsans": "\U0001D5A6", + "/Hsans": "\U0001D5A7", + "/Isans": "\U0001D5A8", + "/Jsans": "\U0001D5A9", + "/Ksans": "\U0001D5AA", + "/Lsans": "\U0001D5AB", + "/Msans": "\U0001D5AC", + "/Nsans": "\U0001D5AD", + "/Osans": "\U0001D5AE", + "/Psans": "\U0001D5AF", + "/Qsans": "\U0001D5B0", + "/Rsans": "\U0001D5B1", + "/Ssans": "\U0001D5B2", + "/Tsans": "\U0001D5B3", + "/Usans": "\U0001D5B4", + "/Vsans": "\U0001D5B5", + "/Wsans": "\U0001D5B6", + "/Xsans": "\U0001D5B7", + "/Ysans": "\U0001D5B8", + "/Zsans": "\U0001D5B9", + "/asans": "\U0001D5BA", + "/bsans": "\U0001D5BB", + "/csans": "\U0001D5BC", + "/dsans": "\U0001D5BD", + "/esans": "\U0001D5BE", + "/fsans": "\U0001D5BF", + "/gsans": "\U0001D5C0", + "/hsans": "\U0001D5C1", + "/isans": "\U0001D5C2", + "/jsans": "\U0001D5C3", + "/ksans": "\U0001D5C4", + "/lsans": "\U0001D5C5", + "/msans": "\U0001D5C6", + "/nsans": "\U0001D5C7", + "/osans": "\U0001D5C8", + "/psans": "\U0001D5C9", + "/qsans": "\U0001D5CA", + "/rsans": "\U0001D5CB", + "/ssans": "\U0001D5CC", + "/tsans": "\U0001D5CD", + "/usans": "\U0001D5CE", + "/vsans": "\U0001D5CF", + "/wsans": "\U0001D5D0", + "/xsans": "\U0001D5D1", + "/ysans": "\U0001D5D2", + "/zsans": "\U0001D5D3", + "/Aboldsans": "\U0001D5D4", + "/Bboldsans": "\U0001D5D5", + "/Cboldsans": "\U0001D5D6", + "/Dboldsans": "\U0001D5D7", + "/Eboldsans": "\U0001D5D8", + "/Fboldsans": "\U0001D5D9", + "/Gboldsans": "\U0001D5DA", + "/Hboldsans": "\U0001D5DB", + "/Iboldsans": "\U0001D5DC", + "/Jboldsans": "\U0001D5DD", + "/Kboldsans": "\U0001D5DE", + "/Lboldsans": "\U0001D5DF", + "/Mboldsans": "\U0001D5E0", + "/Nboldsans": "\U0001D5E1", + "/Oboldsans": "\U0001D5E2", + "/Pboldsans": "\U0001D5E3", + "/Qboldsans": "\U0001D5E4", + "/Rboldsans": "\U0001D5E5", + "/Sboldsans": "\U0001D5E6", + "/Tboldsans": "\U0001D5E7", + "/Uboldsans": "\U0001D5E8", + "/Vboldsans": "\U0001D5E9", + "/Wboldsans": "\U0001D5EA", + "/Xboldsans": "\U0001D5EB", + "/Yboldsans": "\U0001D5EC", + "/Zboldsans": "\U0001D5ED", + "/aboldsans": "\U0001D5EE", + "/bboldsans": "\U0001D5EF", + "/cboldsans": "\U0001D5F0", + "/dboldsans": "\U0001D5F1", + "/eboldsans": "\U0001D5F2", + "/fboldsans": "\U0001D5F3", + "/gboldsans": "\U0001D5F4", + "/hboldsans": "\U0001D5F5", + "/iboldsans": "\U0001D5F6", + "/jboldsans": "\U0001D5F7", + "/kboldsans": "\U0001D5F8", + "/lboldsans": "\U0001D5F9", + "/mboldsans": "\U0001D5FA", + "/nboldsans": "\U0001D5FB", + "/oboldsans": "\U0001D5FC", + "/pboldsans": "\U0001D5FD", + "/qboldsans": "\U0001D5FE", + "/rboldsans": "\U0001D5FF", + "/sboldsans": "\U0001D600", + "/tboldsans": "\U0001D601", + "/uboldsans": "\U0001D602", + "/vboldsans": "\U0001D603", + "/wboldsans": "\U0001D604", + "/xboldsans": "\U0001D605", + "/yboldsans": "\U0001D606", + "/zboldsans": "\U0001D607", + "/Aitalicsans": "\U0001D608", + "/Bitalicsans": "\U0001D609", + "/Citalicsans": "\U0001D60A", + "/Ditalicsans": "\U0001D60B", + "/Eitalicsans": "\U0001D60C", + "/Fitalicsans": "\U0001D60D", + "/Gitalicsans": "\U0001D60E", + "/Hitalicsans": "\U0001D60F", + "/Iitalicsans": "\U0001D610", + "/Jitalicsans": "\U0001D611", + "/Kitalicsans": "\U0001D612", + "/Litalicsans": "\U0001D613", + "/Mitalicsans": "\U0001D614", + "/Nitalicsans": "\U0001D615", + "/Oitalicsans": "\U0001D616", + "/Pitalicsans": "\U0001D617", + "/Qitalicsans": "\U0001D618", + "/Ritalicsans": "\U0001D619", + "/Sitalicsans": "\U0001D61A", + "/Titalicsans": "\U0001D61B", + "/Uitalicsans": "\U0001D61C", + "/Vitalicsans": "\U0001D61D", + "/Witalicsans": "\U0001D61E", + "/Xitalicsans": "\U0001D61F", + "/Yitalicsans": "\U0001D620", + "/Zitalicsans": "\U0001D621", + "/aitalicsans": "\U0001D622", + "/bitalicsans": "\U0001D623", + "/citalicsans": "\U0001D624", + "/ditalicsans": "\U0001D625", + "/eitalicsans": "\U0001D626", + "/fitalicsans": "\U0001D627", + "/gitalicsans": "\U0001D628", + "/hitalicsans": "\U0001D629", + "/iitalicsans": "\U0001D62A", + "/jitalicsans": "\U0001D62B", + "/kitalicsans": "\U0001D62C", + "/litalicsans": "\U0001D62D", + "/mitalicsans": "\U0001D62E", + "/nitalicsans": "\U0001D62F", + "/oitalicsans": "\U0001D630", + "/pitalicsans": "\U0001D631", + "/qitalicsans": "\U0001D632", + "/ritalicsans": "\U0001D633", + "/sitalicsans": "\U0001D634", + "/titalicsans": "\U0001D635", + "/uitalicsans": "\U0001D636", + "/vitalicsans": "\U0001D637", + "/witalicsans": "\U0001D638", + "/xitalicsans": "\U0001D639", + "/yitalicsans": "\U0001D63A", + "/zitalicsans": "\U0001D63B", + "/Abolditalicsans": "\U0001D63C", + "/Bbolditalicsans": "\U0001D63D", + "/Cbolditalicsans": "\U0001D63E", + "/Dbolditalicsans": "\U0001D63F", + "/Ebolditalicsans": "\U0001D640", + "/Fbolditalicsans": "\U0001D641", + "/Gbolditalicsans": "\U0001D642", + "/Hbolditalicsans": "\U0001D643", + "/Ibolditalicsans": "\U0001D644", + "/Jbolditalicsans": "\U0001D645", + "/Kbolditalicsans": "\U0001D646", + "/Lbolditalicsans": "\U0001D647", + "/Mbolditalicsans": "\U0001D648", + "/Nbolditalicsans": "\U0001D649", + "/Obolditalicsans": "\U0001D64A", + "/Pbolditalicsans": "\U0001D64B", + "/Qbolditalicsans": "\U0001D64C", + "/Rbolditalicsans": "\U0001D64D", + "/Sbolditalicsans": "\U0001D64E", + "/Tbolditalicsans": "\U0001D64F", + "/Ubolditalicsans": "\U0001D650", + "/Vbolditalicsans": "\U0001D651", + "/Wbolditalicsans": "\U0001D652", + "/Xbolditalicsans": "\U0001D653", + "/Ybolditalicsans": "\U0001D654", + "/Zbolditalicsans": "\U0001D655", + "/abolditalicsans": "\U0001D656", + "/bbolditalicsans": "\U0001D657", + "/cbolditalicsans": "\U0001D658", + "/dbolditalicsans": "\U0001D659", + "/ebolditalicsans": "\U0001D65A", + "/fbolditalicsans": "\U0001D65B", + "/gbolditalicsans": "\U0001D65C", + "/hbolditalicsans": "\U0001D65D", + "/ibolditalicsans": "\U0001D65E", + "/jbolditalicsans": "\U0001D65F", + "/kbolditalicsans": "\U0001D660", + "/lbolditalicsans": "\U0001D661", + "/mbolditalicsans": "\U0001D662", + "/nbolditalicsans": "\U0001D663", + "/obolditalicsans": "\U0001D664", + "/pbolditalicsans": "\U0001D665", + "/qbolditalicsans": "\U0001D666", + "/rbolditalicsans": "\U0001D667", + "/sbolditalicsans": "\U0001D668", + "/tbolditalicsans": "\U0001D669", + "/ubolditalicsans": "\U0001D66A", + "/vbolditalicsans": "\U0001D66B", + "/wbolditalicsans": "\U0001D66C", + "/xbolditalicsans": "\U0001D66D", + "/ybolditalicsans": "\U0001D66E", + "/zbolditalicsans": "\U0001D66F", + "/Amono": "\U0001D670", + "/Bmono": "\U0001D671", + "/Cmono": "\U0001D672", + "/Dmono": "\U0001D673", + "/Emono": "\U0001D674", + "/Fmono": "\U0001D675", + "/Gmono": "\U0001D676", + "/Hmono": "\U0001D677", + "/Imono": "\U0001D678", + "/Jmono": "\U0001D679", + "/Kmono": "\U0001D67A", + "/Lmono": "\U0001D67B", + "/Mmono": "\U0001D67C", + "/Nmono": "\U0001D67D", + "/Omono": "\U0001D67E", + "/Pmono": "\U0001D67F", + "/Qmono": "\U0001D680", + "/Rmono": "\U0001D681", + "/Smono": "\U0001D682", + "/Tmono": "\U0001D683", + "/Umono": "\U0001D684", + "/Vmono": "\U0001D685", + "/Wmono": "\U0001D686", + "/Xmono": "\U0001D687", + "/Ymono": "\U0001D688", + "/Zmono": "\U0001D689", + "/amono": "\U0001D68A", + "/bmono": "\U0001D68B", + "/cmono": "\U0001D68C", + "/dmono": "\U0001D68D", + "/emono": "\U0001D68E", + "/fmono": "\U0001D68F", + "/gmono": "\U0001D690", + "/hmono": "\U0001D691", + "/imono": "\U0001D692", + "/jmono": "\U0001D693", + "/kmono": "\U0001D694", + "/lmono": "\U0001D695", + "/mmono": "\U0001D696", + "/nmono": "\U0001D697", + "/omono": "\U0001D698", + "/pmono": "\U0001D699", + "/qmono": "\U0001D69A", + "/rmono": "\U0001D69B", + "/smono": "\U0001D69C", + "/tmono": "\U0001D69D", + "/umono": "\U0001D69E", + "/vmono": "\U0001D69F", + "/wmono": "\U0001D6A0", + "/xmono": "\U0001D6A1", + "/ymono": "\U0001D6A2", + "/zmono": "\U0001D6A3", + "/dotlessiitalic": "\U0001D6A4", + "/dotlessjitalic": "\U0001D6A5", + "/Alphabold": "\U0001D6A8", + "/Betabold": "\U0001D6A9", + "/Gammabold": "\U0001D6AA", + "/Deltabold": "\U0001D6AB", + "/Epsilonbold": "\U0001D6AC", + "/Zetabold": "\U0001D6AD", + "/Etabold": "\U0001D6AE", + "/Thetabold": "\U0001D6AF", + "/Iotabold": "\U0001D6B0", + "/Kappabold": "\U0001D6B1", + "/Lamdabold": "\U0001D6B2", + "/Mubold": "\U0001D6B3", + "/Nubold": "\U0001D6B4", + "/Xibold": "\U0001D6B5", + "/Omicronbold": "\U0001D6B6", + "/Pibold": "\U0001D6B7", + "/Rhobold": "\U0001D6B8", + "/Thetasymbolbold": "\U0001D6B9", + "/Sigmabold": "\U0001D6BA", + "/Taubold": "\U0001D6BB", + "/Upsilonbold": "\U0001D6BC", + "/Phibold": "\U0001D6BD", + "/Chibold": "\U0001D6BE", + "/Psibold": "\U0001D6BF", + "/Omegabold": "\U0001D6C0", + "/boldnabla": "\U0001D6C1", + "/alphabold": "\U0001D6C2", + "/betabold": "\U0001D6C3", + "/gammabold": "\U0001D6C4", + "/deltabold": "\U0001D6C5", + "/epsilonbold": "\U0001D6C6", + "/zetabold": "\U0001D6C7", + "/etabold": "\U0001D6C8", + "/thetabold": "\U0001D6C9", + "/iotabold": "\U0001D6CA", + "/kappabold": "\U0001D6CB", + "/lamdabold": "\U0001D6CC", + "/mubold": "\U0001D6CD", + "/nubold": "\U0001D6CE", + "/xibold": "\U0001D6CF", + "/omicronbold": "\U0001D6D0", + "/pibold": "\U0001D6D1", + "/rhobold": "\U0001D6D2", + "/finalsigmabold": "\U0001D6D3", + "/sigmabold": "\U0001D6D4", + "/taubold": "\U0001D6D5", + "/upsilonbold": "\U0001D6D6", + "/phibold": "\U0001D6D7", + "/chibold": "\U0001D6D8", + "/psibold": "\U0001D6D9", + "/omegabold": "\U0001D6DA", + "/boldpartialdiff": "\U0001D6DB", + "/epsilonsymbolbold": "\U0001D6DC", + "/thetasymbolbold": "\U0001D6DD", + "/kappasymbolbold": "\U0001D6DE", + "/phisymbolbold": "\U0001D6DF", + "/rhosymbolbold": "\U0001D6E0", + "/pisymbolbold": "\U0001D6E1", + "/Alphaitalic": "\U0001D6E2", + "/Betaitalic": "\U0001D6E3", + "/Gammaitalic": "\U0001D6E4", + "/Deltaitalic": "\U0001D6E5", + "/Epsilonitalic": "\U0001D6E6", + "/Zetaitalic": "\U0001D6E7", + "/Etaitalic": "\U0001D6E8", + "/Thetaitalic": "\U0001D6E9", + "/Iotaitalic": "\U0001D6EA", + "/Kappaitalic": "\U0001D6EB", + "/Lamdaitalic": "\U0001D6EC", + "/Muitalic": "\U0001D6ED", + "/Nuitalic": "\U0001D6EE", + "/Xiitalic": "\U0001D6EF", + "/Omicronitalic": "\U0001D6F0", + "/Piitalic": "\U0001D6F1", + "/Rhoitalic": "\U0001D6F2", + "/Thetasymbolitalic": "\U0001D6F3", + "/Sigmaitalic": "\U0001D6F4", + "/Tauitalic": "\U0001D6F5", + "/Upsilonitalic": "\U0001D6F6", + "/Phiitalic": "\U0001D6F7", + "/Chiitalic": "\U0001D6F8", + "/Psiitalic": "\U0001D6F9", + "/Omegaitalic": "\U0001D6FA", + "/italicnabla": "\U0001D6FB", + "/alphaitalic": "\U0001D6FC", + "/betaitalic": "\U0001D6FD", + "/gammaitalic": "\U0001D6FE", + "/deltaitalic": "\U0001D6FF", + "/epsilonitalic": "\U0001D700", + "/zetaitalic": "\U0001D701", + "/etaitalic": "\U0001D702", + "/thetaitalic": "\U0001D703", + "/iotaitalic": "\U0001D704", + "/kappaitalic": "\U0001D705", + "/lamdaitalic": "\U0001D706", + "/muitalic": "\U0001D707", + "/nuitalic": "\U0001D708", + "/xiitalic": "\U0001D709", + "/omicronitalic": "\U0001D70A", + "/piitalic": "\U0001D70B", + "/rhoitalic": "\U0001D70C", + "/finalsigmaitalic": "\U0001D70D", + "/sigmaitalic": "\U0001D70E", + "/tauitalic": "\U0001D70F", + "/upsilonitalic": "\U0001D710", + "/phiitalic": "\U0001D711", + "/chiitalic": "\U0001D712", + "/psiitalic": "\U0001D713", + "/omegaitalic": "\U0001D714", + "/italicpartialdiff": "\U0001D715", + "/epsilonsymbolitalic": "\U0001D716", + "/thetasymbolitalic": "\U0001D717", + "/kappasymbolitalic": "\U0001D718", + "/phisymbolitalic": "\U0001D719", + "/rhosymbolitalic": "\U0001D71A", + "/pisymbolitalic": "\U0001D71B", + "/Alphabolditalic": "\U0001D71C", + "/Betabolditalic": "\U0001D71D", + "/Gammabolditalic": "\U0001D71E", + "/Deltabolditalic": "\U0001D71F", + "/Epsilonbolditalic": "\U0001D720", + "/Zetabolditalic": "\U0001D721", + "/Etabolditalic": "\U0001D722", + "/Thetabolditalic": "\U0001D723", + "/Iotabolditalic": "\U0001D724", + "/Kappabolditalic": "\U0001D725", + "/Lamdabolditalic": "\U0001D726", + "/Mubolditalic": "\U0001D727", + "/Nubolditalic": "\U0001D728", + "/Xibolditalic": "\U0001D729", + "/Omicronbolditalic": "\U0001D72A", + "/Pibolditalic": "\U0001D72B", + "/Rhobolditalic": "\U0001D72C", + "/Thetasymbolbolditalic": "\U0001D72D", + "/Sigmabolditalic": "\U0001D72E", + "/Taubolditalic": "\U0001D72F", + "/Upsilonbolditalic": "\U0001D730", + "/Phibolditalic": "\U0001D731", + "/Chibolditalic": "\U0001D732", + "/Psibolditalic": "\U0001D733", + "/Omegabolditalic": "\U0001D734", + "/bolditalicnabla": "\U0001D735", + "/alphabolditalic": "\U0001D736", + "/betabolditalic": "\U0001D737", + "/gammabolditalic": "\U0001D738", + "/deltabolditalic": "\U0001D739", + "/epsilonbolditalic": "\U0001D73A", + "/zetabolditalic": "\U0001D73B", + "/etabolditalic": "\U0001D73C", + "/thetabolditalic": "\U0001D73D", + "/iotabolditalic": "\U0001D73E", + "/kappabolditalic": "\U0001D73F", + "/lamdabolditalic": "\U0001D740", + "/mubolditalic": "\U0001D741", + "/nubolditalic": "\U0001D742", + "/xibolditalic": "\U0001D743", + "/omicronbolditalic": "\U0001D744", + "/pibolditalic": "\U0001D745", + "/rhobolditalic": "\U0001D746", + "/finalsigmabolditalic": "\U0001D747", + "/sigmabolditalic": "\U0001D748", + "/taubolditalic": "\U0001D749", + "/upsilonbolditalic": "\U0001D74A", + "/phibolditalic": "\U0001D74B", + "/chibolditalic": "\U0001D74C", + "/psibolditalic": "\U0001D74D", + "/omegabolditalic": "\U0001D74E", + "/bolditalicpartialdiff": "\U0001D74F", + "/epsilonsymbolbolditalic": "\U0001D750", + "/thetasymbolbolditalic": "\U0001D751", + "/kappasymbolbolditalic": "\U0001D752", + "/phisymbolbolditalic": "\U0001D753", + "/rhosymbolbolditalic": "\U0001D754", + "/pisymbolbolditalic": "\U0001D755", + "/Alphaboldsans": "\U0001D756", + "/Betaboldsans": "\U0001D757", + "/Gammaboldsans": "\U0001D758", + "/Deltaboldsans": "\U0001D759", + "/Epsilonboldsans": "\U0001D75A", + "/Zetaboldsans": "\U0001D75B", + "/Etaboldsans": "\U0001D75C", + "/Thetaboldsans": "\U0001D75D", + "/Iotaboldsans": "\U0001D75E", + "/Kappaboldsans": "\U0001D75F", + "/Lamdaboldsans": "\U0001D760", + "/Muboldsans": "\U0001D761", + "/Nuboldsans": "\U0001D762", + "/Xiboldsans": "\U0001D763", + "/Omicronboldsans": "\U0001D764", + "/Piboldsans": "\U0001D765", + "/Rhoboldsans": "\U0001D766", + "/Thetasymbolboldsans": "\U0001D767", + "/Sigmaboldsans": "\U0001D768", + "/Tauboldsans": "\U0001D769", + "/Upsilonboldsans": "\U0001D76A", + "/Phiboldsans": "\U0001D76B", + "/Chiboldsans": "\U0001D76C", + "/Psiboldsans": "\U0001D76D", + "/Omegaboldsans": "\U0001D76E", + "/boldsansnabla": "\U0001D76F", + "/alphaboldsans": "\U0001D770", + "/betaboldsans": "\U0001D771", + "/gammaboldsans": "\U0001D772", + "/deltaboldsans": "\U0001D773", + "/epsilonboldsans": "\U0001D774", + "/zetaboldsans": "\U0001D775", + "/etaboldsans": "\U0001D776", + "/thetaboldsans": "\U0001D777", + "/iotaboldsans": "\U0001D778", + "/kappaboldsans": "\U0001D779", + "/lamdaboldsans": "\U0001D77A", + "/muboldsans": "\U0001D77B", + "/nuboldsans": "\U0001D77C", + "/xiboldsans": "\U0001D77D", + "/omicronboldsans": "\U0001D77E", + "/piboldsans": "\U0001D77F", + "/rhoboldsans": "\U0001D780", + "/finalsigmaboldsans": "\U0001D781", + "/sigmaboldsans": "\U0001D782", + "/tauboldsans": "\U0001D783", + "/upsilonboldsans": "\U0001D784", + "/phiboldsans": "\U0001D785", + "/chiboldsans": "\U0001D786", + "/psiboldsans": "\U0001D787", + "/omegaboldsans": "\U0001D788", + "/boldsanspartialdiff": "\U0001D789", + "/epsilonsymbolboldsans": "\U0001D78A", + "/thetasymbolboldsans": "\U0001D78B", + "/kappasymbolboldsans": "\U0001D78C", + "/phisymbolboldsans": "\U0001D78D", + "/rhosymbolboldsans": "\U0001D78E", + "/pisymbolboldsans": "\U0001D78F", + "/Alphabolditalicsans": "\U0001D790", + "/Betabolditalicsans": "\U0001D791", + "/Gammabolditalicsans": "\U0001D792", + "/Deltabolditalicsans": "\U0001D793", + "/Epsilonbolditalicsans": "\U0001D794", + "/Zetabolditalicsans": "\U0001D795", + "/Etabolditalicsans": "\U0001D796", + "/Thetabolditalicsans": "\U0001D797", + "/Iotabolditalicsans": "\U0001D798", + "/Kappabolditalicsans": "\U0001D799", + "/Lamdabolditalicsans": "\U0001D79A", + "/Mubolditalicsans": "\U0001D79B", + "/Nubolditalicsans": "\U0001D79C", + "/Xibolditalicsans": "\U0001D79D", + "/Omicronbolditalicsans": "\U0001D79E", + "/Pibolditalicsans": "\U0001D79F", + "/Rhobolditalicsans": "\U0001D7A0", + "/Thetasymbolbolditalicsans": "\U0001D7A1", + "/Sigmabolditalicsans": "\U0001D7A2", + "/Taubolditalicsans": "\U0001D7A3", + "/Upsilonbolditalicsans": "\U0001D7A4", + "/Phibolditalicsans": "\U0001D7A5", + "/Chibolditalicsans": "\U0001D7A6", + "/Psibolditalicsans": "\U0001D7A7", + "/Omegabolditalicsans": "\U0001D7A8", + "/bolditalicsansnabla": "\U0001D7A9", + "/alphabolditalicsans": "\U0001D7AA", + "/betabolditalicsans": "\U0001D7AB", + "/gammabolditalicsans": "\U0001D7AC", + "/deltabolditalicsans": "\U0001D7AD", + "/epsilonbolditalicsans": "\U0001D7AE", + "/zetabolditalicsans": "\U0001D7AF", + "/etabolditalicsans": "\U0001D7B0", + "/thetabolditalicsans": "\U0001D7B1", + "/iotabolditalicsans": "\U0001D7B2", + "/kappabolditalicsans": "\U0001D7B3", + "/lamdabolditalicsans": "\U0001D7B4", + "/mubolditalicsans": "\U0001D7B5", + "/nubolditalicsans": "\U0001D7B6", + "/xibolditalicsans": "\U0001D7B7", + "/omicronbolditalicsans": "\U0001D7B8", + "/pibolditalicsans": "\U0001D7B9", + "/rhobolditalicsans": "\U0001D7BA", + "/finalsigmabolditalicsans": "\U0001D7BB", + "/sigmabolditalicsans": "\U0001D7BC", + "/taubolditalicsans": "\U0001D7BD", + "/upsilonbolditalicsans": "\U0001D7BE", + "/phibolditalicsans": "\U0001D7BF", + "/chibolditalicsans": "\U0001D7C0", + "/psibolditalicsans": "\U0001D7C1", + "/omegabolditalicsans": "\U0001D7C2", + "/bolditalicsanspartialdiff": "\U0001D7C3", + "/epsilonsymbolbolditalicsans": "\U0001D7C4", + "/thetasymbolbolditalicsans": "\U0001D7C5", + "/kappasymbolbolditalicsans": "\U0001D7C6", + "/phisymbolbolditalicsans": "\U0001D7C7", + "/rhosymbolbolditalicsans": "\U0001D7C8", + "/pisymbolbolditalicsans": "\U0001D7C9", + "/Digammabold": "\U0001D7CA", + "/digammabold": "\U0001D7CB", + "/zerobold": "\U0001D7CE", + "/onebold": "\U0001D7CF", + "/twobold": "\U0001D7D0", + "/threebold": "\U0001D7D1", + "/fourbold": "\U0001D7D2", + "/fivebold": "\U0001D7D3", + "/sixbold": "\U0001D7D4", + "/sevenbold": "\U0001D7D5", + "/eightbold": "\U0001D7D6", + "/ninebold": "\U0001D7D7", + "/zerodblstruck": "\U0001D7D8", + "/onedblstruck": "\U0001D7D9", + "/twodblstruck": "\U0001D7DA", + "/threedblstruck": "\U0001D7DB", + "/fourdblstruck": "\U0001D7DC", + "/fivedblstruck": "\U0001D7DD", + "/sixdblstruck": "\U0001D7DE", + "/sevendblstruck": "\U0001D7DF", + "/eightdblstruck": "\U0001D7E0", + "/ninedblstruck": "\U0001D7E1", + "/zerosans": "\U0001D7E2", + "/onesans": "\U0001D7E3", + "/twosans": "\U0001D7E4", + "/threesans": "\U0001D7E5", + "/foursans": "\U0001D7E6", + "/fivesans": "\U0001D7E7", + "/sixsans": "\U0001D7E8", + "/sevensans": "\U0001D7E9", + "/eightsans": "\U0001D7EA", + "/ninesans": "\U0001D7EB", + "/zeroboldsans": "\U0001D7EC", + "/oneboldsans": "\U0001D7ED", + "/twoboldsans": "\U0001D7EE", + "/threeboldsans": "\U0001D7EF", + "/fourboldsans": "\U0001D7F0", + "/fiveboldsans": "\U0001D7F1", + "/sixboldsans": "\U0001D7F2", + "/sevenboldsans": "\U0001D7F3", + "/eightboldsans": "\U0001D7F4", + "/nineboldsans": "\U0001D7F5", + "/zeromono": "\U0001D7F6", + "/onemono": "\U0001D7F7", + "/twomono": "\U0001D7F8", + "/threemono": "\U0001D7F9", + "/fourmono": "\U0001D7FA", + "/fivemono": "\U0001D7FB", + "/sixmono": "\U0001D7FC", + "/sevenmono": "\U0001D7FD", + "/eightmono": "\U0001D7FE", + "/ninemono": "\U0001D7FF", + "/ethi:hhya": "\U0001E7E0", + "/ethi:hhyu": "\U0001E7E1", + "/ethi:hhyi": "\U0001E7E2", + "/ethi:hhyaa": "\U0001E7E3", + "/ethi:hhyee": "\U0001E7E4", + "/ethi:hhye": "\U0001E7E5", + "/ethi:hhyo": "\U0001E7E6", + "/ethi:hhwagurage": "\U0001E7E8", + "/ethi:hhwi": "\U0001E7E9", + "/ethi:hhwee": "\U0001E7EA", + "/ethi:hhwe": "\U0001E7EB", + "/ethi:mwigurage": "\U0001E7ED", + "/ethi:mweegurage": "\U0001E7EE", + "/ethi:qwigurage": "\U0001E7F0", + "/ethi:qweegurage": "\U0001E7F1", + "/ethi:qwegurage": "\U0001E7F2", + "/ethi:bwigurage": "\U0001E7F3", + "/ethi:bweegurage": "\U0001E7F4", + "/ethi:kwigurage": "\U0001E7F5", + "/ethi:kweegurage": "\U0001E7F6", + "/ethi:kwegurage": "\U0001E7F7", + "/ethi:gwigurage": "\U0001E7F8", + "/ethi:gweegurage": "\U0001E7F9", + "/ethi:gwegurage": "\U0001E7FA", + "/ethi:fwigurage": "\U0001E7FB", + "/ethi:fweegurage": "\U0001E7FC", + "/ethi:pwigurage": "\U0001E7FD", + "/ethi:pweegurage": "\U0001E7FE", + "/eastWindMahjong": "\U0001F000", + "/southWindMahjong": "\U0001F001", + "/westWindMahjong": "\U0001F002", + "/northWindMahjong": "\U0001F003", + "/redDragonMahjong": "\U0001F004", + "/greenDragonMahjong": "\U0001F005", + "/whiteDragonMahjong": "\U0001F006", + "/oneOfCharactersMahjong": "\U0001F007", + "/twoOfCharactersMahjong": "\U0001F008", + "/threeOfCharactersMahjong": "\U0001F009", + "/fourOfCharactersMahjong": "\U0001F00A", + "/fiveOfCharactersMahjong": "\U0001F00B", + "/sixOfCharactersMahjong": "\U0001F00C", + "/sevenOfCharactersMahjong": "\U0001F00D", + "/eightOfCharactersMahjong": "\U0001F00E", + "/nineOfCharactersMahjong": "\U0001F00F", + "/oneOfBamboosMahjong": "\U0001F010", + "/twoOfBamboosMahjong": "\U0001F011", + "/threeOfBamboosMahjong": "\U0001F012", + "/fourOfBamboosMahjong": "\U0001F013", + "/fiveOfBamboosMahjong": "\U0001F014", + "/sixOfBamboosMahjong": "\U0001F015", + "/sevenOfBamboosMahjong": "\U0001F016", + "/eightOfBamboosMahjong": "\U0001F017", + "/nineOfBamboosMahjong": "\U0001F018", + "/oneOfCirclesMahjong": "\U0001F019", + "/twoOfCirclesMahjong": "\U0001F01A", + "/threeOfCirclesMahjong": "\U0001F01B", + "/fourOfCirclesMahjong": "\U0001F01C", + "/fiveOfCirclesMahjong": "\U0001F01D", + "/sixOfCirclesMahjong": "\U0001F01E", + "/sevenOfCirclesMahjong": "\U0001F01F", + "/eightOfCirclesMahjong": "\U0001F020", + "/nineOfCirclesMahjong": "\U0001F021", + "/plumMahjong": "\U0001F022", + "/orchidMahjong": "\U0001F023", + "/bambooMahjong": "\U0001F024", + "/chrysanthemumMahjong": "\U0001F025", + "/springMahjong": "\U0001F026", + "/summerMahjong": "\U0001F027", + "/autumnMahjong": "\U0001F028", + "/winterMahjong": "\U0001F029", + "/jokerMahjong": "\U0001F02A", + "/backMahjong": "\U0001F02B", + "/dominohorizontalback": "\U0001F030", + "/dominohorizontal_00_00": "\U0001F031", + "/dominohorizontal_00_01": "\U0001F032", + "/dominohorizontal_00_02": "\U0001F033", + "/dominohorizontal_00_03": "\U0001F034", + "/dominohorizontal_00_04": "\U0001F035", + "/dominohorizontal_00_05": "\U0001F036", + "/dominohorizontal_00_06": "\U0001F037", + "/dominohorizontal_01_00": "\U0001F038", + "/dominohorizontal_01_01": "\U0001F039", + "/dominohorizontal_01_02": "\U0001F03A", + "/dominohorizontal_01_03": "\U0001F03B", + "/dominohorizontal_01_04": "\U0001F03C", + "/dominohorizontal_01_05": "\U0001F03D", + "/dominohorizontal_01_06": "\U0001F03E", + "/dominohorizontal_02_00": "\U0001F03F", + "/dominohorizontal_02_01": "\U0001F040", + "/dominohorizontal_02_02": "\U0001F041", + "/dominohorizontal_02_03": "\U0001F042", + "/dominohorizontal_02_04": "\U0001F043", + "/dominohorizontal_02_05": "\U0001F044", + "/dominohorizontal_02_06": "\U0001F045", + "/dominohorizontal_03_00": "\U0001F046", + "/dominohorizontal_03_01": "\U0001F047", + "/dominohorizontal_03_02": "\U0001F048", + "/dominohorizontal_03_03": "\U0001F049", + "/dominohorizontal_03_04": "\U0001F04A", + "/dominohorizontal_03_05": "\U0001F04B", + "/dominohorizontal_03_06": "\U0001F04C", + "/dominohorizontal_04_00": "\U0001F04D", + "/dominohorizontal_04_01": "\U0001F04E", + "/dominohorizontal_04_02": "\U0001F04F", + "/dominohorizontal_04_03": "\U0001F050", + "/dominohorizontal_04_04": "\U0001F051", + "/dominohorizontal_04_05": "\U0001F052", + "/dominohorizontal_04_06": "\U0001F053", + "/dominohorizontal_05_00": "\U0001F054", + "/dominohorizontal_05_01": "\U0001F055", + "/dominohorizontal_05_02": "\U0001F056", + "/dominohorizontal_05_03": "\U0001F057", + "/dominohorizontal_05_04": "\U0001F058", + "/dominohorizontal_05_05": "\U0001F059", + "/dominohorizontal_05_06": "\U0001F05A", + "/dominohorizontal_06_00": "\U0001F05B", + "/dominohorizontal_06_01": "\U0001F05C", + "/dominohorizontal_06_02": "\U0001F05D", + "/dominohorizontal_06_03": "\U0001F05E", + "/dominohorizontal_06_04": "\U0001F05F", + "/dominohorizontal_06_05": "\U0001F060", + "/dominohorizontal_06_06": "\U0001F061", + "/dominoverticalback": "\U0001F062", + "/dominovertical_00_00": "\U0001F063", + "/dominovertical_00_01": "\U0001F064", + "/dominovertical_00_02": "\U0001F065", + "/dominovertical_00_03": "\U0001F066", + "/dominovertical_00_04": "\U0001F067", + "/dominovertical_00_05": "\U0001F068", + "/dominovertical_00_06": "\U0001F069", + "/dominovertical_01_00": "\U0001F06A", + "/dominovertical_01_01": "\U0001F06B", + "/dominovertical_01_02": "\U0001F06C", + "/dominovertical_01_03": "\U0001F06D", + "/dominovertical_01_04": "\U0001F06E", + "/dominovertical_01_05": "\U0001F06F", + "/dominovertical_01_06": "\U0001F070", + "/dominovertical_02_00": "\U0001F071", + "/dominovertical_02_01": "\U0001F072", + "/dominovertical_02_02": "\U0001F073", + "/dominovertical_02_03": "\U0001F074", + "/dominovertical_02_04": "\U0001F075", + "/dominovertical_02_05": "\U0001F076", + "/dominovertical_02_06": "\U0001F077", + "/dominovertical_03_00": "\U0001F078", + "/dominovertical_03_01": "\U0001F079", + "/dominovertical_03_02": "\U0001F07A", + "/dominovertical_03_03": "\U0001F07B", + "/dominovertical_03_04": "\U0001F07C", + "/dominovertical_03_05": "\U0001F07D", + "/dominovertical_03_06": "\U0001F07E", + "/dominovertical_04_00": "\U0001F07F", + "/dominovertical_04_01": "\U0001F080", + "/dominovertical_04_02": "\U0001F081", + "/dominovertical_04_03": "\U0001F082", + "/dominovertical_04_04": "\U0001F083", + "/dominovertical_04_05": "\U0001F084", + "/dominovertical_04_06": "\U0001F085", + "/dominovertical_05_00": "\U0001F086", + "/dominovertical_05_01": "\U0001F087", + "/dominovertical_05_02": "\U0001F088", + "/dominovertical_05_03": "\U0001F089", + "/dominovertical_05_04": "\U0001F08A", + "/dominovertical_05_05": "\U0001F08B", + "/dominovertical_05_06": "\U0001F08C", + "/dominovertical_06_00": "\U0001F08D", + "/dominovertical_06_01": "\U0001F08E", + "/dominovertical_06_02": "\U0001F08F", + "/dominovertical_06_03": "\U0001F090", + "/dominovertical_06_04": "\U0001F091", + "/dominovertical_06_05": "\U0001F092", + "/dominovertical_06_06": "\U0001F093", + "/cards:back": "\U0001F0A0", + "/cards:aceofspades": "\U0001F0A1", + "/cards:twoofspades": "\U0001F0A2", + "/cards:threeofspades": "\U0001F0A3", + "/cards:fourofspades": "\U0001F0A4", + "/cards:fiveofspades": "\U0001F0A5", + "/cards:sixofspades": "\U0001F0A6", + "/cards:sevenofspades": "\U0001F0A7", + "/cards:eightofspades": "\U0001F0A8", + "/cards:nineofspades": "\U0001F0A9", + "/cards:tenofspades": "\U0001F0AA", + "/cards:jackofspades": "\U0001F0AB", + "/cards:knightofspades": "\U0001F0AC", + "/cards:queenofspades": "\U0001F0AD", + "/cards:kingofspades": "\U0001F0AE", + "/cards:aceofhearts": "\U0001F0B1", + "/cards:twoofhearts": "\U0001F0B2", + "/cards:threeofhearts": "\U0001F0B3", + "/cards:fourofhearts": "\U0001F0B4", + "/cards:fiveofhearts": "\U0001F0B5", + "/cards:sixofhearts": "\U0001F0B6", + "/cards:sevenofhearts": "\U0001F0B7", + "/cards:eightofhearts": "\U0001F0B8", + "/cards:nineofhearts": "\U0001F0B9", + "/cards:tenofhearts": "\U0001F0BA", + "/cards:jackofhearts": "\U0001F0BB", + "/cards:knightofhearts": "\U0001F0BC", + "/cards:queenofhearts": "\U0001F0BD", + "/cards:kingofhearts": "\U0001F0BE", + "/cards:redjoker": "\U0001F0BF", + "/cards:aceofdiamonds": "\U0001F0C1", + "/cards:twoofdiamonds": "\U0001F0C2", + "/cards:threeofdiamonds": "\U0001F0C3", + "/cards:fourofdiamonds": "\U0001F0C4", + "/cards:fiveofdiamonds": "\U0001F0C5", + "/cards:sixofdiamonds": "\U0001F0C6", + "/cards:sevenofdiamonds": "\U0001F0C7", + "/cards:eightofdiamonds": "\U0001F0C8", + "/cards:nineofdiamonds": "\U0001F0C9", + "/cards:tenofdiamonds": "\U0001F0CA", + "/cards:jackofdiamonds": "\U0001F0CB", + "/cards:knightofdiamonds": "\U0001F0CC", + "/cards:queenofdiamonds": "\U0001F0CD", + "/cards:kingofdiamonds": "\U0001F0CE", + "/cards:blackjoker": "\U0001F0CF", + "/cards:aceofclubs": "\U0001F0D1", + "/cards:twoofclubs": "\U0001F0D2", + "/cards:threeofclubs": "\U0001F0D3", + "/cards:fourofclubs": "\U0001F0D4", + "/cards:fiveofclubs": "\U0001F0D5", + "/cards:sixofclubs": "\U0001F0D6", + "/cards:sevenofclubs": "\U0001F0D7", + "/cards:eightofclubs": "\U0001F0D8", + "/cards:nineofclubs": "\U0001F0D9", + "/cards:tenofclubs": "\U0001F0DA", + "/cards:jackofclubs": "\U0001F0DB", + "/cards:knightofclubs": "\U0001F0DC", + "/cards:queenofclubs": "\U0001F0DD", + "/cards:kingofclubs": "\U0001F0DE", + "/cards:whitejoker": "\U0001F0DF", + "/cards:fool": "\U0001F0E0", + "/cards:trump1": "\U0001F0E1", + "/cards:trump2": "\U0001F0E2", + "/cards:trump3": "\U0001F0E3", + "/cards:trump4": "\U0001F0E4", + "/cards:trump5": "\U0001F0E5", + "/cards:trump6": "\U0001F0E6", + "/cards:trump7": "\U0001F0E7", + "/cards:trump8": "\U0001F0E8", + "/cards:trump9": "\U0001F0E9", + "/cards:trump10": "\U0001F0EA", + "/cards:trump11": "\U0001F0EB", + "/cards:trump12": "\U0001F0EC", + "/cards:trump13": "\U0001F0ED", + "/cards:trump14": "\U0001F0EE", + "/cards:trump15": "\U0001F0EF", + "/cards:trump16": "\U0001F0F0", + "/cards:trump17": "\U0001F0F1", + "/cards:trump18": "\U0001F0F2", + "/cards:trump19": "\U0001F0F3", + "/cards:trump20": "\U0001F0F4", + "/cards:trump21": "\U0001F0F5", + "/zerofullstop": "\U0001F100", + "/zerocomma": "\U0001F101", + "/onecomma": "\U0001F102", + "/twocomma": "\U0001F103", + "/threecomma": "\U0001F104", + "/fourcomma": "\U0001F105", + "/fivecomma": "\U0001F106", + "/sixcomma": "\U0001F107", + "/sevencomma": "\U0001F108", + "/eightcomma": "\U0001F109", + "/ninecomma": "\U0001F10A", + "/dingbatSAns-serifzerocircle": "\U0001F10B", + "/dingbatSAns-serifzerocircleblack": "\U0001F10C", + "/zerowithslashcircle": "\U0001F10D", + "/anticlockwisearrowcircle": "\U0001F10E", + "/dollarsignwithoverlaidbackslashcircle": "\U0001F10F", + "/Aparens": "\U0001F110", + "/Bparens": "\U0001F111", + "/Cparens": "\U0001F112", + "/Dparens": "\U0001F113", + "/Eparens": "\U0001F114", + "/Fparens": "\U0001F115", + "/Gparens": "\U0001F116", + "/Hparens": "\U0001F117", + "/Iparens": "\U0001F118", + "/Jparens": "\U0001F119", + "/Kparens": "\U0001F11A", + "/Lparens": "\U0001F11B", + "/Mparens": "\U0001F11C", + "/Nparens": "\U0001F11D", + "/Oparens": "\U0001F11E", + "/Pparens": "\U0001F11F", + "/Qparens": "\U0001F120", + "/Rparens": "\U0001F121", + "/Sparens": "\U0001F122", + "/Tparens": "\U0001F123", + "/Uparens": "\U0001F124", + "/Vparens": "\U0001F125", + "/Wparens": "\U0001F126", + "/Xparens": "\U0001F127", + "/Yparens": "\U0001F128", + "/Zparens": "\U0001F129", + "/Sshell": "\U0001F12A", + "/Citaliccircle": "\U0001F12B", + "/Ritaliccircle": "\U0001F12C", + "/CDcircle": "\U0001F12D", + "/WZcircle": "\U0001F12E", + "/copyleftsymbol": "\U0001F12F", + "/Asquare": "\U0001F130", + "/Bsquare": "\U0001F131", + "/Csquare": "\U0001F132", + "/Dsquare": "\U0001F133", + "/Esquare": "\U0001F134", + "/Fsquare": "\U0001F135", + "/Gsquare": "\U0001F136", + "/Hsquare": "\U0001F137", + "/Isquare": "\U0001F138", + "/Jsquare": "\U0001F139", + "/Ksquare": "\U0001F13A", + "/Lsquare": "\U0001F13B", + "/Msquare": "\U0001F13C", + "/Nsquare": "\U0001F13D", + "/Osquare": "\U0001F13E", + "/Psquare": "\U0001F13F", + "/Qsquare": "\U0001F140", + "/Rsquare": "\U0001F141", + "/Ssquare": "\U0001F142", + "/Tsquare": "\U0001F143", + "/Usquare": "\U0001F144", + "/Vsquare": "\U0001F145", + "/Wsquare": "\U0001F146", + "/Xsquare": "\U0001F147", + "/Ysquare": "\U0001F148", + "/Zsquare": "\U0001F149", + "/HVsquare": "\U0001F14A", + "/MVsquare": "\U0001F14B", + "/SDsquare": "\U0001F14C", + "/SSsquare": "\U0001F14D", + "/PPVsquare": "\U0001F14E", + "/wcsquare": "\U0001F14F", + "/Acircleblack": "\U0001F150", + "/Bcircleblack": "\U0001F151", + "/Ccircleblack": "\U0001F152", + "/Dcircleblack": "\U0001F153", + "/Ecircleblack": "\U0001F154", + "/Fcircleblack": "\U0001F155", + "/Gcircleblack": "\U0001F156", + "/Hcircleblack": "\U0001F157", + "/Icircleblack": "\U0001F158", + "/Jcircleblack": "\U0001F159", + "/Kcircleblack": "\U0001F15A", + "/Lcircleblack": "\U0001F15B", + "/Mcircleblack": "\U0001F15C", + "/Ncircleblack": "\U0001F15D", + "/Ocircleblack": "\U0001F15E", + "/Pcircleblack": "\U0001F15F", + "/Qcircleblack": "\U0001F160", + "/Rcircleblack": "\U0001F161", + "/Scircleblack": "\U0001F162", + "/Tcircleblack": "\U0001F163", + "/Ucircleblack": "\U0001F164", + "/Vcircleblack": "\U0001F165", + "/Wcircleblack": "\U0001F166", + "/Xcircleblack": "\U0001F167", + "/Ycircleblack": "\U0001F168", + "/Zcircleblack": "\U0001F169", + "/raisedmcsign": "\U0001F16A", + "/raisedmdsign": "\U0001F16B", + "/raisedmrsign": "\U0001F16C", + "/cccircle": "\U0001F16D", + "/cwithoverlaidbackslashcircle": "\U0001F16E", + "/humanfigurecircle": "\U0001F16F", + "/Asquareblack": "\U0001F170", + "/Bsquareblack": "\U0001F171", + "/Csquareblack": "\U0001F172", + "/Dsquareblack": "\U0001F173", + "/Esquareblack": "\U0001F174", + "/Fsquareblack": "\U0001F175", + "/Gsquareblack": "\U0001F176", + "/Hsquareblack": "\U0001F177", + "/Isquareblack": "\U0001F178", + "/Jsquareblack": "\U0001F179", + "/Ksquareblack": "\U0001F17A", + "/Lsquareblack": "\U0001F17B", + "/Msquareblack": "\U0001F17C", + "/Nsquareblack": "\U0001F17D", + "/Osquareblack": "\U0001F17E", + "/Psquareblack": "\U0001F17F", + "/Qsquareblack": "\U0001F180", + "/Rsquareblack": "\U0001F181", + "/Ssquareblack": "\U0001F182", + "/Tsquareblack": "\U0001F183", + "/Usquareblack": "\U0001F184", + "/Vsquareblack": "\U0001F185", + "/Wsquareblack": "\U0001F186", + "/Xsquareblack": "\U0001F187", + "/Ysquareblack": "\U0001F188", + "/Zsquareblack": "\U0001F189", + "/Pcrosssquareblack": "\U0001F18A", + "/ICsquareblack": "\U0001F18B", + "/PAsquareblack": "\U0001F18C", + "/SAsquareblack": "\U0001F18D", + "/absquareblack": "\U0001F18E", + "/wcsquareblack": "\U0001F18F", + "/squaredj": "\U0001F190", + "/clsquare": "\U0001F191", + "/coolsquare": "\U0001F192", + "/freesquare": "\U0001F193", + "/idsquare": "\U0001F194", + "/newsquare": "\U0001F195", + "/ngsquare": "\U0001F196", + "/oksquare": "\U0001F197", + "/sossquare": "\U0001F198", + "/upwithexclamationmarksquare": "\U0001F199", + "/vssquare": "\U0001F19A", + "/threedsquare": "\U0001F19B", + "/secondscreensquare": "\U0001F19C", + "/twoksquare": "\U0001F19D", + "/fourksquare": "\U0001F19E", + "/eightksquare": "\U0001F19F", + "/fivepointonesquare": "\U0001F1A0", + "/sevenpointonesquare": "\U0001F1A1", + "/twenty-twopointtwosquare": "\U0001F1A2", + "/sixtypsquare": "\U0001F1A3", + "/onehundredtwentypsquare": "\U0001F1A4", + "/dsquare": "\U0001F1A5", + "/hcsquare": "\U0001F1A6", + "/hdrsquare": "\U0001F1A7", + "/hi-ressquare": "\U0001F1A8", + "/losslesssquare": "\U0001F1A9", + "/shvsquare": "\U0001F1AA", + "/uhdsquare": "\U0001F1AB", + "/vodsquare": "\U0001F1AC", + "/maskworksymbol": "\U0001F1AD", + "/regionalindicatorsymbollettera": "\U0001F1E6", + "/regionalindicatorsymbolletterb": "\U0001F1E7", + "/regionalindicatorsymbolletterc": "\U0001F1E8", + "/regionalindicatorsymbolletterd": "\U0001F1E9", + "/regionalindicatorsymbollettere": "\U0001F1EA", + "/regionalindicatorsymbolletterf": "\U0001F1EB", + "/regionalindicatorsymbolletterg": "\U0001F1EC", + "/regionalindicatorsymbolletterh": "\U0001F1ED", + "/regionalindicatorsymbolletteri": "\U0001F1EE", + "/regionalindicatorsymbolletterj": "\U0001F1EF", + "/regionalindicatorsymbolletterk": "\U0001F1F0", + "/regionalindicatorsymbolletterl": "\U0001F1F1", + "/regionalindicatorsymbolletterm": "\U0001F1F2", + "/regionalindicatorsymbollettern": "\U0001F1F3", + "/regionalindicatorsymbollettero": "\U0001F1F4", + "/regionalindicatorsymbolletterp": "\U0001F1F5", + "/regionalindicatorsymbolletterq": "\U0001F1F6", + "/regionalindicatorsymbolletterr": "\U0001F1F7", + "/regionalindicatorsymbolletters": "\U0001F1F8", + "/regionalindicatorsymbollettert": "\U0001F1F9", + "/regionalindicatorsymbolletteru": "\U0001F1FA", + "/regionalindicatorsymbolletterv": "\U0001F1FB", + "/regionalindicatorsymbolletterw": "\U0001F1FC", + "/regionalindicatorsymbolletterx": "\U0001F1FD", + "/regionalindicatorsymbollettery": "\U0001F1FE", + "/regionalindicatorsymbolletterz": "\U0001F1FF", + "/cyclone": "\U0001F300", + "/foggy": "\U0001F301", + "/closedUmbrella": "\U0001F302", + "/nightStars": "\U0001F303", + "/sunriseOverMountains": "\U0001F304", + "/sunrise": "\U0001F305", + "/cityscapeAtDusk": "\U0001F306", + "/sunsetOverBuildings": "\U0001F307", + "/rainbow": "\U0001F308", + "/bridgeAtNight": "\U0001F309", + "/waterWave": "\U0001F30A", + "/volcano": "\U0001F30B", + "/milkyWay": "\U0001F30C", + "/earthGlobeEuropeAfrica": "\U0001F30D", + "/earthGlobeAmericas": "\U0001F30E", + "/earthGlobeAsiaAustralia": "\U0001F30F", + "/globeMeridians": "\U0001F310", + "/newMoon": "\U0001F311", + "/waxingCrescentMoon": "\U0001F312", + "/firstQuarterMoon": "\U0001F313", + "/waxingGibbousMoon": "\U0001F314", + "/fullMoon": "\U0001F315", + "/waningGibbousMoon": "\U0001F316", + "/lastQuarterMoon": "\U0001F317", + "/waningCrescentMoon": "\U0001F318", + "/crescentMoon": "\U0001F319", + "/newMoonFace": "\U0001F31A", + "/firstQuarterMoonFace": "\U0001F31B", + "/lastQuarterMoonFace": "\U0001F31C", + "/fullMoonFace": "\U0001F31D", + "/sunFace": "\U0001F31E", + "/glowingStar": "\U0001F31F", + "/shootingStar": "\U0001F320", + "/thermometer": "\U0001F321", + "/blackDroplet": "\U0001F322", + "/whiteSun": "\U0001F323", + "/whiteSunSmallCloud": "\U0001F324", + "/whiteSunBehindCloud": "\U0001F325", + "/whiteSunBehindCloudRain": "\U0001F326", + "/cloudRain": "\U0001F327", + "/cloudSnow": "\U0001F328", + "/cloudLightning": "\U0001F329", + "/cloudTornado": "\U0001F32A", + "/fog": "\U0001F32B", + "/windBlowingFace": "\U0001F32C", + "/hotDog": "\U0001F32D", + "/taco": "\U0001F32E", + "/burrito": "\U0001F32F", + "/chestnut": "\U0001F330", + "/seedling": "\U0001F331", + "/evergreenTree": "\U0001F332", + "/deciduousTree": "\U0001F333", + "/palmTree": "\U0001F334", + "/cactus": "\U0001F335", + "/hotPepper": "\U0001F336", + "/tulip": "\U0001F337", + "/cherryBlossom": "\U0001F338", + "/rose": "\U0001F339", + "/hibiscus": "\U0001F33A", + "/sunflower": "\U0001F33B", + "/blossom": "\U0001F33C", + "/earOfMaize": "\U0001F33D", + "/earOfRice": "\U0001F33E", + "/herb": "\U0001F33F", + "/fourLeafClover": "\U0001F340", + "/mapleLeaf": "\U0001F341", + "/fallenLeaf": "\U0001F342", + "/leafFlutteringInWind": "\U0001F343", + "/mushroom": "\U0001F344", + "/tomato": "\U0001F345", + "/aubergine": "\U0001F346", + "/grapes": "\U0001F347", + "/melon": "\U0001F348", + "/watermelon": "\U0001F349", + "/tangerine": "\U0001F34A", + "/lemon": "\U0001F34B", + "/banana": "\U0001F34C", + "/pineapple": "\U0001F34D", + "/redApple": "\U0001F34E", + "/greenApple": "\U0001F34F", + "/pear": "\U0001F350", + "/peach": "\U0001F351", + "/cherries": "\U0001F352", + "/strawberry": "\U0001F353", + "/hamburger": "\U0001F354", + "/sliceOfPizza": "\U0001F355", + "/meatOnBone": "\U0001F356", + "/poultryLeg": "\U0001F357", + "/riceCracker": "\U0001F358", + "/riceBall": "\U0001F359", + "/cookedRice": "\U0001F35A", + "/curryAndRice": "\U0001F35B", + "/steamingBowl": "\U0001F35C", + "/spaghetti": "\U0001F35D", + "/bread": "\U0001F35E", + "/frenchFries": "\U0001F35F", + "/roastedSweetPotato": "\U0001F360", + "/dango": "\U0001F361", + "/oden": "\U0001F362", + "/sushi": "\U0001F363", + "/friedShrimp": "\U0001F364", + "/fishCakeSwirlDesign": "\U0001F365", + "/softIceCream": "\U0001F366", + "/shavedIce": "\U0001F367", + "/iceCream": "\U0001F368", + "/doughnut": "\U0001F369", + "/cookie": "\U0001F36A", + "/chocolateBar": "\U0001F36B", + "/candy": "\U0001F36C", + "/lollipop": "\U0001F36D", + "/custard": "\U0001F36E", + "/honeyPot": "\U0001F36F", + "/shortcake": "\U0001F370", + "/bentoBox": "\U0001F371", + "/potOfFood": "\U0001F372", + "/cooking": "\U0001F373", + "/forkKnife": "\U0001F374", + "/teacupOutHandle": "\U0001F375", + "/sakeBottleAndCup": "\U0001F376", + "/wineGlass": "\U0001F377", + "/cocktailGlass": "\U0001F378", + "/tropicalDrink": "\U0001F379", + "/beerMug": "\U0001F37A", + "/clinkingBeerMugs": "\U0001F37B", + "/babyBottle": "\U0001F37C", + "/forkKnifePlate": "\U0001F37D", + "/bottlePoppingCork": "\U0001F37E", + "/popcorn": "\U0001F37F", + "/ribbon": "\U0001F380", + "/wrappedPresent": "\U0001F381", + "/birthdayCake": "\U0001F382", + "/jackOLantern": "\U0001F383", + "/christmasTree": "\U0001F384", + "/fatherChristmas": "\U0001F385", + "/fireworks": "\U0001F386", + "/fireworkSparkler": "\U0001F387", + "/balloon": "\U0001F388", + "/partyPopper": "\U0001F389", + "/confettiBall": "\U0001F38A", + "/tanabataTree": "\U0001F38B", + "/crossedFlags": "\U0001F38C", + "/pineDecoration": "\U0001F38D", + "/japaneseDolls": "\U0001F38E", + "/carpStreamer": "\U0001F38F", + "/windChime": "\U0001F390", + "/moonViewingCeremony": "\U0001F391", + "/schoolSatchel": "\U0001F392", + "/graduationCap": "\U0001F393", + "/heartTipOnTheLeft": "\U0001F394", + "/bouquetOfFlowers": "\U0001F395", + "/militaryMedal": "\U0001F396", + "/reminderRibbon": "\U0001F397", + "/musicalKeyboardJacks": "\U0001F398", + "/studioMicrophone": "\U0001F399", + "/levelSlider": "\U0001F39A", + "/controlKnobs": "\U0001F39B", + "/beamedAscendingMusicalNotes": "\U0001F39C", + "/beamedDescendingMusicalNotes": "\U0001F39D", + "/filmFrames": "\U0001F39E", + "/admissionTickets": "\U0001F39F", + "/carouselHorse": "\U0001F3A0", + "/ferrisWheel": "\U0001F3A1", + "/rollerCoaster": "\U0001F3A2", + "/fishingPoleAndFish": "\U0001F3A3", + "/microphone": "\U0001F3A4", + "/movieCamera": "\U0001F3A5", + "/cinema": "\U0001F3A6", + "/headphone": "\U0001F3A7", + "/artistPalette": "\U0001F3A8", + "/topHat": "\U0001F3A9", + "/circusTent": "\U0001F3AA", + "/ticket": "\U0001F3AB", + "/clapperBoard": "\U0001F3AC", + "/performingArts": "\U0001F3AD", + "/videoGame": "\U0001F3AE", + "/directHit": "\U0001F3AF", + "/slotMachine": "\U0001F3B0", + "/billiards": "\U0001F3B1", + "/gameDie": "\U0001F3B2", + "/bowling": "\U0001F3B3", + "/flowerPlayingCards": "\U0001F3B4", + "/musicalNote": "\U0001F3B5", + "/multipleMusicalNotes": "\U0001F3B6", + "/saxophone": "\U0001F3B7", + "/guitar": "\U0001F3B8", + "/musicalKeyboard": "\U0001F3B9", + "/trumpet": "\U0001F3BA", + "/violin": "\U0001F3BB", + "/musicalScore": "\U0001F3BC", + "/runningShirtSash": "\U0001F3BD", + "/tennisRacquetAndBall": "\U0001F3BE", + "/skiAndSkiBoot": "\U0001F3BF", + "/basketballAndHoop": "\U0001F3C0", + "/chequeredFlag": "\U0001F3C1", + "/snowboarder": "\U0001F3C2", + "/runner": "\U0001F3C3", + "/surfer": "\U0001F3C4", + "/sportsMedal": "\U0001F3C5", + "/trophy": "\U0001F3C6", + "/horseRacing": "\U0001F3C7", + "/americanFootball": "\U0001F3C8", + "/rugbyFootball": "\U0001F3C9", + "/swimmer": "\U0001F3CA", + "/weightLifter": "\U0001F3CB", + "/golfer": "\U0001F3CC", + "/racingMotorcycle": "\U0001F3CD", + "/racingCar": "\U0001F3CE", + "/cricketBatAndBall": "\U0001F3CF", + "/volleyball": "\U0001F3D0", + "/fieldHockeyStickAndBall": "\U0001F3D1", + "/iceHockeyStickAndPuck": "\U0001F3D2", + "/tableTennisPaddleAndBall": "\U0001F3D3", + "/snowcappedMountain": "\U0001F3D4", + "/camping": "\U0001F3D5", + "/beachUmbrella": "\U0001F3D6", + "/buildingConstruction": "\U0001F3D7", + "/houseBuildings": "\U0001F3D8", + "/cityscape": "\U0001F3D9", + "/derelictHouseBuilding": "\U0001F3DA", + "/classicalBuilding": "\U0001F3DB", + "/desert": "\U0001F3DC", + "/desertIsland": "\U0001F3DD", + "/nationalPark": "\U0001F3DE", + "/stadium": "\U0001F3DF", + "/houseBuilding": "\U0001F3E0", + "/houseGarden": "\U0001F3E1", + "/officeBuilding": "\U0001F3E2", + "/japanesePostOffice": "\U0001F3E3", + "/europeanPostOffice": "\U0001F3E4", + "/hospital": "\U0001F3E5", + "/bank": "\U0001F3E6", + "/automatedTellerMachine": "\U0001F3E7", + "/hotel": "\U0001F3E8", + "/loveHotel": "\U0001F3E9", + "/convenienceStore": "\U0001F3EA", + "/school": "\U0001F3EB", + "/departmentStore": "\U0001F3EC", + "/factory": "\U0001F3ED", + "/izakayaLantern": "\U0001F3EE", + "/japaneseCastle": "\U0001F3EF", + "/europeanCastle": "\U0001F3F0", + "/whitePennant": "\U0001F3F1", + "/blackPennant": "\U0001F3F2", + "/wavingWhiteFlag": "\U0001F3F3", + "/wavingBlackFlag": "\U0001F3F4", + "/rosette": "\U0001F3F5", + "/blackRosette": "\U0001F3F6", + "/label": "\U0001F3F7", + "/badmintonRacquetAndShuttlecock": "\U0001F3F8", + "/bowAndArrow": "\U0001F3F9", + "/amphora": "\U0001F3FA", + "/fitzPatrickType12": "\U0001F3FB", + "/fitzPatrickType3": "\U0001F3FC", + "/fitzPatrickType4": "\U0001F3FD", + "/fitzPatrickType5": "\U0001F3FE", + "/fitzPatrickType6": "\U0001F3FF", + "/rat": "\U0001F400", + "/mouse": "\U0001F401", + "/ox": "\U0001F402", + "/waterBuffalo": "\U0001F403", + "/cow": "\U0001F404", + "/tiger": "\U0001F405", + "/leopard": "\U0001F406", + "/rabbit": "\U0001F407", + "/cat": "\U0001F408", + "/dragon": "\U0001F409", + "/crocodile": "\U0001F40A", + "/whale": "\U0001F40B", + "/snail": "\U0001F40C", + "/snake": "\U0001F40D", + "/horse": "\U0001F40E", + "/ram": "\U0001F40F", + "/goat": "\U0001F410", + "/sheep": "\U0001F411", + "/monkey": "\U0001F412", + "/rooster": "\U0001F413", + "/chicken": "\U0001F414", + "/dog": "\U0001F415", + "/pig": "\U0001F416", + "/boar": "\U0001F417", + "/elephant": "\U0001F418", + "/octopus": "\U0001F419", + "/spiralShell": "\U0001F41A", + "/bug": "\U0001F41B", + "/ant": "\U0001F41C", + "/honeybee": "\U0001F41D", + "/ladyBeetle": "\U0001F41E", + "/fish": "\U0001F41F", + "/tropicalFish": "\U0001F420", + "/blowfish": "\U0001F421", + "/turtle": "\U0001F422", + "/hatchingChick": "\U0001F423", + "/babyChick": "\U0001F424", + "/frontFacingBabyChick": "\U0001F425", + "/bird": "\U0001F426", + "/penguin": "\U0001F427", + "/koala": "\U0001F428", + "/poodle": "\U0001F429", + "/dromedaryCamel": "\U0001F42A", + "/bactrianCamel": "\U0001F42B", + "/dolphin": "\U0001F42C", + "/mouseFace": "\U0001F42D", + "/cowFace": "\U0001F42E", + "/tigerFace": "\U0001F42F", + "/rabbitFace": "\U0001F430", + "/catFace": "\U0001F431", + "/dragonFace": "\U0001F432", + "/spoutingWhale": "\U0001F433", + "/horseFace": "\U0001F434", + "/monkeyFace": "\U0001F435", + "/dogFace": "\U0001F436", + "/pigFace": "\U0001F437", + "/frogFace": "\U0001F438", + "/hamsterFace": "\U0001F439", + "/wolfFace": "\U0001F43A", + "/bearFace": "\U0001F43B", + "/pandaFace": "\U0001F43C", + "/pigNose": "\U0001F43D", + "/pawPrints": "\U0001F43E", + "/chipmunk": "\U0001F43F", + "/eyes": "\U0001F440", + "/eye": "\U0001F441", + "/ear": "\U0001F442", + "/nose": "\U0001F443", + "/mouth": "\U0001F444", + "/tongue": "\U0001F445", + "/whiteUpPointingBackhandIndex": "\U0001F446", + "/whiteDownPointingBackhandIndex": "\U0001F447", + "/whiteLeftPointingBackhandIndex": "\U0001F448", + "/whiteRightPointingBackhandIndex": "\U0001F449", + "/fistedHandSign": "\U0001F44A", + "/wavingHandSign": "\U0001F44B", + "/okHandSign": "\U0001F44C", + "/thumbsUpSign": "\U0001F44D", + "/thumbsDownSign": "\U0001F44E", + "/clappingHandsSign": "\U0001F44F", + "/openHandsSign": "\U0001F450", + "/crown": "\U0001F451", + "/womansHat": "\U0001F452", + "/eyeglasses": "\U0001F453", + "/necktie": "\U0001F454", + "/tShirt": "\U0001F455", + "/jeans": "\U0001F456", + "/dress": "\U0001F457", + "/kimono": "\U0001F458", + "/bikini": "\U0001F459", + "/womansClothes": "\U0001F45A", + "/purse": "\U0001F45B", + "/handbag": "\U0001F45C", + "/pouch": "\U0001F45D", + "/mansShoe": "\U0001F45E", + "/athleticShoe": "\U0001F45F", + "/highHeeledShoe": "\U0001F460", + "/womansSandal": "\U0001F461", + "/womansBoots": "\U0001F462", + "/footprints": "\U0001F463", + "/bustInSilhouette": "\U0001F464", + "/bustsInSilhouette": "\U0001F465", + "/boy": "\U0001F466", + "/girl": "\U0001F467", + "/man": "\U0001F468", + "/woman": "\U0001F469", + "/family": "\U0001F46A", + "/manAndWomanHoldingHands": "\U0001F46B", + "/twoMenHoldingHands": "\U0001F46C", + "/twoWomenHoldingHands": "\U0001F46D", + "/policeOfficer": "\U0001F46E", + "/womanBunnyEars": "\U0001F46F", + "/brideVeil": "\U0001F470", + "/personBlondHair": "\U0001F471", + "/manGuaPiMao": "\U0001F472", + "/manTurban": "\U0001F473", + "/olderMan": "\U0001F474", + "/olderWoman": "\U0001F475", + "/misc:baby": "\U0001F476", + "/constructionWorker": "\U0001F477", + "/princess": "\U0001F478", + "/japaneseOgre": "\U0001F479", + "/japaneseGoblin": "\U0001F47A", + "/ghost": "\U0001F47B", + "/babyAngel": "\U0001F47C", + "/extraterrestrialAlien": "\U0001F47D", + "/alienMonster": "\U0001F47E", + "/imp": "\U0001F47F", + "/skull": "\U0001F480", + "/inmationDeskPerson": "\U0001F481", + "/guardsman": "\U0001F482", + "/dancer": "\U0001F483", + "/lipstick": "\U0001F484", + "/nailPolish": "\U0001F485", + "/faceMassage": "\U0001F486", + "/haircut": "\U0001F487", + "/barberPole": "\U0001F488", + "/syringe": "\U0001F489", + "/pill": "\U0001F48A", + "/kissMark": "\U0001F48B", + "/loveLetter": "\U0001F48C", + "/misc:ring": "\U0001F48D", + "/gemStone": "\U0001F48E", + "/kiss": "\U0001F48F", + "/bouquet": "\U0001F490", + "/coupleHeart": "\U0001F491", + "/wedding": "\U0001F492", + "/beatingHeart": "\U0001F493", + "/brokenHeart": "\U0001F494", + "/twoHearts": "\U0001F495", + "/sparklingHeart": "\U0001F496", + "/growingHeart": "\U0001F497", + "/heartArrow": "\U0001F498", + "/blueHeart": "\U0001F499", + "/greenHeart": "\U0001F49A", + "/yellowHeart": "\U0001F49B", + "/purpleHeart": "\U0001F49C", + "/heartRibbon": "\U0001F49D", + "/revolvingHearts": "\U0001F49E", + "/heartDecoration": "\U0001F49F", + "/diamondShapeADotInside": "\U0001F4A0", + "/electricLightBulb": "\U0001F4A1", + "/anger": "\U0001F4A2", + "/bomb": "\U0001F4A3", + "/sleeping": "\U0001F4A4", + "/collision": "\U0001F4A5", + "/splashingSweat": "\U0001F4A6", + "/droplet": "\U0001F4A7", + "/misc:dash": "\U0001F4A8", + "/pileOfPoo": "\U0001F4A9", + "/flexedBiceps": "\U0001F4AA", + "/dizzy": "\U0001F4AB", + "/speechBalloon": "\U0001F4AC", + "/thoughtBalloon": "\U0001F4AD", + "/whiteFlower": "\U0001F4AE", + "/hundredPoints": "\U0001F4AF", + "/moneyBag": "\U0001F4B0", + "/currencyExchange": "\U0001F4B1", + "/heavyDollarSign": "\U0001F4B2", + "/creditCard": "\U0001F4B3", + "/banknoteYenSign": "\U0001F4B4", + "/banknoteDollarSign": "\U0001F4B5", + "/banknoteEuroSign": "\U0001F4B6", + "/banknotePoundSign": "\U0001F4B7", + "/moneyWings": "\U0001F4B8", + "/chartUpTrendAndYenSign": "\U0001F4B9", + "/seat": "\U0001F4BA", + "/personalComputer": "\U0001F4BB", + "/briefcase": "\U0001F4BC", + "/minidisc": "\U0001F4BD", + "/floppyDisk": "\U0001F4BE", + "/opticalDisc": "\U0001F4BF", + "/dvd": "\U0001F4C0", + "/fileFolder": "\U0001F4C1", + "/openFileFolder": "\U0001F4C2", + "/pageCurl": "\U0001F4C3", + "/pageFacingUp": "\U0001F4C4", + "/calendar": "\U0001F4C5", + "/tearOffCalendar": "\U0001F4C6", + "/cardIndex": "\U0001F4C7", + "/chartUpTrend": "\U0001F4C8", + "/chartDownTrend": "\U0001F4C9", + "/barChart": "\U0001F4CA", + "/clipboard": "\U0001F4CB", + "/pushpin": "\U0001F4CC", + "/roundPushpin": "\U0001F4CD", + "/paperclip": "\U0001F4CE", + "/straightRuler": "\U0001F4CF", + "/triangularRuler": "\U0001F4D0", + "/bookmarkTabs": "\U0001F4D1", + "/ledger": "\U0001F4D2", + "/notebook": "\U0001F4D3", + "/notebookDecorativeCover": "\U0001F4D4", + "/closedBook": "\U0001F4D5", + "/openBook": "\U0001F4D6", + "/greenBook": "\U0001F4D7", + "/blueBook": "\U0001F4D8", + "/orangeBook": "\U0001F4D9", + "/books": "\U0001F4DA", + "/nameBadge": "\U0001F4DB", + "/scroll": "\U0001F4DC", + "/memo": "\U0001F4DD", + "/telephoneReceiver": "\U0001F4DE", + "/pager": "\U0001F4DF", + "/faxMachine": "\U0001F4E0", + "/satelliteAntenna": "\U0001F4E1", + "/publicAddressLoudspeaker": "\U0001F4E2", + "/cheeringMegaphone": "\U0001F4E3", + "/outboxTray": "\U0001F4E4", + "/inboxTray": "\U0001F4E5", + "/package": "\U0001F4E6", + "/eMail": "\U0001F4E7", + "/incomingEnvelope": "\U0001F4E8", + "/envelopeDownArrowAbove": "\U0001F4E9", + "/closedMailboxLoweredFlag": "\U0001F4EA", + "/closedMailboxRaisedFlag": "\U0001F4EB", + "/openMailboxRaisedFlag": "\U0001F4EC", + "/openMailboxLoweredFlag": "\U0001F4ED", + "/postbox": "\U0001F4EE", + "/postalHorn": "\U0001F4EF", + "/newspaper": "\U0001F4F0", + "/mobilePhone": "\U0001F4F1", + "/mobilePhoneRightwardsArrowAtLeft": "\U0001F4F2", + "/vibrationMode": "\U0001F4F3", + "/mobilePhoneOff": "\U0001F4F4", + "/noMobilePhones": "\U0001F4F5", + "/antennaBars": "\U0001F4F6", + "/camera": "\U0001F4F7", + "/cameraFlash": "\U0001F4F8", + "/videoCamera": "\U0001F4F9", + "/television": "\U0001F4FA", + "/radio": "\U0001F4FB", + "/videocassette": "\U0001F4FC", + "/filmProjector": "\U0001F4FD", + "/portableStereo": "\U0001F4FE", + "/prayerBeads": "\U0001F4FF", + "/twistedRightwardsArrows": "\U0001F500", + "/clockwiseRightwardsAndLeftwardsOpenCircleArrows": "\U0001F501", + "/clockwiseRightwardsAndLeftwardsOpenCircleArrowsCircledOneOverlay": "\U0001F502", + "/clockwiseDownAndUpOpenCircleArrows": "\U0001F503", + "/anticlockwiseDownAndUpOpenCircleArrows": "\U0001F504", + "/lowBrightness": "\U0001F505", + "/highBrightness": "\U0001F506", + "/speakerCancellationStroke": "\U0001F507", + "/speaker": "\U0001F508", + "/speakerOneSoundWave": "\U0001F509", + "/speakerThreeSoundWaves": "\U0001F50A", + "/battery": "\U0001F50B", + "/electricPlug": "\U0001F50C", + "/leftPointingMagnifyingGlass": "\U0001F50D", + "/rightPointingMagnifyingGlass": "\U0001F50E", + "/lockInkPen": "\U0001F50F", + "/closedLockKey": "\U0001F510", + "/key": "\U0001F511", + "/lock": "\U0001F512", + "/openLock": "\U0001F513", + "/bellsymbol": "\U0001F514", + "/bellsymbolCancellationStroke": "\U0001F515", + "/bookmark": "\U0001F516", + "/link": "\U0001F517", + "/radioButton": "\U0001F518", + "/backLeftwardsArrowAbove": "\U0001F519", + "/endLeftwardsArrowAbove": "\U0001F51A", + "/onExclamationMarkLeftRightArrowAbove": "\U0001F51B", + "/soonRightwardsArrowAbove": "\U0001F51C", + "/topUpArrowAbove": "\U0001F51D", + "/noOneUnderEighteen": "\U0001F51E", + "/keycapTen": "\U0001F51F", + "/inputCapLatin": "\U0001F520", + "/inputSmallLatin": "\U0001F521", + "/inputNumbers": "\U0001F522", + "/inputSymbols": "\U0001F523", + "/inputLatin": "\U0001F524", + "/fire": "\U0001F525", + "/electricTorch": "\U0001F526", + "/wrench": "\U0001F527", + "/hammer": "\U0001F528", + "/nutAndBolt": "\U0001F529", + "/hocho": "\U0001F52A", + "/pistol": "\U0001F52B", + "/microscope": "\U0001F52C", + "/telescope": "\U0001F52D", + "/crystalBall": "\U0001F52E", + "/sixPointedStarMiddleDot": "\U0001F52F", + "/japaneseBeginner": "\U0001F530", + "/tridentEmblem": "\U0001F531", + "/blackSquareButton": "\U0001F532", + "/whiteSquareButton": "\U0001F533", + "/largeRedCircle": "\U0001F534", + "/largeBlueCircle": "\U0001F535", + "/largeOrangeDiamond": "\U0001F536", + "/largeBlueDiamond": "\U0001F537", + "/smallOrangeDiamond": "\U0001F538", + "/smallBlueDiamond": "\U0001F539", + "/upPointingRedTriangle": "\U0001F53A", + "/downPointingRedTriangle": "\U0001F53B", + "/upPointingSmallRedTriangle": "\U0001F53C", + "/downPointingSmallRedTriangle": "\U0001F53D", + "/lowerRightShadowedWhiteCircle": "\U0001F53E", + "/upperRightShadowedWhiteCircle": "\U0001F53F", + "/circledCrossPommee": "\U0001F540", + "/crossPommeeHalfCircleBelow": "\U0001F541", + "/crossPommee": "\U0001F542", + "/notchedLeftSemicircleThreeDots": "\U0001F543", + "/notchedRightSemicircleThreeDots": "\U0001F544", + "/marksChapter": "\U0001F545", + "/whiteLatinCross": "\U0001F546", + "/heavyLatinCross": "\U0001F547", + "/celticCross": "\U0001F548", + "/om": "\U0001F549", + "/doveOfPeace": "\U0001F54A", + "/kaaba": "\U0001F54B", + "/mosque": "\U0001F54C", + "/synagogue": "\U0001F54D", + "/menorahNineBranches": "\U0001F54E", + "/bowlOfHygieia": "\U0001F54F", + "/clockOne": "\U0001F550", + "/clockTwo": "\U0001F551", + "/clockThree": "\U0001F552", + "/clockFour": "\U0001F553", + "/clockFive": "\U0001F554", + "/clockSix": "\U0001F555", + "/clockSeven": "\U0001F556", + "/clockEight": "\U0001F557", + "/clockNine": "\U0001F558", + "/clockTen": "\U0001F559", + "/clockEleven": "\U0001F55A", + "/clockTwelve": "\U0001F55B", + "/clockOneThirty": "\U0001F55C", + "/clockTwoThirty": "\U0001F55D", + "/clockThreeThirty": "\U0001F55E", + "/clockFourThirty": "\U0001F55F", + "/clockFiveThirty": "\U0001F560", + "/clockSixThirty": "\U0001F561", + "/clockSevenThirty": "\U0001F562", + "/clockEightThirty": "\U0001F563", + "/clockNineThirty": "\U0001F564", + "/clockTenThirty": "\U0001F565", + "/clockElevenThirty": "\U0001F566", + "/clockTwelveThirty": "\U0001F567", + "/rightSpeaker": "\U0001F568", + "/rightSpeakerOneSoundWave": "\U0001F569", + "/rightSpeakerThreeSoundWaves": "\U0001F56A", + "/bullhorn": "\U0001F56B", + "/bullhornSoundWaves": "\U0001F56C", + "/ringingBellsymbol": "\U0001F56D", + "/book": "\U0001F56E", + "/candle": "\U0001F56F", + "/mantelpieceClock": "\U0001F570", + "/blackSkullAndCrossbones": "\U0001F571", + "/noPiracy": "\U0001F572", + "/hole": "\U0001F573", + "/manInBusinessSuitLevitating": "\U0001F574", + "/sleuthOrSpy": "\U0001F575", + "/darkSunglasses": "\U0001F576", + "/spider": "\U0001F577", + "/spiderWeb": "\U0001F578", + "/joystick": "\U0001F579", + "/manDancing": "\U0001F57A", + "/leftHandTelephoneReceiver": "\U0001F57B", + "/telephoneReceiverPage": "\U0001F57C", + "/rightHandTelephoneReceiver": "\U0001F57D", + "/whiteTouchtoneTelephone": "\U0001F57E", + "/blackTouchtoneTelephone": "\U0001F57F", + "/telephoneOnTopOfModem": "\U0001F580", + "/clamshellMobilePhone": "\U0001F581", + "/backOfEnvelope": "\U0001F582", + "/stampedEnvelope": "\U0001F583", + "/envelopeLightning": "\U0001F584", + "/flyingEnvelope": "\U0001F585", + "/penOverStampedEnvelope": "\U0001F586", + "/linkedPaperclips": "\U0001F587", + "/blackPushpin": "\U0001F588", + "/lowerLeftPencil": "\U0001F589", + "/lowerLeftBallpointPen": "\U0001F58A", + "/lowerLeftFountainPen": "\U0001F58B", + "/lowerLeftPaintbrush": "\U0001F58C", + "/lowerLeftCrayon": "\U0001F58D", + "/leftWritingHand": "\U0001F58E", + "/turnedOkHandSign": "\U0001F58F", + "/raisedHandFingersSplayed": "\U0001F590", + "/reversedRaisedHandFingersSplayed": "\U0001F591", + "/reversedThumbsUpSign": "\U0001F592", + "/reversedThumbsDownSign": "\U0001F593", + "/reversedVictoryHand": "\U0001F594", + "/reversedHandMiddleFingerExtended": "\U0001F595", + "/raisedHandPartBetweenMiddleAndRingFingers": "\U0001F596", + "/whiteDownPointingLeftHandIndex": "\U0001F597", + "/sidewaysWhiteLeftPointingIndex": "\U0001F598", + "/sidewaysWhiteRightPointingIndex": "\U0001F599", + "/sidewaysBlackLeftPointingIndex": "\U0001F59A", + "/sidewaysBlackRightPointingIndex": "\U0001F59B", + "/blackLeftPointingBackhandIndex": "\U0001F59C", + "/blackRightPointingBackhandIndex": "\U0001F59D", + "/sidewaysWhiteUpPointingIndex": "\U0001F59E", + "/sidewaysWhiteDownPointingIndex": "\U0001F59F", + "/sidewaysBlackUpPointingIndex": "\U0001F5A0", + "/sidewaysBlackDownPointingIndex": "\U0001F5A1", + "/blackUpPointingBackhandIndex": "\U0001F5A2", + "/blackDownPointingBackhandIndex": "\U0001F5A3", + "/blackHeart": "\U0001F5A4", + "/desktopComputer": "\U0001F5A5", + "/keyboardAndMouse": "\U0001F5A6", + "/threeNetworkedComputers": "\U0001F5A7", + "/printer": "\U0001F5A8", + "/pocketCalculator": "\U0001F5A9", + "/blackHardShellFloppyDisk": "\U0001F5AA", + "/whiteHardShellFloppyDisk": "\U0001F5AB", + "/softShellFloppyDisk": "\U0001F5AC", + "/tapeCartridge": "\U0001F5AD", + "/wiredKeyboard": "\U0001F5AE", + "/oneButtonMouse": "\U0001F5AF", + "/twoButtonMouse": "\U0001F5B0", + "/threeButtonMouse": "\U0001F5B1", + "/trackball": "\U0001F5B2", + "/oldPersonalComputer": "\U0001F5B3", + "/hardDisk": "\U0001F5B4", + "/screen": "\U0001F5B5", + "/printerIcon": "\U0001F5B6", + "/faxIcon": "\U0001F5B7", + "/opticalDiscIcon": "\U0001F5B8", + "/documentText": "\U0001F5B9", + "/documentTextAndPicture": "\U0001F5BA", + "/documentPicture": "\U0001F5BB", + "/framePicture": "\U0001F5BC", + "/frameTiles": "\U0001F5BD", + "/frameAnX": "\U0001F5BE", + "/blackFolder": "\U0001F5BF", + "/folder": "\U0001F5C0", + "/openFolder": "\U0001F5C1", + "/cardIndexDividers": "\U0001F5C2", + "/cardFileBox": "\U0001F5C3", + "/fileCabinet": "\U0001F5C4", + "/emptyNote": "\U0001F5C5", + "/emptyNotePage": "\U0001F5C6", + "/emptyNotePad": "\U0001F5C7", + "/note": "\U0001F5C8", + "/notePage": "\U0001F5C9", + "/notePad": "\U0001F5CA", + "/emptyDocument": "\U0001F5CB", + "/emptyPage": "\U0001F5CC", + "/emptyPages": "\U0001F5CD", + "/document": "\U0001F5CE", + "/page": "\U0001F5CF", + "/pages": "\U0001F5D0", + "/wastebasket": "\U0001F5D1", + "/spiralNotePad": "\U0001F5D2", + "/spiralCalendarPad": "\U0001F5D3", + "/desktopWindow": "\U0001F5D4", + "/minimize": "\U0001F5D5", + "/maximize": "\U0001F5D6", + "/overlap": "\U0001F5D7", + "/clockwiseRightAndLeftSemicircleArrows": "\U0001F5D8", + "/cancellationX": "\U0001F5D9", + "/increaseFontSize": "\U0001F5DA", + "/decreaseFontSize": "\U0001F5DB", + "/compression": "\U0001F5DC", + "/oldKey": "\U0001F5DD", + "/rolledUpNewspaper": "\U0001F5DE", + "/pageCircledText": "\U0001F5DF", + "/stockChart": "\U0001F5E0", + "/daggerKnife": "\U0001F5E1", + "/lips": "\U0001F5E2", + "/speakingHeadInSilhouette": "\U0001F5E3", + "/threeRaysAbove": "\U0001F5E4", + "/threeRaysBelow": "\U0001F5E5", + "/threeRaysLeft": "\U0001F5E6", + "/threeRaysRight": "\U0001F5E7", + "/leftSpeechBubble": "\U0001F5E8", + "/rightSpeechBubble": "\U0001F5E9", + "/twoSpeechBubbles": "\U0001F5EA", + "/threeSpeechBubbles": "\U0001F5EB", + "/leftThoughtBubble": "\U0001F5EC", + "/rightThoughtBubble": "\U0001F5ED", + "/leftAngerBubble": "\U0001F5EE", + "/rightAngerBubble": "\U0001F5EF", + "/moodBubble": "\U0001F5F0", + "/lightningMoodBubble": "\U0001F5F1", + "/lightningMood": "\U0001F5F2", + "/ballotBoxBallot": "\U0001F5F3", + "/ballotScriptX": "\U0001F5F4", + "/ballotBoxScriptX": "\U0001F5F5", + "/ballotBoldScriptX": "\U0001F5F6", + "/ballotBoxBoldScriptX": "\U0001F5F7", + "/lightCheckMark": "\U0001F5F8", + "/ballotBoxBoldCheck": "\U0001F5F9", + "/worldMap": "\U0001F5FA", + "/mountFuji": "\U0001F5FB", + "/tokyoTower": "\U0001F5FC", + "/statueOfLiberty": "\U0001F5FD", + "/silhouetteOfJapan": "\U0001F5FE", + "/moyai": "\U0001F5FF", + "/grinningFace": "\U0001F600", + "/grinningFaceWithSmilingEyes": "\U0001F601", + "/faceWithTearsOfJoy": "\U0001F602", + "/smilingFaceWithOpenMouth": "\U0001F603", + "/smilingFaceWithOpenMouthAndSmilingEyes": "\U0001F604", + "/smilingFaceWithOpenMouthAndColdSweat": "\U0001F605", + "/smilingFaceWithOpenMouthAndTightlyClosedEyes": "\U0001F606", + "/smilingFaceWithHalo": "\U0001F607", + "/smilingFaceWithHorns": "\U0001F608", + "/winkingFace": "\U0001F609", + "/smilingFaceWithSmilingEyes": "\U0001F60A", + "/faceSavouringDeliciousFood": "\U0001F60B", + "/relievedFace": "\U0001F60C", + "/smilingFaceWithHeartShapedEyes": "\U0001F60D", + "/smilingFaceWithSunglasses": "\U0001F60E", + "/smirkingFace": "\U0001F60F", + "/neutralFace": "\U0001F610", + "/expressionlessFace": "\U0001F611", + "/unamusedFace": "\U0001F612", + "/faceWithColdSweat": "\U0001F613", + "/pensiveFace": "\U0001F614", + "/confusedFace": "\U0001F615", + "/confoundedFace": "\U0001F616", + "/kissingFace": "\U0001F617", + "/faceThrowingAKiss": "\U0001F618", + "/kissingFaceWithSmilingEyes": "\U0001F619", + "/kissingFaceWithClosedEyes": "\U0001F61A", + "/faceWithStuckOutTongue": "\U0001F61B", + "/faceWithStuckOutTongueAndWinkingEye": "\U0001F61C", + "/faceWithStuckOutTongueAndTightlyClosedEyes": "\U0001F61D", + "/disappointedFace": "\U0001F61E", + "/worriedFace": "\U0001F61F", + "/angryFace": "\U0001F620", + "/poutingFace": "\U0001F621", + "/cryingFace": "\U0001F622", + "/perseveringFace": "\U0001F623", + "/faceWithLookOfTriumph": "\U0001F624", + "/disappointedButRelievedFace": "\U0001F625", + "/frowningFaceWithOpenMouth": "\U0001F626", + "/anguishedFace": "\U0001F627", + "/fearfulFace": "\U0001F628", + "/wearyFace": "\U0001F629", + "/sleepyFace": "\U0001F62A", + "/tiredFace": "\U0001F62B", + "/grimacingFace": "\U0001F62C", + "/loudlyCryingFace": "\U0001F62D", + "/faceWithOpenMouth": "\U0001F62E", + "/hushedFace": "\U0001F62F", + "/faceWithOpenMouthAndColdSweat": "\U0001F630", + "/faceScreamingInFear": "\U0001F631", + "/astonishedFace": "\U0001F632", + "/flushedFace": "\U0001F633", + "/sleepingFace": "\U0001F634", + "/dizzyFace": "\U0001F635", + "/faceWithoutMouth": "\U0001F636", + "/faceWithMedicalMask": "\U0001F637", + "/grinningCatFaceWithSmilingEyes": "\U0001F638", + "/catFaceWithTearsOfJoy": "\U0001F639", + "/smilingCatFaceWithOpenMouth": "\U0001F63A", + "/smilingCatFaceWithHeartShapedEyes": "\U0001F63B", + "/catFaceWithWrySmile": "\U0001F63C", + "/kissingCatFaceWithClosedEyes": "\U0001F63D", + "/poutingCatFace": "\U0001F63E", + "/cryingCatFace": "\U0001F63F", + "/wearyCatFace": "\U0001F640", + "/slightlyFrowningFace": "\U0001F641", + "/slightlySmilingFace": "\U0001F642", + "/upsideDownFace": "\U0001F643", + "/faceWithRollingEyes": "\U0001F644", + "/faceWithNoGoodGesture": "\U0001F645", + "/faceWithOkGesture": "\U0001F646", + "/personBowingDeeply": "\U0001F647", + "/seeNoEvilMonkey": "\U0001F648", + "/hearNoEvilMonkey": "\U0001F649", + "/speakNoEvilMonkey": "\U0001F64A", + "/happyPersonRaisingOneHand": "\U0001F64B", + "/personRaisingBothHandsInCelebration": "\U0001F64C", + "/personFrowning": "\U0001F64D", + "/personWithPoutingFace": "\U0001F64E", + "/personWithFoldedHands": "\U0001F64F", + "/rocket": "\U0001F680", + "/helicopter": "\U0001F681", + "/steamLocomotive": "\U0001F682", + "/railwayCar": "\U0001F683", + "/highSpeedTrain": "\U0001F684", + "/highSpeedTrainWithBulletNose": "\U0001F685", + "/train": "\U0001F686", + "/metro": "\U0001F687", + "/lightRail": "\U0001F688", + "/station": "\U0001F689", + "/tram": "\U0001F68A", + "/tramCar": "\U0001F68B", + "/bus": "\U0001F68C", + "/oncomingBus": "\U0001F68D", + "/trolleybus": "\U0001F68E", + "/busStop": "\U0001F68F", + "/minibus": "\U0001F690", + "/ambulance": "\U0001F691", + "/fireEngine": "\U0001F692", + "/policeCar": "\U0001F693", + "/oncomingPoliceCar": "\U0001F694", + "/taxi": "\U0001F695", + "/oncomingTaxi": "\U0001F696", + "/automobile": "\U0001F697", + "/oncomingAutomobile": "\U0001F698", + "/recreationalVehicle": "\U0001F699", + "/deliveryTruck": "\U0001F69A", + "/articulatedLorry": "\U0001F69B", + "/tractor": "\U0001F69C", + "/monorail": "\U0001F69D", + "/mountainRailway": "\U0001F69E", + "/suspensionRailway": "\U0001F69F", + "/mountainCableway": "\U0001F6A0", + "/aerialTramway": "\U0001F6A1", + "/ship": "\U0001F6A2", + "/rowboat": "\U0001F6A3", + "/speedboat": "\U0001F6A4", + "/horizontalTrafficLight": "\U0001F6A5", + "/verticalTrafficLight": "\U0001F6A6", + "/constructionSign": "\U0001F6A7", + "/policeCarsRevolvingLight": "\U0001F6A8", + "/triangularFlagOnPost": "\U0001F6A9", + "/door": "\U0001F6AA", + "/noEntrySign": "\U0001F6AB", + "/smoking": "\U0001F6AC", + "/noSmoking": "\U0001F6AD", + "/putLitterInItsPlace": "\U0001F6AE", + "/doNotLitter": "\U0001F6AF", + "/potableWater": "\U0001F6B0", + "/nonPotableWater": "\U0001F6B1", + "/bicycle": "\U0001F6B2", + "/noBicycles": "\U0001F6B3", + "/bicyclist": "\U0001F6B4", + "/mountainBicyclist": "\U0001F6B5", + "/pedestrian": "\U0001F6B6", + "/noPedestrians": "\U0001F6B7", + "/childrenCrossing": "\U0001F6B8", + "/mens": "\U0001F6B9", + "/womens": "\U0001F6BA", + "/restroom": "\U0001F6BB", + "/trns:baby": "\U0001F6BC", + "/toilet": "\U0001F6BD", + "/waterCloset": "\U0001F6BE", + "/shower": "\U0001F6BF", + "/bath": "\U0001F6C0", + "/bathtub": "\U0001F6C1", + "/passportControl": "\U0001F6C2", + "/customs": "\U0001F6C3", + "/baggageClaim": "\U0001F6C4", + "/leftLuggage": "\U0001F6C5", + "/triangleWithRoundedCorners": "\U0001F6C6", + "/prohibitedSign": "\U0001F6C7", + "/circledInformationSource": "\U0001F6C8", + "/boys": "\U0001F6C9", + "/girls": "\U0001F6CA", + "/couchAndLamp": "\U0001F6CB", + "/sleepingAccommodation": "\U0001F6CC", + "/shoppingBags": "\U0001F6CD", + "/bellhopBell": "\U0001F6CE", + "/bed": "\U0001F6CF", + "/placeOfWorship": "\U0001F6D0", + "/octagonalSign": "\U0001F6D1", + "/shoppingTrolley": "\U0001F6D2", + "/stupa": "\U0001F6D3", + "/pagoda": "\U0001F6D4", + "/hinduTemple": "\U0001F6D5", + "/hut": "\U0001F6D6", + "/elevator": "\U0001F6D7", + "/wireless": "\U0001F6DC", + "/playgroundSlide": "\U0001F6DD", + "/wheel": "\U0001F6DE", + "/ringBuoy": "\U0001F6DF", + "/hammerAndWrench": "\U0001F6E0", + "/shield": "\U0001F6E1", + "/oilDrum": "\U0001F6E2", + "/motorway": "\U0001F6E3", + "/railwayTrack": "\U0001F6E4", + "/motorBoat": "\U0001F6E5", + "/upPointingMilitaryAirplane": "\U0001F6E6", + "/upPointingAirplane": "\U0001F6E7", + "/upPointingSmallAirplane": "\U0001F6E8", + "/smallAirplane": "\U0001F6E9", + "/northeastPointingAirplane": "\U0001F6EA", + "/airplaneDeparture": "\U0001F6EB", + "/airplaneArriving": "\U0001F6EC", + "/satellite": "\U0001F6F0", + "/oncomingFireEngine": "\U0001F6F1", + "/dieselLocomotive": "\U0001F6F2", + "/passengerShip": "\U0001F6F3", + "/scooter": "\U0001F6F4", + "/motorScooter": "\U0001F6F5", + "/canoe": "\U0001F6F6", + "/sled": "\U0001F6F7", + "/flyingSaucer": "\U0001F6F8", + "/skateboard": "\U0001F6F9", + "/autoRickshaw": "\U0001F6FA", + "/pickupTruck": "\U0001F6FB", + "/rollerSkate": "\U0001F6FC", + "/blackLeftpointingIsoscelesRightTriangle": "\U0001F780", + "/blackUppointingIsoscelesRightTriangle": "\U0001F781", + "/blackRightpointingIsoscelesRightTriangle": "\U0001F782", + "/blackDownpointingIsoscelesRightTriangle": "\U0001F783", + "/blackSlightlySmallCircle": "\U0001F784", + "/mediumBoldWhiteCircle": "\U0001F785", + "/boldWhiteCircle": "\U0001F786", + "/heavyWhiteCircle": "\U0001F787", + "/veryHeavyWhiteCircle": "\U0001F788", + "/extremelyHeavyWhiteCircle": "\U0001F789", + "/whiteCircleContainingBlackSmallCircle": "\U0001F78A", + "/roundTarget": "\U0001F78B", + "/blackTinySquare": "\U0001F78C", + "/blackSlightlySmallSquare": "\U0001F78D", + "/lightWhiteSquare": "\U0001F78E", + "/mediumWhiteSquare": "\U0001F78F", + "/boldWhiteSquare": "\U0001F790", + "/heavyWhiteSquare": "\U0001F791", + "/veryHeavyWhiteSquare": "\U0001F792", + "/extremelyHeavyWhiteSquare": "\U0001F793", + "/whiteSquareContainingBlackVerySmallSquare": "\U0001F794", + "/whiteSquareContainingBlackMediumSquare": "\U0001F795", + "/squareTarget": "\U0001F796", + "/blackTinyDiamond": "\U0001F797", + "/blackVerySmallDiamond": "\U0001F798", + "/blackMediumSmallDiamond": "\U0001F799", + "/whiteDiamondContainingBlackVerySmallDiamond": "\U0001F79A", + "/whiteDiamondContainingBlackMediumDiamond": "\U0001F79B", + "/diamondTarget": "\U0001F79C", + "/blackTinyLozenge": "\U0001F79D", + "/blackVerySmallLozenge": "\U0001F79E", + "/blackMediumSmallLozenge": "\U0001F79F", + "/whiteLozengeContainingBlackSmallLozenge": "\U0001F7A0", + "/thinGreekCross": "\U0001F7A1", + "/lightGreekCross": "\U0001F7A2", + "/mediumGreekCross": "\U0001F7A3", + "/boldGreekCross": "\U0001F7A4", + "/veryBoldGreekCross": "\U0001F7A5", + "/veryHeavyGreekCross": "\U0001F7A6", + "/extremelyHeavyGreekCross": "\U0001F7A7", + "/thinSaltire": "\U0001F7A8", + "/lightSaltire": "\U0001F7A9", + "/mediumSaltire": "\U0001F7AA", + "/boldSaltire": "\U0001F7AB", + "/heavySaltire": "\U0001F7AC", + "/veryHeavySaltire": "\U0001F7AD", + "/extremelyHeavySaltire": "\U0001F7AE", + "/lightFiveSpokedAsterisk": "\U0001F7AF", + "/mediumFiveSpokedAsterisk": "\U0001F7B0", + "/boldFiveSpokedAsterisk": "\U0001F7B1", + "/heavyFiveSpokedAsterisk": "\U0001F7B2", + "/veryHeavyFiveSpokedAsterisk": "\U0001F7B3", + "/extremelyHeavyFiveSpokedAsterisk": "\U0001F7B4", + "/lightSixSpokedAsterisk": "\U0001F7B5", + "/mediumSixSpokedAsterisk": "\U0001F7B6", + "/boldSixSpokedAsterisk": "\U0001F7B7", + "/heavySixSpokedAsterisk": "\U0001F7B8", + "/veryHeavySixSpokedAsterisk": "\U0001F7B9", + "/extremelyHeavySixSpokedAsterisk": "\U0001F7BA", + "/lightEightSpokedAsterisk": "\U0001F7BB", + "/mediumEightSpokedAsterisk": "\U0001F7BC", + "/boldEightSpokedAsterisk": "\U0001F7BD", + "/heavyEightSpokedAsterisk": "\U0001F7BE", + "/veryHeavyEightSpokedAsterisk": "\U0001F7BF", + "/lightThreePointedBlackStar": "\U0001F7C0", + "/mediumThreePointedBlackStar": "\U0001F7C1", + "/threePointedBlackStar": "\U0001F7C2", + "/mediumThreePointedPinwheelStar": "\U0001F7C3", + "/lightFourPointedBlackStar": "\U0001F7C4", + "/mediumFourPointedBlackStar": "\U0001F7C5", + "/fourPointedBlackStar": "\U0001F7C6", + "/mediumFourPointedPinwheelStar": "\U0001F7C7", + "/reverseLightFourPointedPinwheelStar": "\U0001F7C8", + "/lightFivePointedBlackStar": "\U0001F7C9", + "/heavyFivePointedBlackStar": "\U0001F7CA", + "/mediumSixPointedBlackStar": "\U0001F7CB", + "/heavySixPointedBlackStar": "\U0001F7CC", + "/sixPointedPinwheelStar": "\U0001F7CD", + "/mediumEightPointedBlackStar": "\U0001F7CE", + "/heavyEightPointedBlackStar": "\U0001F7CF", + "/veryHeavyEightPointedBlackStar": "\U0001F7D0", + "/heavyEightPointedPinwheelStar": "\U0001F7D1", + "/lightTwelvePointedBlackStar": "\U0001F7D2", + "/heavyTwelvePointedBlackStar": "\U0001F7D3", + "/heavyTwelvePointedPinwheelStar": "\U0001F7D4", + "/circledTriangle": "\U0001F7D5", + "/negativeCircledTriangle": "\U0001F7D6", + "/circledSquare": "\U0001F7D7", + "/negativeCircledSquare": "\U0001F7D8", + "/ninePointedWhiteStar": "\U0001F7D9", + "/largeOrangeCircle": "\U0001F7E0", + "/largeYellowCircle": "\U0001F7E1", + "/largeGreenCircle": "\U0001F7E2", + "/largePurpleCircle": "\U0001F7E3", + "/largeBrownCircle": "\U0001F7E4", + "/largeRedSquare": "\U0001F7E5", + "/largeBlueSquare": "\U0001F7E6", + "/largeOrangeSquare": "\U0001F7E7", + "/largeYellowSquare": "\U0001F7E8", + "/largeGreenSquare": "\U0001F7E9", + "/largePurpleSquare": "\U0001F7EA", + "/largeBrownSquare": "\U0001F7EB", + "/heavyEqualsSign": "\U0001F7F0", + "/leftwardsArrowWithSmallTriangleArrowhead": "\U0001F800", + "/upwardsArrowWithSmallTriangleArrowhead": "\U0001F801", + "/rightwardsArrowWithSmallTriangleArrowhead": "\U0001F802", + "/downwardsArrowWithSmallTriangleArrowhead": "\U0001F803", + "/leftwardsArrowWithMediumTriangleArrowhead": "\U0001F804", + "/upwardsArrowWithMediumTriangleArrowhead": "\U0001F805", + "/rightwardsArrowWithMediumTriangleArrowhead": "\U0001F806", + "/downwardsArrowWithMediumTriangleArrowhead": "\U0001F807", + "/leftwardsArrowWithLargeTriangleArrowhead": "\U0001F808", + "/upwardsArrowWithLargeTriangleArrowhead": "\U0001F809", + "/rightwardsArrowWithLargeTriangleArrowhead": "\U0001F80A", + "/downwardsArrowWithLargeTriangleArrowhead": "\U0001F80B", + "/leftwardsArrowWithSmallEquilateralArrowhead": "\U0001F810", + "/upwardsArrowWithSmallEquilateralArrowhead": "\U0001F811", + "/rightwardsArrowWithSmallEquilateralArrowhead": "\U0001F812", + "/downwardsArrowWithSmallEquilateralArrowhead": "\U0001F813", + "/leftwardsArrowWithEquilateralArrowhead": "\U0001F814", + "/upwardsArrowWithEquilateralArrowhead": "\U0001F815", + "/rightwardsArrowWithEquilateralArrowhead": "\U0001F816", + "/downwardsArrowWithEquilateralArrowhead": "\U0001F817", + "/heavyLeftwardsArrowWithEquilateralArrowhead": "\U0001F818", + "/heavyUpwardsArrowWithEquilateralArrowhead": "\U0001F819", + "/heavyRightwardsArrowWithEquilateralArrowhead": "\U0001F81A", + "/heavyDownwardsArrowWithEquilateralArrowhead": "\U0001F81B", + "/heavyLeftwardsArrowWithLargeEquilateralArrowhead": "\U0001F81C", + "/heavyUpwardsArrowWithLargeEquilateralArrowhead": "\U0001F81D", + "/heavyRightwardsArrowWithLargeEquilateralArrowhead": "\U0001F81E", + "/heavyDownwardsArrowWithLargeEquilateralArrowhead": "\U0001F81F", + "/leftwardsTriangleheadedArrowWithNarrowShaft": "\U0001F820", + "/upwardsTriangleheadedArrowWithNarrowShaft": "\U0001F821", + "/rightwardsTriangleheadedArrowWithNarrowShaft": "\U0001F822", + "/downwardsTriangleheadedArrowWithNarrowShaft": "\U0001F823", + "/leftwardsTriangleheadedArrowWithMediumShaft": "\U0001F824", + "/upwardsTriangleheadedArrowWithMediumShaft": "\U0001F825", + "/rightwardsTriangleheadedArrowWithMediumShaft": "\U0001F826", + "/downwardsTriangleheadedArrowWithMediumShaft": "\U0001F827", + "/leftwardsTriangleheadedArrowWithBoldShaft": "\U0001F828", + "/upwardsTriangleheadedArrowWithBoldShaft": "\U0001F829", + "/rightwardsTriangleheadedArrowWithBoldShaft": "\U0001F82A", + "/downwardsTriangleheadedArrowWithBoldShaft": "\U0001F82B", + "/leftwardsTriangleheadedArrowWithHeavyShaft": "\U0001F82C", + "/upwardsTriangleheadedArrowWithHeavyShaft": "\U0001F82D", + "/rightwardsTriangleheadedArrowWithHeavyShaft": "\U0001F82E", + "/downwardsTriangleheadedArrowWithHeavyShaft": "\U0001F82F", + "/leftwardsTriangleheadedArrowWithVeryHeavyShaft": "\U0001F830", + "/upwardsTriangleheadedArrowWithVeryHeavyShaft": "\U0001F831", + "/rightwardsTriangleheadedArrowWithVeryHeavyShaft": "\U0001F832", + "/downwardsTriangleheadedArrowWithVeryHeavyShaft": "\U0001F833", + "/leftwardsFingerpostArrow": "\U0001F834", + "/upwardsFingerpostArrow": "\U0001F835", + "/rightwardsFingerpostArrow": "\U0001F836", + "/downwardsFingerpostArrow": "\U0001F837", + "/leftwardsSquaredArrow": "\U0001F838", + "/upwardsSquaredArrow": "\U0001F839", + "/rightwardsSquaredArrow": "\U0001F83A", + "/downwardsSquaredArrow": "\U0001F83B", + "/leftwardsCompressedArrow": "\U0001F83C", + "/upwardsCompressedArrow": "\U0001F83D", + "/rightwardsCompressedArrow": "\U0001F83E", + "/downwardsCompressedArrow": "\U0001F83F", + "/leftwardsHeavyCompressedArrow": "\U0001F840", + "/upwardsHeavyCompressedArrow": "\U0001F841", + "/rightwardsHeavyCompressedArrow": "\U0001F842", + "/downwardsHeavyCompressedArrow": "\U0001F843", + "/leftwardsHeavyArrow": "\U0001F844", + "/upwardsHeavyArrow": "\U0001F845", + "/rightwardsHeavyArrow": "\U0001F846", + "/downwardsHeavyArrow": "\U0001F847", + "/leftwardsSansserifArrow": "\U0001F850", + "/upwardsSansserifArrow": "\U0001F851", + "/rightwardsSansserifArrow": "\U0001F852", + "/downwardsSansserifArrow": "\U0001F853", + "/northWestSansserifArrow": "\U0001F854", + "/northEastSansserifArrow": "\U0001F855", + "/southEastSansserifArrow": "\U0001F856", + "/southWestSansserifArrow": "\U0001F857", + "/leftRightSansserifArrow": "\U0001F858", + "/upDownSansserifArrow": "\U0001F859", + "/wideheadedLeftwardsLightBarbArrow": "\U0001F860", + "/wideheadedUpwardsLightBarbArrow": "\U0001F861", + "/wideheadedRightwardsLightBarbArrow": "\U0001F862", + "/wideheadedDownwardsLightBarbArrow": "\U0001F863", + "/wideheadedNorthWestLightBarbArrow": "\U0001F864", + "/wideheadedNorthEastLightBarbArrow": "\U0001F865", + "/wideheadedSouthEastLightBarbArrow": "\U0001F866", + "/wideheadedSouthWestLightBarbArrow": "\U0001F867", + "/wideheadedLeftwardsBarbArrow": "\U0001F868", + "/wideheadedUpwardsBarbArrow": "\U0001F869", + "/wideheadedRightwardsBarbArrow": "\U0001F86A", + "/wideheadedDownwardsBarbArrow": "\U0001F86B", + "/wideheadedNorthWestBarbArrow": "\U0001F86C", + "/wideheadedNorthEastBarbArrow": "\U0001F86D", + "/wideheadedSouthEastBarbArrow": "\U0001F86E", + "/wideheadedSouthWestBarbArrow": "\U0001F86F", + "/wideheadedLeftwardsMediumBarbArrow": "\U0001F870", + "/wideheadedUpwardsMediumBarbArrow": "\U0001F871", + "/wideheadedRightwardsMediumBarbArrow": "\U0001F872", + "/wideheadedDownwardsMediumBarbArrow": "\U0001F873", + "/wideheadedNorthWestMediumBarbArrow": "\U0001F874", + "/wideheadedNorthEastMediumBarbArrow": "\U0001F875", + "/wideheadedSouthEastMediumBarbArrow": "\U0001F876", + "/wideheadedSouthWestMediumBarbArrow": "\U0001F877", + "/wideheadedLeftwardsHeavyBarbArrow": "\U0001F878", + "/wideheadedUpwardsHeavyBarbArrow": "\U0001F879", + "/wideheadedRightwardsHeavyBarbArrow": "\U0001F87A", + "/wideheadedDownwardsHeavyBarbArrow": "\U0001F87B", + "/wideheadedNorthWestHeavyBarbArrow": "\U0001F87C", + "/wideheadedNorthEastHeavyBarbArrow": "\U0001F87D", + "/wideheadedSouthEastHeavyBarbArrow": "\U0001F87E", + "/wideheadedSouthWestHeavyBarbArrow": "\U0001F87F", + "/wideheadedLeftwardsVeryHeavyBarbArrow": "\U0001F880", + "/wideheadedUpwardsVeryHeavyBarbArrow": "\U0001F881", + "/wideheadedRightwardsVeryHeavyBarbArrow": "\U0001F882", + "/wideheadedDownwardsVeryHeavyBarbArrow": "\U0001F883", + "/wideheadedNorthWestVeryHeavyBarbArrow": "\U0001F884", + "/wideheadedNorthEastVeryHeavyBarbArrow": "\U0001F885", + "/wideheadedSouthEastVeryHeavyBarbArrow": "\U0001F886", + "/wideheadedSouthWestVeryHeavyBarbArrow": "\U0001F887", + "/leftwardsTriangleArrowhead": "\U0001F890", + "/upwardsTriangleArrowhead": "\U0001F891", + "/rightwardsTriangleArrowhead": "\U0001F892", + "/downwardsTriangleArrowhead": "\U0001F893", + "/leftwardsWhiteArrowWithinTriangleArrowhead": "\U0001F894", + "/upwardsWhiteArrowWithinTriangleArrowhead": "\U0001F895", + "/rightwardsWhiteArrowWithinTriangleArrowhead": "\U0001F896", + "/downwardsWhiteArrowWithinTriangleArrowhead": "\U0001F897", + "/leftwardsArrowWithNotchedTail": "\U0001F898", + "/upwardsArrowWithNotchedTail": "\U0001F899", + "/rightwardsArrowWithNotchedTail": "\U0001F89A", + "/downwardsArrowWithNotchedTail": "\U0001F89B", + "/heavyArrowShaftWidthOne": "\U0001F89C", + "/heavyArrowShaftWidthTwoThirds": "\U0001F89D", + "/heavyArrowShaftWidthOneHalf": "\U0001F89E", + "/heavyArrowShaftWidthOneThird": "\U0001F89F", + "/leftwardsBottomshadedWhiteArrow": "\U0001F8A0", + "/rightwardsBottomShadedWhiteArrow": "\U0001F8A1", + "/leftwardsTopShadedWhiteArrow": "\U0001F8A2", + "/rightwardsTopShadedWhiteArrow": "\U0001F8A3", + "/leftwardsLeftshadedWhiteArrow": "\U0001F8A4", + "/rightwardsRightshadedWhiteArrow": "\U0001F8A5", + "/leftwardsRightshadedWhiteArrow": "\U0001F8A6", + "/rightwardsLeftshadedWhiteArrow": "\U0001F8A7", + "/leftwardsBacktiltedShadowedWhiteArrow": "\U0001F8A8", + "/rightwardsBacktiltedShadowedWhiteArrow": "\U0001F8A9", + "/leftwardsFronttiltedShadowedWhiteArrow": "\U0001F8AA", + "/rightwardsFronttiltedShadowedWhiteArrow": "\U0001F8AB", + "/whiteArrowShaftWidthOne": "\U0001F8AC", + "/whiteArrowShaftWidthTwoThirds": "\U0001F8AD", + "/arrowPointingUpwardsThenNorthWest": "\U0001F8B0", + "/arrowPointingRightwardsThenCurvingSouthWest": "\U0001F8B1", + "/rightwardsArrowWithLowerHook": "\U0001F8B2", + "/downwardsBlackArrowToBar": "\U0001F8B3", + "/negativeSquaredLeftwardsArrow": "\U0001F8B4", + "/negativeSquaredUpwardsArrow": "\U0001F8B5", + "/negativeSquaredRightwardsArrow": "\U0001F8B6", + "/negativeSquaredDownwardsArrow": "\U0001F8B7", + "/northWestArrowFromBar": "\U0001F8B8", + "/northEastArrowFromBar": "\U0001F8B9", + "/southEastArrowFromBar": "\U0001F8BA", + "/southWestArrowFromBar": "\U0001F8BB", + "/leftwardsArrowFromDownwardsArrow": "\U0001F8C0", + "/rightwardsArrowFromDownwardsArrow": "\U0001F8C1", + "/neutralKing": "\U0001FA00", + "/neutralQueen": "\U0001FA01", + "/neutralRook": "\U0001FA02", + "/neutralBishop": "\U0001FA03", + "/neutralKnight": "\U0001FA04", + "/neutralPawn": "\U0001FA05", + "/whiteKnight45": "\U0001FA06", + "/blackKnight45": "\U0001FA07", + "/neutralKnight45": "\U0001FA08", + "/whiteKing90": "\U0001FA09", + "/whiteQueen90": "\U0001FA0A", + "/whiteRook90": "\U0001FA0B", + "/whiteBishop90": "\U0001FA0C", + "/whiteKnight90": "\U0001FA0D", + "/whitePawn90": "\U0001FA0E", + "/blackKing90": "\U0001FA0F", + "/blackQueen90": "\U0001FA10", + "/blackRook90": "\U0001FA11", + "/blackBishop90": "\U0001FA12", + "/blackKnight90": "\U0001FA13", + "/blackPawn90": "\U0001FA14", + "/neutralKing90": "\U0001FA15", + "/neutralQueen90": "\U0001FA16", + "/neutralRook90": "\U0001FA17", + "/neutralBishop90": "\U0001FA18", + "/neutralKnight90": "\U0001FA19", + "/neutralPawn90": "\U0001FA1A", + "/whiteKnight135": "\U0001FA1B", + "/blackKnight135": "\U0001FA1C", + "/neutralKnight135": "\U0001FA1D", + "/whiteKingTurned": "\U0001FA1E", + "/whiteQueenTurned": "\U0001FA1F", + "/whiteRookTurned": "\U0001FA20", + "/whiteBishopTurned": "\U0001FA21", + "/whiteKnightTurned": "\U0001FA22", + "/whitePawnTurned": "\U0001FA23", + "/blackKingTurned": "\U0001FA24", + "/blackQueenTurned": "\U0001FA25", + "/blackRookTurned": "\U0001FA26", + "/blackBishopTurned": "\U0001FA27", + "/blackKnightTurned": "\U0001FA28", + "/blackPawnTurned": "\U0001FA29", + "/neutralKingTurned": "\U0001FA2A", + "/neutralQueenTurned": "\U0001FA2B", + "/neutralRookTurned": "\U0001FA2C", + "/neutralBishopTurned": "\U0001FA2D", + "/neutralKnightTurned": "\U0001FA2E", + "/neutralPawnTurned": "\U0001FA2F", + "/whiteKnight225": "\U0001FA30", + "/blackKnight225": "\U0001FA31", + "/neutralKnight225": "\U0001FA32", + "/whiteKing270": "\U0001FA33", + "/whiteQueen270": "\U0001FA34", + "/whiteRook270": "\U0001FA35", + "/whiteBishop270": "\U0001FA36", + "/whiteKnight270": "\U0001FA37", + "/whitePawn270": "\U0001FA38", + "/blackKing270": "\U0001FA39", + "/blackQueen270": "\U0001FA3A", + "/blackRook270": "\U0001FA3B", + "/blackBishop270": "\U0001FA3C", + "/blackKnight270": "\U0001FA3D", + "/blackPawn270": "\U0001FA3E", + "/neutralKing270": "\U0001FA3F", + "/neutralQueen270": "\U0001FA40", + "/neutralRook270": "\U0001FA41", + "/neutralBishop270": "\U0001FA42", + "/neutralKnight270": "\U0001FA43", + "/neutralPawn270": "\U0001FA44", + "/whiteKnight315": "\U0001FA45", + "/blackKnight315": "\U0001FA46", + "/neutralKnight315": "\U0001FA47", + "/whiteEquihopper": "\U0001FA48", + "/blackEquihopper": "\U0001FA49", + "/neutralEquihopper": "\U0001FA4A", + "/whiteEquihopper90": "\U0001FA4B", + "/blackEquihopper90": "\U0001FA4C", + "/neutralEquihopper90": "\U0001FA4D", + "/whiteKnight-queen": "\U0001FA4E", + "/whiteKnightRook": "\U0001FA4F", + "/whiteKnightBishop": "\U0001FA50", + "/blackKnightQueen": "\U0001FA51", + "/blackKnightRook": "\U0001FA52", + "/blackKnightBishop": "\U0001FA53", + "/redGeneralXq": "\U0001FA60", + "/redMandarinXq": "\U0001FA61", + "/redElephantXq": "\U0001FA62", + "/redHorseXq": "\U0001FA63", + "/redChariotXq": "\U0001FA64", + "/redCannonXq": "\U0001FA65", + "/redSoldierXq": "\U0001FA66", + "/blackGeneralXq": "\U0001FA67", + "/blackMandarinXq": "\U0001FA68", + "/blackElephantXq": "\U0001FA69", + "/blackHorseXq": "\U0001FA6A", + "/blackChariotXq": "\U0001FA6B", + "/blackCannonXq": "\U0001FA6C", + "/blackSoldierXq": "\U0001FA6D", + "/balletShoes": "\U0001FA70", + "/onepieceSwimsuit": "\U0001FA71", + "/briefs": "\U0001FA72", + "/shorts": "\U0001FA73", + "/thongSandal": "\U0001FA74", + "/lightBlueHeart": "\U0001FA75", + "/greyHeart": "\U0001FA76", + "/pinkHeart": "\U0001FA77", + "/dropOfBlood": "\U0001FA78", + "/adhesiveBandage": "\U0001FA79", + "/stethoscope": "\U0001FA7A", + "/xray": "\U0001FA7B", + "/crutch": "\U0001FA7C", + "/yoyo": "\U0001FA80", + "/kite": "\U0001FA81", + "/parachute": "\U0001FA82", + "/boomerang": "\U0001FA83", + "/magicWand": "\U0001FA84", + "/pinata": "\U0001FA85", + "/nestingDolls": "\U0001FA86", + "/maracas": "\U0001FA87", + "/flute": "\U0001FA88", + "/harp": "\U0001FA89", + "/shovel": "\U0001FA8F", + "/ringedPlanet": "\U0001FA90", + "/symb:chair": "\U0001FA91", + "/razor": "\U0001FA92", + "/axe": "\U0001FA93", + "/diyaLamp": "\U0001FA94", + "/banjo": "\U0001FA95", + "/militaryHelmet": "\U0001FA96", + "/accordion": "\U0001FA97", + "/longDrum": "\U0001FA98", + "/coin": "\U0001FA99", + "/carpentrySaw": "\U0001FA9A", + "/screwdriver": "\U0001FA9B", + "/ladder": "\U0001FA9C", + "/symb:hook": "\U0001FA9D", + "/mirror": "\U0001FA9E", + "/window": "\U0001FA9F", + "/plunger": "\U0001FAA0", + "/sewingNeedle": "\U0001FAA1", + "/knot": "\U0001FAA2", + "/bucket": "\U0001FAA3", + "/mouseTrap": "\U0001FAA4", + "/toothbrush": "\U0001FAA5", + "/headstone": "\U0001FAA6", + "/placard": "\U0001FAA7", + "/rock": "\U0001FAA8", + "/mirrorBall": "\U0001FAA9", + "/identificationCard": "\U0001FAAA", + "/lowBattery": "\U0001FAAB", + "/hamsa": "\U0001FAAC", + "/foldingHandFan": "\U0001FAAD", + "/hairPick": "\U0001FAAE", + "/khanda": "\U0001FAAF", + "/fly": "\U0001FAB0", + "/worm": "\U0001FAB1", + "/beetle": "\U0001FAB2", + "/cockroach": "\U0001FAB3", + "/pottedPlant": "\U0001FAB4", + "/wood": "\U0001FAB5", + "/feather": "\U0001FAB6", + "/lotus": "\U0001FAB7", + "/coral": "\U0001FAB8", + "/emptyNest": "\U0001FAB9", + "/nestWithEggs": "\U0001FABA", + "/hyacinth": "\U0001FABB", + "/jellyfish": "\U0001FABC", + "/wing": "\U0001FABD", + "/leaflessTree": "\U0001FABE", + "/goose": "\U0001FABF", + "/anatomicalHeart": "\U0001FAC0", + "/lungs": "\U0001FAC1", + "/peopleHugging": "\U0001FAC2", + "/pregnantMan": "\U0001FAC3", + "/pregnantPerson": "\U0001FAC4", + "/personWithCrown": "\U0001FAC5", + "/fingerprint": "\U0001FAC6", + "/moose": "\U0001FACE", + "/donkey": "\U0001FACF", + "/blueberries": "\U0001FAD0", + "/bellPepper": "\U0001FAD1", + "/olive": "\U0001FAD2", + "/flatbread": "\U0001FAD3", + "/tamale": "\U0001FAD4", + "/fondue": "\U0001FAD5", + "/teapot": "\U0001FAD6", + "/pouringLiquid": "\U0001FAD7", + "/beans": "\U0001FAD8", + "/jar": "\U0001FAD9", + "/gingerRoot": "\U0001FADA", + "/peaPod": "\U0001FADB", + "/rootVegetable": "\U0001FADC", + "/splatter": "\U0001FADF", + "/meltingFace": "\U0001FAE0", + "/salutingFace": "\U0001FAE1", + "/faceWithOpenEyesAndHandOverMouth": "\U0001FAE2", + "/faceWithPeekingEye": "\U0001FAE3", + "/faceWithDiagonalMouth": "\U0001FAE4", + "/dottedLineFace": "\U0001FAE5", + "/bitingLip": "\U0001FAE6", + "/bubbles": "\U0001FAE7", + "/shakingFace": "\U0001FAE8", + "/faceWithBagsUnderEyes": "\U0001FAE9", + "/handWithIndexFingerAndThumbCrossed": "\U0001FAF0", + "/rightwardsHand": "\U0001FAF1", + "/leftwardsHand": "\U0001FAF2", + "/palmDownHand": "\U0001FAF3", + "/palmUpHand": "\U0001FAF4", + "/indexPointingAtTheViewer": "\U0001FAF5", + "/heartHands": "\U0001FAF6", + "/leftwardsPushingHand": "\U0001FAF7", + "/rightwardsPushingHand": "\U0001FAF8", + # cmex10.afm from https://ctan.org/texarchive/fonts/cm/afm + "/angleleftBig": "\u2329", + "/angleleftBigg": "\u2329", + "/angleleftbig": "\u2329", + "/angleleftbigg": "\u2329", + "/anglerightBig": "\u232A", + "/anglerightBigg": "\u232A", + "/anglerightbig": "\u232A", + "/anglerightbigg": "\u232A", + "/arrowdbldownbt": "\u21D3", + "/arrowdbluptop": "\u21D1", + # "/arrowdblvertex": "", # Not mappable? + "/arrowdownbt": "\u2193", + "/arrowuptp": "\u2191", + "/backslashBig": "\u005C", + "/backslashBigg": "\u005C", + "/backslashbig": "\u005C", + "/backslashbigg": "\u005C", + # "/bracehoriztipdownleft": "", # Not mappable? + # "/bracehoriztipdownright": "", # Not mappable? + # "/bracehoriztipupleft": "", # Not mappable? + # "/bracehoriztipupright": "", # Not mappable? + "/braceleftBig": "\u007B", + "/braceleftBigg": "\u007B", + "/braceleftbig": "\u007B", + "/braceleftbigg": "\u007B", + "/bracerightBig": "\u007D", + "/bracerightBigg": "\u007D", + "/bracerightbig": "\u007D", + "/bracerightbigg": "\u007D", + "/bracketleftBig": "\u005B", + "/bracketleftBigg": "\u005B", + "/bracketleftbig": "\u005B", + "/bracketleftbigg": "\u005B", + "/bracketleftmid": "\u005B", + "/bracketrightBig": "\u005D", + "/bracketrightBigg": "\u005D", + "/bracketrightbig": "\u005D", + "/bracketrightbigg": "\u005D", + "/bracketrightmid": "\u005D", + "/ceilingleftBig": "\u2308", + "/ceilingleftBigg": "\u2308", + "/ceilingleftbig": "\u2308", + "/ceilingleftbigg": "\u2308", + "/ceilingrightBig": "\u2309", + "/ceilingrightBigg": "\u2309", + "/ceilingrightbig": "\u2309", + "/ceilingrightbigg": "\u2309", + "/circledotdisplaystyle": "\u2A00", + "/circledottextstyle": "\u2A00", + "/circleintegraldisplaystyle": "\u222E", + "/circleintegraltextstyle": "\u222E", + "/circlemultiplydisplaystyle": "\u2A02", + "/circlemultiplytextstyle": "\u2A02", + "/circleplusdisplaystyle": "\u2A01", + "/circleplustextstyle": "\u2A01", + "/circumflexWide": "\u005E", + "/circumflexWider": "\u005E", + "/circumflexWidest": "\u005E", + "/coproductdisplaystyle": "\u2210", + "/coproducttextstyle": "\u2210", + "/floorleftBig": "\u230A", + "/floorleftBigg": "\u230A", + "/floorleftbig": "\u230A", + "/floorleftbigg": "\u230A", + "/floorrightBig": "\u230B", + "/floorrightBigg": "\u230B", + "/floorrightbig": "\u230B", + "/floorrightbigg": "\u230B", + "/integraldisplaystyle": "\u222B", + "/integraltextstyle": "\u222B", + "/intersectiondisplaystyle": "\u22C2", + "/intersectiontextstyle": "\u22C2", + # "/linedblvertex": "", # Not mappable? + # "/linevertex": "", # Not mappable? + "/logicalanddisplaystyle": "\u22C0", + "/logicalandtextstyle": "\u22C0", + "/logicalordisplaystyle": "\u22C1", + "/logicalortextstyle": "\u22C1", + "/parenleftBig": "\u0028", + "/parenleftBigg": "\u0028", + "/parenleftbig": "\u0028", + "/parenleftbigg": "\u0028", + "/parenleftmid": "\u0028", + "/parenrightBig": "\u0029", + "/parenrightBigg": "\u0029", + "/parenrightbig": "\u0029", + "/parenrightbigg": "\u0029", + "/parenrightmid": "\u0029", + "/productdisplaystyle": "\u220F", + "/producttextstyle": "\u220F", + "/radicalBig": "\u221A", + "/radicalBigg": "\u221A", + # "/radicalExtbt": "", # Not mappable? + # "/radicalExtmid": "", # Not mappable? + # "/radicalExttp": "", # Not mappable? + "/radicalbig": "\u221A", + "/radicalbigg": "\u221A", + "/slashBig": "\u002F", + "/slashBigg": "\u002F", + "/slashbig": "\u002F", + "/slashbigg": "\u002F", + "/squareuniondisplaystyle": "\u2A06", + "/squareuniontextstyle": "\u2A06", + "/summationdisplaystyle": "\u2211", + "/summationtextstyle": "\u2211", + "/tildeWide": "\u02DC", + "/tildeWider": "\u02DC", + "/tildeWidest": "\u02DC", + "/uniondisplaystyle": "\u22C3", + "/unionplusdisplaystyle": "\u2A04", + "/unionplustextstyle": "\u2A04", + "/uniontextstyle": "\u22C3", + # TeX fonts: https://ctan.math.utah.edu/ctan/tex-archive/info/fontname/fontname.html#Encodings-1 + "/cwm": "\u200B", + # "/perthousandzero": "", # Not mappable? + "/visiblespace": "\u2423", + "/S_S": "\u1E9E", + "/ell": "\u2113", + "/anglearc": "\u22BE", + "/diameter": "\u2300", + "/angbracketleftbig": "\u27E8", + "/angbracketrightbig": "\u27E9", + "/vextendsingle": "\u23D0", + "/vextenddouble": "\u2016", + "/angbracketleftbigg": "\u27E8", + "/angbracketrightbigg": "\u27E9", + "/angbracketleftBigg": "\u27E8", + "/angbracketrightBigg": "\u27E9", + "/angbracketleftBig": "\u27E8", + "/angbracketrightBig": "\u27E9", + "/unionsqtext": "\u2A06", + "/unionsqdisplay": "\u2A06", + "/contintegraltext": "\u222E", + "/contintegraldisplay": "\u222E", + "/circledottext": "\u2A00", + "/circledotdisplay": "\u2A00", + "/circleplustext": "\u2A01", + "/circleplusdisplay": "\u2A01", + "/circlemultiplytext": "\u2A02", + "/circlemultiplydisplay": "\u2A02", + "/summationtext": "\u2211", + "/producttext": "\u220F", + "/integraltext": "\u222B", + "/uniontext": "\u22C3", + "/intersectiontext": "\u22C2", + "/unionmultitext": "\u2A04", + "/logicalandtext": "\u22C0", + "/logicalortext": "\u22C1", + "/summationdisplay": "\u2211", + "/productdisplay": "\u220F", + "/integraldisplay": "\u222B", + "/uniondisplay": "\u22C3", + "/intersectiondisplay": "\u22C2", + "/unionmultidisplay": "\u2A04", + "/logicalanddisplay": "\u22C0", + "/logicalordisplay": "\u22C1", + "/coproducttext": "\u2210", + "/coproductdisplay": "\u2210", + "/hatwide": "\u02C6", + "/hatwider": "\u02C6", + "/hatwidest": "\u02C6", + "/tildewide": "\u007E", + "/tildewider": "\u007E", + "/tildewidest": "\u007E", + "/radicalbt": "\u221A", + # "/radicalvertex": "", # Not mappable? + # "/radicaltp": "", # Not mappable? + # "/arrowvertexdbl": "", # Not mappable? + "/arrowtp": "\u2191", + "/arrowbt": "\u2193", + # "/bracehtipdownleft": "", # Not mappable? + # "/bracehtipdownright": "", # Not mappable? + # "/bracehtipupleft": "", # Not mappable? + # "/bracehtipupright": "", # Not mappable? + "/arrowdbltp": "\u21D1", + "/arrowdblbt": "\u21D3", + "/diamondmath": "\u22C4", + "/circleminus": "\u2296", + "/circledivide": "\u2298", + "/circledot": "\u2299", + "/circlecopyrt": "\u00A9", + "/equivasymptotic": "\u224D", + "/precedesequal": "\u227C", + "/followsequal": "\u227D", + "/lessmuch": "\u226A", + "/greatermuch": "\u226B", + "/follows": "\u227B", + "/arrownortheast": "\u2197", + "/arrowsoutheast": "\u2198", + "/similarequal": "\u2243", + "/arrownorthwest": "\u2196", + "/arrowsouthwest": "\u2199", + "/prime": "\u2032", + "/owner": "\u220B", + "/triangle": "\u2206", + "/triangleinv": "\u25BD", + "/negationslash": "\u2044", + "/mapsto": "\u21A6", + "/Rfractur": "\u211C", + "/Ifractur": "\u2111", + "/latticetop": "\u22A4", + "/unionmulti": "\u2A04", + "/turnstileleft": "\u22A3", + "/turnstileright": "\u22A2", + "/angbracketleft": "\u27E8", + "/angbracketright": "\u27E9", + "/bardbl": "\u2016", + "/arrowbothv": "\u2195", + "/arrowdblbothv": "\u21D5", + "/coproduct": "\u2210", + "/unionsq": "\u2294", + "/intersectionsq": "\u2293", + "/subsetsqequal": "\u2291", + "/supersetsqequal": "\u2292", + "/epsilon1": "\u03F5", + "/pi1": "\u03D6", + "/rho1": "\u03F1", + "/arrowlefttophalf": "\u21BC", + "/arrowleftbothalf": "\u21BD", + "/arrowrighttophalf": "\u21C0", + "/arrowrightbothalf": "\u21C1", + "/arrowhookleft": "\u21AA", + "/arrowhookright": "\u21A9", + "/triangleleft": "\u25C1", + "/star": "\u22C6", + "/flat": "\u266D", + "/natural": "\u266E", + "/sharp": "\u266F", + "/slurbelow": "\u2323", + "/slurabove": "\u2322", + "/vector": "\u20D7", + "/tie": "\u2040", + # "/suppress": "", # Not mapped? + "/Togonek": "\u0162", + # Missing old names. Non-mappable characters have been left out. + "/Dbar": "\u0110", + "/FFIsmall": "\u0066\u0066\u0069", # As ligature: U+FB03 + "/FFLsmall": "\u0066\u0066\u006C", # As ligature: U+FB04 + "/FFsmall": "\u0066\u0066", # As ligature: U+FB00 + "/FIsmall": "\u0066\u0069", # As ligature: U+FB01 + "/FLsmall": "\u0066\u006C", # As ligature: U+FB02 + "/Finv": "\u2132", + "/Germandblssmall": "\u00DF", + "/Gmir": "\u2141", + "/Omegainv": "\u2127", + "/SS": "\u1E9E", + "/SSsmall": "\u00DF", + "/Yen": "\u00A5", + "/anticlockwise": "\u27F2", + "/approxorequal": "\u224A", + "/archleftdown": "\u21B6", + "/archrightdown": "\u21B7", + "/arrowparrleftright": "\u21C6", + "/arrowparrrightleft": "\u21C4", + "/arrowtailleft": "\u21A2", + "/arrowtailright": "\u21A3", + "/arrowtripleleft": "\u21DA", + "/arrowtripleright": "\u21DB", + "/asteriskcentered": "\u2217", + "/beth": "\u2136", + "/circleR": "\u24C7", + "/circleS": "\u24C8", + "/circleasterisk": "\u229B", + "/circleequal": "\u229C", + "/circlering": "\u229A", + "/clockFaceEightOclock": "\U0001F557", + "/clockFaceElevenOclock": "\U0001F55A", + "/clockFaceFiveOclock": "\U0001F554", + "/clockFaceFourOclock": "\U0001F553", + "/clockFaceNineOclock": "\U0001F558", + "/clockFaceOneOclock": "\U0001F550", + "/clockFaceSevenOclock": "\U0001F556", + "/clockFaceSixOclock": "\U0001F555", + "/clockFaceTenOclock": "\U0001F559", + "/clockFaceThreeOclock": "\U0001F552", + "/clockFaceTwelveOclock": "\U0001F55B", + "/clockFaceTwoOclock": "\U0001F551", + "/clockwise": "\u27F3", + "/clockwiseDownwardsAndUpwardsOpenCircleArrows": "\U0001F503", + "/ct": "\u0063\u0074", + "/curlyleft": "\u21AB", + "/curlyright": "\u21AC", + "/daleth": "\u2138", + "/dbar": "\u0111", + "/dblarrowdwn": "\u21CA", + "/dblarrowheadleft": "\u219E", + "/dblarrowheadright": "\u21A0", + "/dblbracketleft": "\u27E6", + "/dblbracketright": "\u27E7", + "/defines": "\u225C", + "/diamondsolid": "\u25C6", + "/difference": "\u224F", + "/dividemultiply": "\u22C7", + "/downfall": "\u22CE", + "/epsiloninv": "\u03F6", + "/equaldotleftright": "\u2252", + "/equaldotrightleft": "\u2253", + "/equalorfollows": "\u22DF", + "/equalorsimilar": "\u2242", + "/equalsdots": "\u2251", + "/follownotdbleqv": "\u2ABA", + "/follownotslnteql": "\u2AB6", + "/followornoteqvlnt": "\u22E9", + "/followsorcurly": "\u227D", + "/followsorequal": "\u227F", + "/forcesbar": "\u22AA", + "/geomequivalent": "\u224E", + "/harpoondownleft": "\u21C3", + "/harpoondownright": "\u21C2", + "/harpoonleftright": "\u21CB", + "/harpoonrightleft": "\u21CC", + "/harpoonupleft": "\u21BF", + "/harpoonupright": "\u21BE", + "/hyphenchar": "\u002D", + "/hyphenminus": "\u002D", + "/intercal": "\u22BA", + "/interrobangdown": "\u2E18", + "/lessdbleqlgreater": "\u2A8B", + "/lessdblequal": "\u2266", + "/lessequalgreater": "\u22DA", + "/lessnotdblequal": "\u2268", + "/lessorapproxeql": "\u2A85", + "/lessorequalslant": "\u2A7D", + "/lessornotdbleql": "\u2268", + "/lessornotequal": "\u2268", + "/lessorsimilar": "\u2272", + "/longdbls": "\u017F\u017F", + "/longsh": "\u017F\u0068", + "/longsi": "\u017F\u0069", + "/longsl": "\u017F\u006C", + "/longst": "\u017F\u0074", + "/multicloseleft": "\u22C9", + "/multicloseright": "\u22CA", + "/multiopenleft": "\u22CB", + "/multiopenright": "\u22CC", + "/notapproxequal": "\u2247", + "/notarrowboth": "\u21AE", + "/notarrowleft": "\u219A", + "/notarrowright": "\u219B", + "/notbar": "\u2224", + "/notdblarrowboth": "\u21CE", + "/notdblarrowleft": "\u21CD", + "/notdblarrowright": "\u21CF", + "/notfollows": "\u2281", + "/notfollowsoreql": "\u22E1", + "/notforces": "\u22AE", + "/notforcesextra": "\u22AF", + "/notgreaterequal": "\u2271", + "/notlessequal": "\u2270", + "/notsatisfies": "\u22AD", + "/notsimilar": "\u2241", + "/notsubseteql": "\u2288", + "/notsubsetoreql": "\u228A", + "/notsuperseteql": "\u2289", + "/notsupersetoreql": "\u228B", + "/nottriangeqlleft": "\u22EC", + "/nottriangeqlright": "\u22ED", + "/nottriangleleft": "\u22EA", + "/nottriangleright": "\u22EB", + "/notturnstile": "\u22AC", + "/orunderscore": "\u22BB", + "/planckover2pi": "\u210F", + "/planckover2pi1": "\u210F", + "/precedenotdbleqv": "\u2AB9", + "/precedenotslnteql": "\u2AB5", + "/precedeornoteqvlnt": "\u22E8", + "/precedesorcurly": "\u227C", + "/primereverse": "\u2035", + "/punctdash": "\u2014", + "/rangedash": "\u2013", + "/revasymptequal": "\u22CD", + "/revsimilar": "\u223D", + "/rightanglene": "\u231D", + "/rightanglenw": "\u231C", + "/rightanglese": "\u231F", + "/rightanglesw": "\u231E", + "/ringingBell": "\U0001F56D", + "/satisfies": "\u22A8", + "/square": "\u25A1", + "/squaredot": "\u22A1", + "/squareminus": "\u229F", + "/squaremultiply": "\u22A0", + "/squareplus": "\u229E", + "/squaresolid": "\u25A0", + "/squiggleleftright": "\u21AD", + "/squiggleright": "\u219D", + "/st": "\u0073\u0074", + "/subsetdblequal": "\u2AC5", + "/subsetnoteql": "\u228A", + "/subsetornotdbleql": "\u2ACB", + "/supersetdbl": "\u22D1", + "/supersetdblequal": "\u2AC6", + "/supersetnoteql": "\u228B", + "/supersetornotdbleql": "\u2ACC", + "/triangledownsld": "\u25BC", + "/triangleleftequal": "\u22B4", + "/triangleleftsld": "\u25C0", + "/trianglerightequal": "\u22B5", + "/trianglerightsld": "\u25B6", + "/trianglesolid": "\u25B2", + "/uprise": "\u22CF", + "/visualspace": "\u2423", +} + + +def _complete() -> None: + for i in range(256): + adobe_glyphs[f"/a{i}"] = chr(i) + adobe_glyphs["/.notdef"] = "□" + + +_complete() diff --git a/app/vendor/pypdf/_codecs/core_font_metrics.py b/app/vendor/pypdf/_codecs/core_font_metrics.py new file mode 100644 index 0000000..f086ef7 --- /dev/null +++ b/app/vendor/pypdf/_codecs/core_font_metrics.py @@ -0,0 +1,4483 @@ +# This file is based upon the 14 core AFM files provided by Adobe/Macromedia at +# https://download.macromedia.com/pub/developer/opentype/tech-notes/Core14_AFMs.zip +# The original copyright follows: +# +# ----------------------------------------------------------------------------------------------- +# Core 14 AFM Files - ReadMe +# +# This file and the 14 PostScript(R) AFM files it accompanies may be used, copied, and +# distributed for any purpose and without charge, with or without modification, provided that all +# copyright notices are retained; that the AFM files are not distributed without this file; that +# all modifications to this file or any of the AFM files are prominently noted in the modified +# file(s); and that this paragraph is not modified. Adobe Systems has no responsibility or +# obligation to support the use of the AFM files. +# ----------------------------------------------------------------------------------------------- + + +from pypdf._font import CoreFontMetrics, FontDescriptor + +CORE_FONT_METRICS: dict[str, CoreFontMetrics] = { + # Generated from Courier.afm + # Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights + # Reserved. + "Courier": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Courier", + family="Courier", + weight="Medium", + ascent=629, + descent=-157, + cap_height=562, + x_height=426, + italic_angle=0, + flags=33, + bbox=(-23.0, -250.0, 715.0, 805.0), + ), + character_widths={ + " ": 600, + "default": 600, + "!": 600, + '"': 600, + "#": 600, + "$": 600, + "%": 600, + "&": 600, + "\u2019": 600, + "(": 600, + ")": 600, + "*": 600, + "+": 600, + ",": 600, + "-": 600, + ".": 600, + "/": 600, + "0": 600, + "1": 600, + "2": 600, + "3": 600, + "4": 600, + "5": 600, + "6": 600, + "7": 600, + "8": 600, + "9": 600, + ":": 600, + ";": 600, + "<": 600, + "=": 600, + ">": 600, + "?": 600, + "@": 600, + "A": 600, + "B": 600, + "C": 600, + "D": 600, + "E": 600, + "F": 600, + "G": 600, + "H": 600, + "I": 600, + "J": 600, + "K": 600, + "L": 600, + "M": 600, + "N": 600, + "O": 600, + "P": 600, + "Q": 600, + "R": 600, + "S": 600, + "T": 600, + "U": 600, + "V": 600, + "W": 600, + "X": 600, + "Y": 600, + "Z": 600, + "[": 600, + "\\": 600, + "]": 600, + "^": 600, + "_": 600, + "\u2018": 600, + "a": 600, + "b": 600, + "c": 600, + "d": 600, + "e": 600, + "f": 600, + "g": 600, + "h": 600, + "i": 600, + "j": 600, + "k": 600, + "l": 600, + "m": 600, + "n": 600, + "o": 600, + "p": 600, + "q": 600, + "r": 600, + "s": 600, + "t": 600, + "u": 600, + "v": 600, + "w": 600, + "x": 600, + "y": 600, + "z": 600, + "{": 600, + "|": 600, + "}": 600, + "~": 600, + "\xa1": 600, + "\xa2": 600, + "\xa3": 600, + "\u2044": 600, + "\xa5": 600, + "\u0192": 600, + "\xa7": 600, + "\xa4": 600, + "'": 600, + "\u201c": 600, + "\xab": 600, + "\u2039": 600, + "\u203a": 600, + "\ufb01": 600, + "\ufb02": 600, + "\u2013": 600, + "\u2020": 600, + "\u2021": 600, + "\xb7": 600, + "\xb6": 600, + "\u2022": 600, + "\u201a": 600, + "\u201e": 600, + "\u201d": 600, + "\xbb": 600, + "\u2026": 600, + "\u2030": 600, + "\xbf": 600, + "`": 600, + "\xb4": 600, + "\u02c6": 600, + "\u02dc": 600, + "\xaf": 600, + "\u02d8": 600, + "\u02d9": 600, + "\xa8": 600, + "\u02da": 600, + "\xb8": 600, + "\u02dd": 600, + "\u02db": 600, + "\u02c7": 600, + "\u2014": 600, + "\xc6": 600, + "\xaa": 600, + "\u0141": 600, + "\xd8": 600, + "\u0152": 600, + "\xba": 600, + "\xe6": 600, + "\u0131": 600, + "\u0142": 600, + "\xf8": 600, + "\u0153": 600, + "\xdf": 600, + "\xcf": 600, + "\xe9": 600, + "\u0103": 600, + "\u0171": 600, + "\u011b": 600, + "\u0178": 600, + "\xf7": 600, + "\xdd": 600, + "\xc2": 600, + "\xe1": 600, + "\xdb": 600, + "\xfd": 600, + "\u0219": 600, + "\xea": 600, + "\u016e": 600, + "\xdc": 600, + "\u0105": 600, + "\xda": 600, + "\u0173": 600, + "\xcb": 600, + "\u0110": 600, + "\uf6c3": 600, + "\xa9": 600, + "\u0112": 600, + "\u010d": 600, + "\xe5": 600, + "\u0145": 600, + "\u013a": 600, + "\xe0": 600, + "\u0162": 600, + "\u0106": 600, + "\xe3": 600, + "\u0116": 600, + "\u0161": 600, + "\u015f": 600, + "\xed": 600, + "\u25ca": 600, + "\u0158": 600, + "\u0122": 600, + "\xfb": 600, + "\xe2": 600, + "\u0100": 600, + "\u0159": 600, + "\xe7": 600, + "\u017b": 600, + "\xde": 600, + "\u014c": 600, + "\u0154": 600, + "\u015a": 600, + "\u010f": 600, + "\u016a": 600, + "\u016f": 600, + "\xb3": 600, + "\xd2": 600, + "\xc0": 600, + "\u0102": 600, + "\xd7": 600, + "\xfa": 600, + "\u0164": 600, + "\u2202": 600, + "\xff": 600, + "\u0143": 600, + "\xee": 600, + "\xca": 600, + "\xe4": 600, + "\xeb": 600, + "\u0107": 600, + "\u0144": 600, + "\u016b": 600, + "\u0147": 600, + "\xcd": 600, + "\xb1": 600, + "\xa6": 600, + "\xae": 600, + "\u011e": 600, + "\u0130": 600, + "\u2211": 600, + "\xc8": 600, + "\u0155": 600, + "\u014d": 600, + "\u0179": 600, + "\u017d": 600, + "\u2265": 600, + "\xd0": 600, + "\xc7": 600, + "\u013c": 600, + "\u0165": 600, + "\u0119": 600, + "\u0172": 600, + "\xc1": 600, + "\xc4": 600, + "\xe8": 600, + "\u017a": 600, + "\u012f": 600, + "\xd3": 600, + "\xf3": 600, + "\u0101": 600, + "\u015b": 600, + "\xef": 600, + "\xd4": 600, + "\xd9": 600, + "\u2206": 600, + "\xfe": 600, + "\xb2": 600, + "\xd6": 600, + "\xb5": 600, + "\xec": 600, + "\u0151": 600, + "\u0118": 600, + "\u0111": 600, + "\xbe": 600, + "\u015e": 600, + "\u013e": 600, + "\u0136": 600, + "\u0139": 600, + "\u2122": 600, + "\u0117": 600, + "\xcc": 600, + "\u012a": 600, + "\u013d": 600, + "\xbd": 600, + "\u2264": 600, + "\xf4": 600, + "\xf1": 600, + "\u0170": 600, + "\xc9": 600, + "\u0113": 600, + "\u011f": 600, + "\xbc": 600, + "\u0160": 600, + "\u0218": 600, + "\u0150": 600, + "\xb0": 600, + "\xf2": 600, + "\u010c": 600, + "\xf9": 600, + "\u221a": 600, + "\u010e": 600, + "\u0157": 600, + "\xd1": 600, + "\xf5": 600, + "\u0156": 600, + "\u013b": 600, + "\xc3": 600, + "\u0104": 600, + "\xc5": 600, + "\xd5": 600, + "\u017c": 600, + "\u011a": 600, + "\u012e": 600, + "\u0137": 600, + "\u2212": 600, + "\xce": 600, + "\u0148": 600, + "\u0163": 600, + "\xac": 600, + "\xf6": 600, + "\xfc": 600, + "\u2260": 600, + "\u0123": 600, + "\xf0": 600, + "\u017e": 600, + "\u0146": 600, + "\xb9": 600, + "\u012b": 600, + "\u20ac": 600, + }, + ), + # Generated from Courier-Bold.afm + # Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. + "Courier-Bold": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Courier-Bold", + family="Courier", + weight="Bold", + ascent=629, + descent=-157, + cap_height=562, + x_height=439, + italic_angle=0, + flags=33, + bbox=(-113.0, -250.0, 749.0, 801.0), + ), + character_widths={ + " ": 600, + "default": 600, + "!": 600, + '"': 600, + "#": 600, + "$": 600, + "%": 600, + "&": 600, + "\u2019": 600, + "(": 600, + ")": 600, + "*": 600, + "+": 600, + ",": 600, + "-": 600, + ".": 600, + "/": 600, + "0": 600, + "1": 600, + "2": 600, + "3": 600, + "4": 600, + "5": 600, + "6": 600, + "7": 600, + "8": 600, + "9": 600, + ":": 600, + ";": 600, + "<": 600, + "=": 600, + ">": 600, + "?": 600, + "@": 600, + "A": 600, + "B": 600, + "C": 600, + "D": 600, + "E": 600, + "F": 600, + "G": 600, + "H": 600, + "I": 600, + "J": 600, + "K": 600, + "L": 600, + "M": 600, + "N": 600, + "O": 600, + "P": 600, + "Q": 600, + "R": 600, + "S": 600, + "T": 600, + "U": 600, + "V": 600, + "W": 600, + "X": 600, + "Y": 600, + "Z": 600, + "[": 600, + "\\": 600, + "]": 600, + "^": 600, + "_": 600, + "\u2018": 600, + "a": 600, + "b": 600, + "c": 600, + "d": 600, + "e": 600, + "f": 600, + "g": 600, + "h": 600, + "i": 600, + "j": 600, + "k": 600, + "l": 600, + "m": 600, + "n": 600, + "o": 600, + "p": 600, + "q": 600, + "r": 600, + "s": 600, + "t": 600, + "u": 600, + "v": 600, + "w": 600, + "x": 600, + "y": 600, + "z": 600, + "{": 600, + "|": 600, + "}": 600, + "~": 600, + "\xa1": 600, + "\xa2": 600, + "\xa3": 600, + "\u2044": 600, + "\xa5": 600, + "\u0192": 600, + "\xa7": 600, + "\xa4": 600, + "'": 600, + "\u201c": 600, + "\xab": 600, + "\u2039": 600, + "\u203a": 600, + "\ufb01": 600, + "\ufb02": 600, + "\u2013": 600, + "\u2020": 600, + "\u2021": 600, + "\xb7": 600, + "\xb6": 600, + "\u2022": 600, + "\u201a": 600, + "\u201e": 600, + "\u201d": 600, + "\xbb": 600, + "\u2026": 600, + "\u2030": 600, + "\xbf": 600, + "`": 600, + "\xb4": 600, + "\u02c6": 600, + "\u02dc": 600, + "\xaf": 600, + "\u02d8": 600, + "\u02d9": 600, + "\xa8": 600, + "\u02da": 600, + "\xb8": 600, + "\u02dd": 600, + "\u02db": 600, + "\u02c7": 600, + "\u2014": 600, + "\xc6": 600, + "\xaa": 600, + "\u0141": 600, + "\xd8": 600, + "\u0152": 600, + "\xba": 600, + "\xe6": 600, + "\u0131": 600, + "\u0142": 600, + "\xf8": 600, + "\u0153": 600, + "\xdf": 600, + "\xcf": 600, + "\xe9": 600, + "\u0103": 600, + "\u0171": 600, + "\u011b": 600, + "\u0178": 600, + "\xf7": 600, + "\xdd": 600, + "\xc2": 600, + "\xe1": 600, + "\xdb": 600, + "\xfd": 600, + "\u0219": 600, + "\xea": 600, + "\u016e": 600, + "\xdc": 600, + "\u0105": 600, + "\xda": 600, + "\u0173": 600, + "\xcb": 600, + "\u0110": 600, + "\uf6c3": 600, + "\xa9": 600, + "\u0112": 600, + "\u010d": 600, + "\xe5": 600, + "\u0145": 600, + "\u013a": 600, + "\xe0": 600, + "\u0162": 600, + "\u0106": 600, + "\xe3": 600, + "\u0116": 600, + "\u0161": 600, + "\u015f": 600, + "\xed": 600, + "\u25ca": 600, + "\u0158": 600, + "\u0122": 600, + "\xfb": 600, + "\xe2": 600, + "\u0100": 600, + "\u0159": 600, + "\xe7": 600, + "\u017b": 600, + "\xde": 600, + "\u014c": 600, + "\u0154": 600, + "\u015a": 600, + "\u010f": 600, + "\u016a": 600, + "\u016f": 600, + "\xb3": 600, + "\xd2": 600, + "\xc0": 600, + "\u0102": 600, + "\xd7": 600, + "\xfa": 600, + "\u0164": 600, + "\u2202": 600, + "\xff": 600, + "\u0143": 600, + "\xee": 600, + "\xca": 600, + "\xe4": 600, + "\xeb": 600, + "\u0107": 600, + "\u0144": 600, + "\u016b": 600, + "\u0147": 600, + "\xcd": 600, + "\xb1": 600, + "\xa6": 600, + "\xae": 600, + "\u011e": 600, + "\u0130": 600, + "\u2211": 600, + "\xc8": 600, + "\u0155": 600, + "\u014d": 600, + "\u0179": 600, + "\u017d": 600, + "\u2265": 600, + "\xd0": 600, + "\xc7": 600, + "\u013c": 600, + "\u0165": 600, + "\u0119": 600, + "\u0172": 600, + "\xc1": 600, + "\xc4": 600, + "\xe8": 600, + "\u017a": 600, + "\u012f": 600, + "\xd3": 600, + "\xf3": 600, + "\u0101": 600, + "\u015b": 600, + "\xef": 600, + "\xd4": 600, + "\xd9": 600, + "\u2206": 600, + "\xfe": 600, + "\xb2": 600, + "\xd6": 600, + "\xb5": 600, + "\xec": 600, + "\u0151": 600, + "\u0118": 600, + "\u0111": 600, + "\xbe": 600, + "\u015e": 600, + "\u013e": 600, + "\u0136": 600, + "\u0139": 600, + "\u2122": 600, + "\u0117": 600, + "\xcc": 600, + "\u012a": 600, + "\u013d": 600, + "\xbd": 600, + "\u2264": 600, + "\xf4": 600, + "\xf1": 600, + "\u0170": 600, + "\xc9": 600, + "\u0113": 600, + "\u011f": 600, + "\xbc": 600, + "\u0160": 600, + "\u0218": 600, + "\u0150": 600, + "\xb0": 600, + "\xf2": 600, + "\u010c": 600, + "\xf9": 600, + "\u221a": 600, + "\u010e": 600, + "\u0157": 600, + "\xd1": 600, + "\xf5": 600, + "\u0156": 600, + "\u013b": 600, + "\xc3": 600, + "\u0104": 600, + "\xc5": 600, + "\xd5": 600, + "\u017c": 600, + "\u011a": 600, + "\u012e": 600, + "\u0137": 600, + "\u2212": 600, + "\xce": 600, + "\u0148": 600, + "\u0163": 600, + "\xac": 600, + "\xf6": 600, + "\xfc": 600, + "\u2260": 600, + "\u0123": 600, + "\xf0": 600, + "\u017e": 600, + "\u0146": 600, + "\xb9": 600, + "\u012b": 600, + "\u20ac": 600, + }, + ), + # Generated from Courier-BoldOblique.afm + # Copyright (c) 1989, 1990, 1991, 1993, 1997 Adobe Systems Incorporated. All Rights Reserved. + "Courier-BoldOblique": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Courier-BoldOblique", + family="Courier", + weight="Bold", + ascent=629, + descent=-157, + cap_height=562, + x_height=439, + italic_angle=-12, + flags=97, + bbox=(-57.0, -250.0, 869.0, 801.0), + ), + character_widths={ + " ": 600, + "default": 600, + "!": 600, + '"': 600, + "#": 600, + "$": 600, + "%": 600, + "&": 600, + "\u2019": 600, + "(": 600, + ")": 600, + "*": 600, + "+": 600, + ",": 600, + "-": 600, + ".": 600, + "/": 600, + "0": 600, + "1": 600, + "2": 600, + "3": 600, + "4": 600, + "5": 600, + "6": 600, + "7": 600, + "8": 600, + "9": 600, + ":": 600, + ";": 600, + "<": 600, + "=": 600, + ">": 600, + "?": 600, + "@": 600, + "A": 600, + "B": 600, + "C": 600, + "D": 600, + "E": 600, + "F": 600, + "G": 600, + "H": 600, + "I": 600, + "J": 600, + "K": 600, + "L": 600, + "M": 600, + "N": 600, + "O": 600, + "P": 600, + "Q": 600, + "R": 600, + "S": 600, + "T": 600, + "U": 600, + "V": 600, + "W": 600, + "X": 600, + "Y": 600, + "Z": 600, + "[": 600, + "\\": 600, + "]": 600, + "^": 600, + "_": 600, + "\u2018": 600, + "a": 600, + "b": 600, + "c": 600, + "d": 600, + "e": 600, + "f": 600, + "g": 600, + "h": 600, + "i": 600, + "j": 600, + "k": 600, + "l": 600, + "m": 600, + "n": 600, + "o": 600, + "p": 600, + "q": 600, + "r": 600, + "s": 600, + "t": 600, + "u": 600, + "v": 600, + "w": 600, + "x": 600, + "y": 600, + "z": 600, + "{": 600, + "|": 600, + "}": 600, + "~": 600, + "\xa1": 600, + "\xa2": 600, + "\xa3": 600, + "\u2044": 600, + "\xa5": 600, + "\u0192": 600, + "\xa7": 600, + "\xa4": 600, + "'": 600, + "\u201c": 600, + "\xab": 600, + "\u2039": 600, + "\u203a": 600, + "\ufb01": 600, + "\ufb02": 600, + "\u2013": 600, + "\u2020": 600, + "\u2021": 600, + "\xb7": 600, + "\xb6": 600, + "\u2022": 600, + "\u201a": 600, + "\u201e": 600, + "\u201d": 600, + "\xbb": 600, + "\u2026": 600, + "\u2030": 600, + "\xbf": 600, + "`": 600, + "\xb4": 600, + "\u02c6": 600, + "\u02dc": 600, + "\xaf": 600, + "\u02d8": 600, + "\u02d9": 600, + "\xa8": 600, + "\u02da": 600, + "\xb8": 600, + "\u02dd": 600, + "\u02db": 600, + "\u02c7": 600, + "\u2014": 600, + "\xc6": 600, + "\xaa": 600, + "\u0141": 600, + "\xd8": 600, + "\u0152": 600, + "\xba": 600, + "\xe6": 600, + "\u0131": 600, + "\u0142": 600, + "\xf8": 600, + "\u0153": 600, + "\xdf": 600, + "\xcf": 600, + "\xe9": 600, + "\u0103": 600, + "\u0171": 600, + "\u011b": 600, + "\u0178": 600, + "\xf7": 600, + "\xdd": 600, + "\xc2": 600, + "\xe1": 600, + "\xdb": 600, + "\xfd": 600, + "\u0219": 600, + "\xea": 600, + "\u016e": 600, + "\xdc": 600, + "\u0105": 600, + "\xda": 600, + "\u0173": 600, + "\xcb": 600, + "\u0110": 600, + "\uf6c3": 600, + "\xa9": 600, + "\u0112": 600, + "\u010d": 600, + "\xe5": 600, + "\u0145": 600, + "\u013a": 600, + "\xe0": 600, + "\u0162": 600, + "\u0106": 600, + "\xe3": 600, + "\u0116": 600, + "\u0161": 600, + "\u015f": 600, + "\xed": 600, + "\u25ca": 600, + "\u0158": 600, + "\u0122": 600, + "\xfb": 600, + "\xe2": 600, + "\u0100": 600, + "\u0159": 600, + "\xe7": 600, + "\u017b": 600, + "\xde": 600, + "\u014c": 600, + "\u0154": 600, + "\u015a": 600, + "\u010f": 600, + "\u016a": 600, + "\u016f": 600, + "\xb3": 600, + "\xd2": 600, + "\xc0": 600, + "\u0102": 600, + "\xd7": 600, + "\xfa": 600, + "\u0164": 600, + "\u2202": 600, + "\xff": 600, + "\u0143": 600, + "\xee": 600, + "\xca": 600, + "\xe4": 600, + "\xeb": 600, + "\u0107": 600, + "\u0144": 600, + "\u016b": 600, + "\u0147": 600, + "\xcd": 600, + "\xb1": 600, + "\xa6": 600, + "\xae": 600, + "\u011e": 600, + "\u0130": 600, + "\u2211": 600, + "\xc8": 600, + "\u0155": 600, + "\u014d": 600, + "\u0179": 600, + "\u017d": 600, + "\u2265": 600, + "\xd0": 600, + "\xc7": 600, + "\u013c": 600, + "\u0165": 600, + "\u0119": 600, + "\u0172": 600, + "\xc1": 600, + "\xc4": 600, + "\xe8": 600, + "\u017a": 600, + "\u012f": 600, + "\xd3": 600, + "\xf3": 600, + "\u0101": 600, + "\u015b": 600, + "\xef": 600, + "\xd4": 600, + "\xd9": 600, + "\u2206": 600, + "\xfe": 600, + "\xb2": 600, + "\xd6": 600, + "\xb5": 600, + "\xec": 600, + "\u0151": 600, + "\u0118": 600, + "\u0111": 600, + "\xbe": 600, + "\u015e": 600, + "\u013e": 600, + "\u0136": 600, + "\u0139": 600, + "\u2122": 600, + "\u0117": 600, + "\xcc": 600, + "\u012a": 600, + "\u013d": 600, + "\xbd": 600, + "\u2264": 600, + "\xf4": 600, + "\xf1": 600, + "\u0170": 600, + "\xc9": 600, + "\u0113": 600, + "\u011f": 600, + "\xbc": 600, + "\u0160": 600, + "\u0218": 600, + "\u0150": 600, + "\xb0": 600, + "\xf2": 600, + "\u010c": 600, + "\xf9": 600, + "\u221a": 600, + "\u010e": 600, + "\u0157": 600, + "\xd1": 600, + "\xf5": 600, + "\u0156": 600, + "\u013b": 600, + "\xc3": 600, + "\u0104": 600, + "\xc5": 600, + "\xd5": 600, + "\u017c": 600, + "\u011a": 600, + "\u012e": 600, + "\u0137": 600, + "\u2212": 600, + "\xce": 600, + "\u0148": 600, + "\u0163": 600, + "\xac": 600, + "\xf6": 600, + "\xfc": 600, + "\u2260": 600, + "\u0123": 600, + "\xf0": 600, + "\u017e": 600, + "\u0146": 600, + "\xb9": 600, + "\u012b": 600, + "\u20ac": 600, + }, + ), + # Generated from Courier-Oblique.afm + # Copyright (c) 1989, 1990, 1991, 1992, 1993, 1997 Adobe Systems Incorporated. All Rights + # Reserved. + "Courier-Oblique": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Courier-Oblique", + family="Courier", + weight="Medium", + ascent=629, + descent=-157, + cap_height=562, + x_height=426, + italic_angle=-12, + flags=97, + bbox=(-27.0, -250.0, 849.0, 805.0), + ), + character_widths={ + " ": 600, + "default": 600, + "!": 600, + '"': 600, + "#": 600, + "$": 600, + "%": 600, + "&": 600, + "\u2019": 600, + "(": 600, + ")": 600, + "*": 600, + "+": 600, + ",": 600, + "-": 600, + ".": 600, + "/": 600, + "0": 600, + "1": 600, + "2": 600, + "3": 600, + "4": 600, + "5": 600, + "6": 600, + "7": 600, + "8": 600, + "9": 600, + ":": 600, + ";": 600, + "<": 600, + "=": 600, + ">": 600, + "?": 600, + "@": 600, + "A": 600, + "B": 600, + "C": 600, + "D": 600, + "E": 600, + "F": 600, + "G": 600, + "H": 600, + "I": 600, + "J": 600, + "K": 600, + "L": 600, + "M": 600, + "N": 600, + "O": 600, + "P": 600, + "Q": 600, + "R": 600, + "S": 600, + "T": 600, + "U": 600, + "V": 600, + "W": 600, + "X": 600, + "Y": 600, + "Z": 600, + "[": 600, + "\\": 600, + "]": 600, + "^": 600, + "_": 600, + "\u2018": 600, + "a": 600, + "b": 600, + "c": 600, + "d": 600, + "e": 600, + "f": 600, + "g": 600, + "h": 600, + "i": 600, + "j": 600, + "k": 600, + "l": 600, + "m": 600, + "n": 600, + "o": 600, + "p": 600, + "q": 600, + "r": 600, + "s": 600, + "t": 600, + "u": 600, + "v": 600, + "w": 600, + "x": 600, + "y": 600, + "z": 600, + "{": 600, + "|": 600, + "}": 600, + "~": 600, + "\xa1": 600, + "\xa2": 600, + "\xa3": 600, + "\u2044": 600, + "\xa5": 600, + "\u0192": 600, + "\xa7": 600, + "\xa4": 600, + "'": 600, + "\u201c": 600, + "\xab": 600, + "\u2039": 600, + "\u203a": 600, + "\ufb01": 600, + "\ufb02": 600, + "\u2013": 600, + "\u2020": 600, + "\u2021": 600, + "\xb7": 600, + "\xb6": 600, + "\u2022": 600, + "\u201a": 600, + "\u201e": 600, + "\u201d": 600, + "\xbb": 600, + "\u2026": 600, + "\u2030": 600, + "\xbf": 600, + "`": 600, + "\xb4": 600, + "\u02c6": 600, + "\u02dc": 600, + "\xaf": 600, + "\u02d8": 600, + "\u02d9": 600, + "\xa8": 600, + "\u02da": 600, + "\xb8": 600, + "\u02dd": 600, + "\u02db": 600, + "\u02c7": 600, + "\u2014": 600, + "\xc6": 600, + "\xaa": 600, + "\u0141": 600, + "\xd8": 600, + "\u0152": 600, + "\xba": 600, + "\xe6": 600, + "\u0131": 600, + "\u0142": 600, + "\xf8": 600, + "\u0153": 600, + "\xdf": 600, + "\xcf": 600, + "\xe9": 600, + "\u0103": 600, + "\u0171": 600, + "\u011b": 600, + "\u0178": 600, + "\xf7": 600, + "\xdd": 600, + "\xc2": 600, + "\xe1": 600, + "\xdb": 600, + "\xfd": 600, + "\u0219": 600, + "\xea": 600, + "\u016e": 600, + "\xdc": 600, + "\u0105": 600, + "\xda": 600, + "\u0173": 600, + "\xcb": 600, + "\u0110": 600, + "\uf6c3": 600, + "\xa9": 600, + "\u0112": 600, + "\u010d": 600, + "\xe5": 600, + "\u0145": 600, + "\u013a": 600, + "\xe0": 600, + "\u0162": 600, + "\u0106": 600, + "\xe3": 600, + "\u0116": 600, + "\u0161": 600, + "\u015f": 600, + "\xed": 600, + "\u25ca": 600, + "\u0158": 600, + "\u0122": 600, + "\xfb": 600, + "\xe2": 600, + "\u0100": 600, + "\u0159": 600, + "\xe7": 600, + "\u017b": 600, + "\xde": 600, + "\u014c": 600, + "\u0154": 600, + "\u015a": 600, + "\u010f": 600, + "\u016a": 600, + "\u016f": 600, + "\xb3": 600, + "\xd2": 600, + "\xc0": 600, + "\u0102": 600, + "\xd7": 600, + "\xfa": 600, + "\u0164": 600, + "\u2202": 600, + "\xff": 600, + "\u0143": 600, + "\xee": 600, + "\xca": 600, + "\xe4": 600, + "\xeb": 600, + "\u0107": 600, + "\u0144": 600, + "\u016b": 600, + "\u0147": 600, + "\xcd": 600, + "\xb1": 600, + "\xa6": 600, + "\xae": 600, + "\u011e": 600, + "\u0130": 600, + "\u2211": 600, + "\xc8": 600, + "\u0155": 600, + "\u014d": 600, + "\u0179": 600, + "\u017d": 600, + "\u2265": 600, + "\xd0": 600, + "\xc7": 600, + "\u013c": 600, + "\u0165": 600, + "\u0119": 600, + "\u0172": 600, + "\xc1": 600, + "\xc4": 600, + "\xe8": 600, + "\u017a": 600, + "\u012f": 600, + "\xd3": 600, + "\xf3": 600, + "\u0101": 600, + "\u015b": 600, + "\xef": 600, + "\xd4": 600, + "\xd9": 600, + "\u2206": 600, + "\xfe": 600, + "\xb2": 600, + "\xd6": 600, + "\xb5": 600, + "\xec": 600, + "\u0151": 600, + "\u0118": 600, + "\u0111": 600, + "\xbe": 600, + "\u015e": 600, + "\u013e": 600, + "\u0136": 600, + "\u0139": 600, + "\u2122": 600, + "\u0117": 600, + "\xcc": 600, + "\u012a": 600, + "\u013d": 600, + "\xbd": 600, + "\u2264": 600, + "\xf4": 600, + "\xf1": 600, + "\u0170": 600, + "\xc9": 600, + "\u0113": 600, + "\u011f": 600, + "\xbc": 600, + "\u0160": 600, + "\u0218": 600, + "\u0150": 600, + "\xb0": 600, + "\xf2": 600, + "\u010c": 600, + "\xf9": 600, + "\u221a": 600, + "\u010e": 600, + "\u0157": 600, + "\xd1": 600, + "\xf5": 600, + "\u0156": 600, + "\u013b": 600, + "\xc3": 600, + "\u0104": 600, + "\xc5": 600, + "\xd5": 600, + "\u017c": 600, + "\u011a": 600, + "\u012e": 600, + "\u0137": 600, + "\u2212": 600, + "\xce": 600, + "\u0148": 600, + "\u0163": 600, + "\xac": 600, + "\xf6": 600, + "\xfc": 600, + "\u2260": 600, + "\u0123": 600, + "\xf0": 600, + "\u017e": 600, + "\u0146": 600, + "\xb9": 600, + "\u012b": 600, + "\u20ac": 600, + }, + ), + # Generated from Helvetica.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. + # Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries. + "Helvetica": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Helvetica", + family="Helvetica", + weight="Medium", + ascent=718, + descent=-207, + cap_height=718, + x_height=523, + italic_angle=0, + flags=32, + bbox=(-166.0, -225.0, 1000.0, 931.0), + ), + character_widths={ + " ": 278, + "default": 556, + "!": 278, + '"': 355, + "#": 556, + "$": 556, + "%": 889, + "&": 667, + "\u2019": 222, + "(": 333, + ")": 333, + "*": 389, + "+": 584, + ",": 278, + "-": 333, + ".": 278, + "/": 278, + "0": 556, + "1": 556, + "2": 556, + "3": 556, + "4": 556, + "5": 556, + "6": 556, + "7": 556, + "8": 556, + "9": 556, + ":": 278, + ";": 278, + "<": 584, + "=": 584, + ">": 584, + "?": 556, + "@": 1015, + "A": 667, + "B": 667, + "C": 722, + "D": 722, + "E": 667, + "F": 611, + "G": 778, + "H": 722, + "I": 278, + "J": 500, + "K": 667, + "L": 556, + "M": 833, + "N": 722, + "O": 778, + "P": 667, + "Q": 778, + "R": 722, + "S": 667, + "T": 611, + "U": 722, + "V": 667, + "W": 944, + "X": 667, + "Y": 667, + "Z": 611, + "[": 278, + "\\": 278, + "]": 278, + "^": 469, + "_": 556, + "\u2018": 222, + "a": 556, + "b": 556, + "c": 500, + "d": 556, + "e": 556, + "f": 278, + "g": 556, + "h": 556, + "i": 222, + "j": 222, + "k": 500, + "l": 222, + "m": 833, + "n": 556, + "o": 556, + "p": 556, + "q": 556, + "r": 333, + "s": 500, + "t": 278, + "u": 556, + "v": 500, + "w": 722, + "x": 500, + "y": 500, + "z": 500, + "{": 334, + "|": 260, + "}": 334, + "~": 584, + "\xa1": 333, + "\xa2": 556, + "\xa3": 556, + "\u2044": 167, + "\xa5": 556, + "\u0192": 556, + "\xa7": 556, + "\xa4": 556, + "'": 191, + "\u201c": 333, + "\xab": 556, + "\u2039": 333, + "\u203a": 333, + "\ufb01": 500, + "\ufb02": 500, + "\u2013": 556, + "\u2020": 556, + "\u2021": 556, + "\xb7": 278, + "\xb6": 537, + "\u2022": 350, + "\u201a": 222, + "\u201e": 333, + "\u201d": 333, + "\xbb": 556, + "\u2026": 1000, + "\u2030": 1000, + "\xbf": 611, + "`": 333, + "\xb4": 333, + "\u02c6": 333, + "\u02dc": 333, + "\xaf": 333, + "\u02d8": 333, + "\u02d9": 333, + "\xa8": 333, + "\u02da": 333, + "\xb8": 333, + "\u02dd": 333, + "\u02db": 333, + "\u02c7": 333, + "\u2014": 1000, + "\xc6": 1000, + "\xaa": 370, + "\u0141": 556, + "\xd8": 778, + "\u0152": 1000, + "\xba": 365, + "\xe6": 889, + "\u0131": 278, + "\u0142": 222, + "\xf8": 611, + "\u0153": 944, + "\xdf": 611, + "\xcf": 278, + "\xe9": 556, + "\u0103": 556, + "\u0171": 556, + "\u011b": 556, + "\u0178": 667, + "\xf7": 584, + "\xdd": 667, + "\xc2": 667, + "\xe1": 556, + "\xdb": 722, + "\xfd": 500, + "\u0219": 500, + "\xea": 556, + "\u016e": 722, + "\xdc": 722, + "\u0105": 556, + "\xda": 722, + "\u0173": 556, + "\xcb": 667, + "\u0110": 722, + "\uf6c3": 250, + "\xa9": 737, + "\u0112": 667, + "\u010d": 500, + "\xe5": 556, + "\u0145": 722, + "\u013a": 222, + "\xe0": 556, + "\u0162": 611, + "\u0106": 722, + "\xe3": 556, + "\u0116": 667, + "\u0161": 500, + "\u015f": 500, + "\xed": 278, + "\u25ca": 471, + "\u0158": 722, + "\u0122": 778, + "\xfb": 556, + "\xe2": 556, + "\u0100": 667, + "\u0159": 333, + "\xe7": 500, + "\u017b": 611, + "\xde": 667, + "\u014c": 778, + "\u0154": 722, + "\u015a": 667, + "\u010f": 643, + "\u016a": 722, + "\u016f": 556, + "\xb3": 333, + "\xd2": 778, + "\xc0": 667, + "\u0102": 667, + "\xd7": 584, + "\xfa": 556, + "\u0164": 611, + "\u2202": 476, + "\xff": 500, + "\u0143": 722, + "\xee": 278, + "\xca": 667, + "\xe4": 556, + "\xeb": 556, + "\u0107": 500, + "\u0144": 556, + "\u016b": 556, + "\u0147": 722, + "\xcd": 278, + "\xb1": 584, + "\xa6": 260, + "\xae": 737, + "\u011e": 778, + "\u0130": 278, + "\u2211": 600, + "\xc8": 667, + "\u0155": 333, + "\u014d": 556, + "\u0179": 611, + "\u017d": 611, + "\u2265": 549, + "\xd0": 722, + "\xc7": 722, + "\u013c": 222, + "\u0165": 317, + "\u0119": 556, + "\u0172": 722, + "\xc1": 667, + "\xc4": 667, + "\xe8": 556, + "\u017a": 500, + "\u012f": 222, + "\xd3": 778, + "\xf3": 556, + "\u0101": 556, + "\u015b": 500, + "\xef": 278, + "\xd4": 778, + "\xd9": 722, + "\u2206": 612, + "\xfe": 556, + "\xb2": 333, + "\xd6": 778, + "\xb5": 556, + "\xec": 278, + "\u0151": 556, + "\u0118": 667, + "\u0111": 556, + "\xbe": 834, + "\u015e": 667, + "\u013e": 299, + "\u0136": 667, + "\u0139": 556, + "\u2122": 1000, + "\u0117": 556, + "\xcc": 278, + "\u012a": 278, + "\u013d": 556, + "\xbd": 834, + "\u2264": 549, + "\xf4": 556, + "\xf1": 556, + "\u0170": 722, + "\xc9": 667, + "\u0113": 556, + "\u011f": 556, + "\xbc": 834, + "\u0160": 667, + "\u0218": 667, + "\u0150": 778, + "\xb0": 400, + "\xf2": 556, + "\u010c": 722, + "\xf9": 556, + "\u221a": 453, + "\u010e": 722, + "\u0157": 333, + "\xd1": 722, + "\xf5": 556, + "\u0156": 722, + "\u013b": 556, + "\xc3": 667, + "\u0104": 667, + "\xc5": 667, + "\xd5": 778, + "\u017c": 500, + "\u011a": 667, + "\u012e": 278, + "\u0137": 500, + "\u2212": 584, + "\xce": 278, + "\u0148": 556, + "\u0163": 278, + "\xac": 584, + "\xf6": 556, + "\xfc": 556, + "\u2260": 549, + "\u0123": 556, + "\xf0": 556, + "\u017e": 500, + "\u0146": 556, + "\xb9": 333, + "\u012b": 278, + "\u20ac": 556, + }, + ), + # Generated from Helvetica-Bold.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. + # Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries. + "Helvetica-Bold": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Helvetica-Bold", + family="Helvetica", + weight="Bold", + ascent=718, + descent=-207, + cap_height=718, + x_height=532, + italic_angle=0, + flags=32, + bbox=(-170.0, -228.0, 1003.0, 962.0), + ), + character_widths={ + " ": 278, + "default": 556, + "!": 333, + '"': 474, + "#": 556, + "$": 556, + "%": 889, + "&": 722, + "\u2019": 278, + "(": 333, + ")": 333, + "*": 389, + "+": 584, + ",": 278, + "-": 333, + ".": 278, + "/": 278, + "0": 556, + "1": 556, + "2": 556, + "3": 556, + "4": 556, + "5": 556, + "6": 556, + "7": 556, + "8": 556, + "9": 556, + ":": 333, + ";": 333, + "<": 584, + "=": 584, + ">": 584, + "?": 611, + "@": 975, + "A": 722, + "B": 722, + "C": 722, + "D": 722, + "E": 667, + "F": 611, + "G": 778, + "H": 722, + "I": 278, + "J": 556, + "K": 722, + "L": 611, + "M": 833, + "N": 722, + "O": 778, + "P": 667, + "Q": 778, + "R": 722, + "S": 667, + "T": 611, + "U": 722, + "V": 667, + "W": 944, + "X": 667, + "Y": 667, + "Z": 611, + "[": 333, + "\\": 278, + "]": 333, + "^": 584, + "_": 556, + "\u2018": 278, + "a": 556, + "b": 611, + "c": 556, + "d": 611, + "e": 556, + "f": 333, + "g": 611, + "h": 611, + "i": 278, + "j": 278, + "k": 556, + "l": 278, + "m": 889, + "n": 611, + "o": 611, + "p": 611, + "q": 611, + "r": 389, + "s": 556, + "t": 333, + "u": 611, + "v": 556, + "w": 778, + "x": 556, + "y": 556, + "z": 500, + "{": 389, + "|": 280, + "}": 389, + "~": 584, + "\xa1": 333, + "\xa2": 556, + "\xa3": 556, + "\u2044": 167, + "\xa5": 556, + "\u0192": 556, + "\xa7": 556, + "\xa4": 556, + "'": 238, + "\u201c": 500, + "\xab": 556, + "\u2039": 333, + "\u203a": 333, + "\ufb01": 611, + "\ufb02": 611, + "\u2013": 556, + "\u2020": 556, + "\u2021": 556, + "\xb7": 278, + "\xb6": 556, + "\u2022": 350, + "\u201a": 278, + "\u201e": 500, + "\u201d": 500, + "\xbb": 556, + "\u2026": 1000, + "\u2030": 1000, + "\xbf": 611, + "`": 333, + "\xb4": 333, + "\u02c6": 333, + "\u02dc": 333, + "\xaf": 333, + "\u02d8": 333, + "\u02d9": 333, + "\xa8": 333, + "\u02da": 333, + "\xb8": 333, + "\u02dd": 333, + "\u02db": 333, + "\u02c7": 333, + "\u2014": 1000, + "\xc6": 1000, + "\xaa": 370, + "\u0141": 611, + "\xd8": 778, + "\u0152": 1000, + "\xba": 365, + "\xe6": 889, + "\u0131": 278, + "\u0142": 278, + "\xf8": 611, + "\u0153": 944, + "\xdf": 611, + "\xcf": 278, + "\xe9": 556, + "\u0103": 556, + "\u0171": 611, + "\u011b": 556, + "\u0178": 667, + "\xf7": 584, + "\xdd": 667, + "\xc2": 722, + "\xe1": 556, + "\xdb": 722, + "\xfd": 556, + "\u0219": 556, + "\xea": 556, + "\u016e": 722, + "\xdc": 722, + "\u0105": 556, + "\xda": 722, + "\u0173": 611, + "\xcb": 667, + "\u0110": 722, + "\uf6c3": 250, + "\xa9": 737, + "\u0112": 667, + "\u010d": 556, + "\xe5": 556, + "\u0145": 722, + "\u013a": 278, + "\xe0": 556, + "\u0162": 611, + "\u0106": 722, + "\xe3": 556, + "\u0116": 667, + "\u0161": 556, + "\u015f": 556, + "\xed": 278, + "\u25ca": 494, + "\u0158": 722, + "\u0122": 778, + "\xfb": 611, + "\xe2": 556, + "\u0100": 722, + "\u0159": 389, + "\xe7": 556, + "\u017b": 611, + "\xde": 667, + "\u014c": 778, + "\u0154": 722, + "\u015a": 667, + "\u010f": 743, + "\u016a": 722, + "\u016f": 611, + "\xb3": 333, + "\xd2": 778, + "\xc0": 722, + "\u0102": 722, + "\xd7": 584, + "\xfa": 611, + "\u0164": 611, + "\u2202": 494, + "\xff": 556, + "\u0143": 722, + "\xee": 278, + "\xca": 667, + "\xe4": 556, + "\xeb": 556, + "\u0107": 556, + "\u0144": 611, + "\u016b": 611, + "\u0147": 722, + "\xcd": 278, + "\xb1": 584, + "\xa6": 280, + "\xae": 737, + "\u011e": 778, + "\u0130": 278, + "\u2211": 600, + "\xc8": 667, + "\u0155": 389, + "\u014d": 611, + "\u0179": 611, + "\u017d": 611, + "\u2265": 549, + "\xd0": 722, + "\xc7": 722, + "\u013c": 278, + "\u0165": 389, + "\u0119": 556, + "\u0172": 722, + "\xc1": 722, + "\xc4": 722, + "\xe8": 556, + "\u017a": 500, + "\u012f": 278, + "\xd3": 778, + "\xf3": 611, + "\u0101": 556, + "\u015b": 556, + "\xef": 278, + "\xd4": 778, + "\xd9": 722, + "\u2206": 612, + "\xfe": 611, + "\xb2": 333, + "\xd6": 778, + "\xb5": 611, + "\xec": 278, + "\u0151": 611, + "\u0118": 667, + "\u0111": 611, + "\xbe": 834, + "\u015e": 667, + "\u013e": 400, + "\u0136": 722, + "\u0139": 611, + "\u2122": 1000, + "\u0117": 556, + "\xcc": 278, + "\u012a": 278, + "\u013d": 611, + "\xbd": 834, + "\u2264": 549, + "\xf4": 611, + "\xf1": 611, + "\u0170": 722, + "\xc9": 667, + "\u0113": 556, + "\u011f": 611, + "\xbc": 834, + "\u0160": 667, + "\u0218": 667, + "\u0150": 778, + "\xb0": 400, + "\xf2": 611, + "\u010c": 722, + "\xf9": 611, + "\u221a": 549, + "\u010e": 722, + "\u0157": 389, + "\xd1": 722, + "\xf5": 611, + "\u0156": 722, + "\u013b": 611, + "\xc3": 722, + "\u0104": 722, + "\xc5": 722, + "\xd5": 778, + "\u017c": 500, + "\u011a": 667, + "\u012e": 278, + "\u0137": 556, + "\u2212": 584, + "\xce": 278, + "\u0148": 611, + "\u0163": 333, + "\xac": 584, + "\xf6": 611, + "\xfc": 611, + "\u2260": 549, + "\u0123": 611, + "\xf0": 611, + "\u017e": 500, + "\u0146": 611, + "\xb9": 333, + "\u012b": 278, + "\u20ac": 556, + }, + ), + # Generated from Helvetica-BoldOblique.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. + # Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries. + "Helvetica-BoldOblique": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Helvetica-BoldOblique", + family="Helvetica", + weight="Bold", + ascent=718, + descent=-207, + cap_height=718, + x_height=532, + italic_angle=-12, + flags=96, + bbox=(-174.0, -228.0, 1114.0, 962.0), + ), + character_widths={ + " ": 278, + "default": 556, + "!": 333, + '"': 474, + "#": 556, + "$": 556, + "%": 889, + "&": 722, + "\u2019": 278, + "(": 333, + ")": 333, + "*": 389, + "+": 584, + ",": 278, + "-": 333, + ".": 278, + "/": 278, + "0": 556, + "1": 556, + "2": 556, + "3": 556, + "4": 556, + "5": 556, + "6": 556, + "7": 556, + "8": 556, + "9": 556, + ":": 333, + ";": 333, + "<": 584, + "=": 584, + ">": 584, + "?": 611, + "@": 975, + "A": 722, + "B": 722, + "C": 722, + "D": 722, + "E": 667, + "F": 611, + "G": 778, + "H": 722, + "I": 278, + "J": 556, + "K": 722, + "L": 611, + "M": 833, + "N": 722, + "O": 778, + "P": 667, + "Q": 778, + "R": 722, + "S": 667, + "T": 611, + "U": 722, + "V": 667, + "W": 944, + "X": 667, + "Y": 667, + "Z": 611, + "[": 333, + "\\": 278, + "]": 333, + "^": 584, + "_": 556, + "\u2018": 278, + "a": 556, + "b": 611, + "c": 556, + "d": 611, + "e": 556, + "f": 333, + "g": 611, + "h": 611, + "i": 278, + "j": 278, + "k": 556, + "l": 278, + "m": 889, + "n": 611, + "o": 611, + "p": 611, + "q": 611, + "r": 389, + "s": 556, + "t": 333, + "u": 611, + "v": 556, + "w": 778, + "x": 556, + "y": 556, + "z": 500, + "{": 389, + "|": 280, + "}": 389, + "~": 584, + "\xa1": 333, + "\xa2": 556, + "\xa3": 556, + "\u2044": 167, + "\xa5": 556, + "\u0192": 556, + "\xa7": 556, + "\xa4": 556, + "'": 238, + "\u201c": 500, + "\xab": 556, + "\u2039": 333, + "\u203a": 333, + "\ufb01": 611, + "\ufb02": 611, + "\u2013": 556, + "\u2020": 556, + "\u2021": 556, + "\xb7": 278, + "\xb6": 556, + "\u2022": 350, + "\u201a": 278, + "\u201e": 500, + "\u201d": 500, + "\xbb": 556, + "\u2026": 1000, + "\u2030": 1000, + "\xbf": 611, + "`": 333, + "\xb4": 333, + "\u02c6": 333, + "\u02dc": 333, + "\xaf": 333, + "\u02d8": 333, + "\u02d9": 333, + "\xa8": 333, + "\u02da": 333, + "\xb8": 333, + "\u02dd": 333, + "\u02db": 333, + "\u02c7": 333, + "\u2014": 1000, + "\xc6": 1000, + "\xaa": 370, + "\u0141": 611, + "\xd8": 778, + "\u0152": 1000, + "\xba": 365, + "\xe6": 889, + "\u0131": 278, + "\u0142": 278, + "\xf8": 611, + "\u0153": 944, + "\xdf": 611, + "\xcf": 278, + "\xe9": 556, + "\u0103": 556, + "\u0171": 611, + "\u011b": 556, + "\u0178": 667, + "\xf7": 584, + "\xdd": 667, + "\xc2": 722, + "\xe1": 556, + "\xdb": 722, + "\xfd": 556, + "\u0219": 556, + "\xea": 556, + "\u016e": 722, + "\xdc": 722, + "\u0105": 556, + "\xda": 722, + "\u0173": 611, + "\xcb": 667, + "\u0110": 722, + "\uf6c3": 250, + "\xa9": 737, + "\u0112": 667, + "\u010d": 556, + "\xe5": 556, + "\u0145": 722, + "\u013a": 278, + "\xe0": 556, + "\u0162": 611, + "\u0106": 722, + "\xe3": 556, + "\u0116": 667, + "\u0161": 556, + "\u015f": 556, + "\xed": 278, + "\u25ca": 494, + "\u0158": 722, + "\u0122": 778, + "\xfb": 611, + "\xe2": 556, + "\u0100": 722, + "\u0159": 389, + "\xe7": 556, + "\u017b": 611, + "\xde": 667, + "\u014c": 778, + "\u0154": 722, + "\u015a": 667, + "\u010f": 743, + "\u016a": 722, + "\u016f": 611, + "\xb3": 333, + "\xd2": 778, + "\xc0": 722, + "\u0102": 722, + "\xd7": 584, + "\xfa": 611, + "\u0164": 611, + "\u2202": 494, + "\xff": 556, + "\u0143": 722, + "\xee": 278, + "\xca": 667, + "\xe4": 556, + "\xeb": 556, + "\u0107": 556, + "\u0144": 611, + "\u016b": 611, + "\u0147": 722, + "\xcd": 278, + "\xb1": 584, + "\xa6": 280, + "\xae": 737, + "\u011e": 778, + "\u0130": 278, + "\u2211": 600, + "\xc8": 667, + "\u0155": 389, + "\u014d": 611, + "\u0179": 611, + "\u017d": 611, + "\u2265": 549, + "\xd0": 722, + "\xc7": 722, + "\u013c": 278, + "\u0165": 389, + "\u0119": 556, + "\u0172": 722, + "\xc1": 722, + "\xc4": 722, + "\xe8": 556, + "\u017a": 500, + "\u012f": 278, + "\xd3": 778, + "\xf3": 611, + "\u0101": 556, + "\u015b": 556, + "\xef": 278, + "\xd4": 778, + "\xd9": 722, + "\u2206": 612, + "\xfe": 611, + "\xb2": 333, + "\xd6": 778, + "\xb5": 611, + "\xec": 278, + "\u0151": 611, + "\u0118": 667, + "\u0111": 611, + "\xbe": 834, + "\u015e": 667, + "\u013e": 400, + "\u0136": 722, + "\u0139": 611, + "\u2122": 1000, + "\u0117": 556, + "\xcc": 278, + "\u012a": 278, + "\u013d": 611, + "\xbd": 834, + "\u2264": 549, + "\xf4": 611, + "\xf1": 611, + "\u0170": 722, + "\xc9": 667, + "\u0113": 556, + "\u011f": 611, + "\xbc": 834, + "\u0160": 667, + "\u0218": 667, + "\u0150": 778, + "\xb0": 400, + "\xf2": 611, + "\u010c": 722, + "\xf9": 611, + "\u221a": 549, + "\u010e": 722, + "\u0157": 389, + "\xd1": 722, + "\xf5": 611, + "\u0156": 722, + "\u013b": 611, + "\xc3": 722, + "\u0104": 722, + "\xc5": 722, + "\xd5": 778, + "\u017c": 500, + "\u011a": 667, + "\u012e": 278, + "\u0137": 556, + "\u2212": 584, + "\xce": 278, + "\u0148": 611, + "\u0163": 333, + "\xac": 584, + "\xf6": 611, + "\xfc": 611, + "\u2260": 549, + "\u0123": 611, + "\xf0": 611, + "\u017e": 500, + "\u0146": 611, + "\xb9": 333, + "\u012b": 278, + "\u20ac": 556, + }, + ), + # Generated from Helvetica-Oblique.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All Rights Reserved. + # Helvetica is a trademark of Linotype-Hell AG and/or its subsidiaries. + "Helvetica-Oblique": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Helvetica-Oblique", + family="Helvetica", + weight="Medium", + ascent=718, + descent=-207, + cap_height=718, + x_height=523, + italic_angle=-12, + flags=96, + bbox=(-170.0, -225.0, 1116.0, 931.0), + ), + character_widths={ + " ": 278, + "default": 556, + "!": 278, + '"': 355, + "#": 556, + "$": 556, + "%": 889, + "&": 667, + "\u2019": 222, + "(": 333, + ")": 333, + "*": 389, + "+": 584, + ",": 278, + "-": 333, + ".": 278, + "/": 278, + "0": 556, + "1": 556, + "2": 556, + "3": 556, + "4": 556, + "5": 556, + "6": 556, + "7": 556, + "8": 556, + "9": 556, + ":": 278, + ";": 278, + "<": 584, + "=": 584, + ">": 584, + "?": 556, + "@": 1015, + "A": 667, + "B": 667, + "C": 722, + "D": 722, + "E": 667, + "F": 611, + "G": 778, + "H": 722, + "I": 278, + "J": 500, + "K": 667, + "L": 556, + "M": 833, + "N": 722, + "O": 778, + "P": 667, + "Q": 778, + "R": 722, + "S": 667, + "T": 611, + "U": 722, + "V": 667, + "W": 944, + "X": 667, + "Y": 667, + "Z": 611, + "[": 278, + "\\": 278, + "]": 278, + "^": 469, + "_": 556, + "\u2018": 222, + "a": 556, + "b": 556, + "c": 500, + "d": 556, + "e": 556, + "f": 278, + "g": 556, + "h": 556, + "i": 222, + "j": 222, + "k": 500, + "l": 222, + "m": 833, + "n": 556, + "o": 556, + "p": 556, + "q": 556, + "r": 333, + "s": 500, + "t": 278, + "u": 556, + "v": 500, + "w": 722, + "x": 500, + "y": 500, + "z": 500, + "{": 334, + "|": 260, + "}": 334, + "~": 584, + "\xa1": 333, + "\xa2": 556, + "\xa3": 556, + "\u2044": 167, + "\xa5": 556, + "\u0192": 556, + "\xa7": 556, + "\xa4": 556, + "'": 191, + "\u201c": 333, + "\xab": 556, + "\u2039": 333, + "\u203a": 333, + "\ufb01": 500, + "\ufb02": 500, + "\u2013": 556, + "\u2020": 556, + "\u2021": 556, + "\xb7": 278, + "\xb6": 537, + "\u2022": 350, + "\u201a": 222, + "\u201e": 333, + "\u201d": 333, + "\xbb": 556, + "\u2026": 1000, + "\u2030": 1000, + "\xbf": 611, + "`": 333, + "\xb4": 333, + "\u02c6": 333, + "\u02dc": 333, + "\xaf": 333, + "\u02d8": 333, + "\u02d9": 333, + "\xa8": 333, + "\u02da": 333, + "\xb8": 333, + "\u02dd": 333, + "\u02db": 333, + "\u02c7": 333, + "\u2014": 1000, + "\xc6": 1000, + "\xaa": 370, + "\u0141": 556, + "\xd8": 778, + "\u0152": 1000, + "\xba": 365, + "\xe6": 889, + "\u0131": 278, + "\u0142": 222, + "\xf8": 611, + "\u0153": 944, + "\xdf": 611, + "\xcf": 278, + "\xe9": 556, + "\u0103": 556, + "\u0171": 556, + "\u011b": 556, + "\u0178": 667, + "\xf7": 584, + "\xdd": 667, + "\xc2": 667, + "\xe1": 556, + "\xdb": 722, + "\xfd": 500, + "\u0219": 500, + "\xea": 556, + "\u016e": 722, + "\xdc": 722, + "\u0105": 556, + "\xda": 722, + "\u0173": 556, + "\xcb": 667, + "\u0110": 722, + "\uf6c3": 250, + "\xa9": 737, + "\u0112": 667, + "\u010d": 500, + "\xe5": 556, + "\u0145": 722, + "\u013a": 222, + "\xe0": 556, + "\u0162": 611, + "\u0106": 722, + "\xe3": 556, + "\u0116": 667, + "\u0161": 500, + "\u015f": 500, + "\xed": 278, + "\u25ca": 471, + "\u0158": 722, + "\u0122": 778, + "\xfb": 556, + "\xe2": 556, + "\u0100": 667, + "\u0159": 333, + "\xe7": 500, + "\u017b": 611, + "\xde": 667, + "\u014c": 778, + "\u0154": 722, + "\u015a": 667, + "\u010f": 643, + "\u016a": 722, + "\u016f": 556, + "\xb3": 333, + "\xd2": 778, + "\xc0": 667, + "\u0102": 667, + "\xd7": 584, + "\xfa": 556, + "\u0164": 611, + "\u2202": 476, + "\xff": 500, + "\u0143": 722, + "\xee": 278, + "\xca": 667, + "\xe4": 556, + "\xeb": 556, + "\u0107": 500, + "\u0144": 556, + "\u016b": 556, + "\u0147": 722, + "\xcd": 278, + "\xb1": 584, + "\xa6": 260, + "\xae": 737, + "\u011e": 778, + "\u0130": 278, + "\u2211": 600, + "\xc8": 667, + "\u0155": 333, + "\u014d": 556, + "\u0179": 611, + "\u017d": 611, + "\u2265": 549, + "\xd0": 722, + "\xc7": 722, + "\u013c": 222, + "\u0165": 317, + "\u0119": 556, + "\u0172": 722, + "\xc1": 667, + "\xc4": 667, + "\xe8": 556, + "\u017a": 500, + "\u012f": 222, + "\xd3": 778, + "\xf3": 556, + "\u0101": 556, + "\u015b": 500, + "\xef": 278, + "\xd4": 778, + "\xd9": 722, + "\u2206": 612, + "\xfe": 556, + "\xb2": 333, + "\xd6": 778, + "\xb5": 556, + "\xec": 278, + "\u0151": 556, + "\u0118": 667, + "\u0111": 556, + "\xbe": 834, + "\u015e": 667, + "\u013e": 299, + "\u0136": 667, + "\u0139": 556, + "\u2122": 1000, + "\u0117": 556, + "\xcc": 278, + "\u012a": 278, + "\u013d": 556, + "\xbd": 834, + "\u2264": 549, + "\xf4": 556, + "\xf1": 556, + "\u0170": 722, + "\xc9": 667, + "\u0113": 556, + "\u011f": 556, + "\xbc": 834, + "\u0160": 667, + "\u0218": 667, + "\u0150": 778, + "\xb0": 400, + "\xf2": 556, + "\u010c": 722, + "\xf9": 556, + "\u221a": 453, + "\u010e": 722, + "\u0157": 333, + "\xd1": 722, + "\xf5": 556, + "\u0156": 722, + "\u013b": 556, + "\xc3": 667, + "\u0104": 667, + "\xc5": 667, + "\xd5": 778, + "\u017c": 500, + "\u011a": 667, + "\u012e": 278, + "\u0137": 500, + "\u2212": 584, + "\xce": 278, + "\u0148": 556, + "\u0163": 278, + "\xac": 584, + "\xf6": 556, + "\xfc": 556, + "\u2260": 549, + "\u0123": 556, + "\xf0": 556, + "\u017e": 500, + "\u0146": 556, + "\xb9": 333, + "\u012b": 278, + "\u20ac": 556, + }, + ), + # Generated from Symbol.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1997 Adobe Systems Incorporated. All rights reserved. + "Symbol": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Symbol", + family="Symbol", + weight="Medium", + ascent=0.0, + descent=0.0, + cap_height=0.0, + x_height=0.0, + italic_angle=0, + flags=4, + bbox=(-180.0, -293.0, 1090.0, 1010.0), + ), + character_widths={ + " ": 250, + "default": 500, + "!": 333, + "\u2200": 713, + "#": 500, + "\u2203": 549, + "%": 833, + "&": 778, + "\u220b": 439, + "(": 333, + ")": 333, + "\u2217": 500, + "+": 549, + ",": 250, + "\u2212": 549, + ".": 250, + "/": 278, + "0": 500, + "1": 500, + "2": 500, + "3": 500, + "4": 500, + "5": 500, + "6": 500, + "7": 500, + "8": 500, + "9": 500, + ":": 278, + ";": 278, + "<": 549, + "=": 549, + ">": 549, + "?": 444, + "\u2245": 549, + "\u0391": 722, + "\u0392": 667, + "\u03a7": 722, + "\u2206": 612, + "\u0395": 611, + "\u03a6": 763, + "\u0393": 603, + "\u0397": 722, + "\u0399": 333, + "\u03d1": 631, + "\u039a": 722, + "\u039b": 686, + "\u039c": 889, + "\u039d": 722, + "\u039f": 722, + "\u03a0": 768, + "\u0398": 741, + "\u03a1": 556, + "\u03a3": 592, + "\u03a4": 611, + "\u03a5": 690, + "\u03c2": 439, + "\u2126": 768, + "\u039e": 645, + "\u03a8": 795, + "\u0396": 611, + "[": 333, + "\u2234": 863, + "]": 333, + "\u22a5": 658, + "_": 500, + "\uf8e5": 500, + "\u03b1": 631, + "\u03b2": 549, + "\u03c7": 549, + "\u03b4": 494, + "\u03b5": 439, + "\u03c6": 521, + "\u03b3": 411, + "\u03b7": 603, + "\u03b9": 329, + "\u03d5": 603, + "\u03ba": 549, + "\u03bb": 549, + "\xb5": 576, + "\u03bd": 521, + "\u03bf": 549, + "\u03c0": 549, + "\u03b8": 521, + "\u03c1": 549, + "\u03c3": 603, + "\u03c4": 439, + "\u03c5": 576, + "\u03d6": 713, + "\u03c9": 686, + "\u03be": 493, + "\u03c8": 686, + "\u03b6": 494, + "{": 480, + "|": 200, + "}": 480, + "\u223c": 549, + "\u20ac": 750, + "\u03d2": 620, + "\u2032": 247, + "\u2264": 549, + "\u2044": 167, + "\u221e": 713, + "\u0192": 500, + "\u2663": 753, + "\u2666": 753, + "\u2665": 753, + "\u2660": 753, + "\u2194": 1042, + "\u2190": 987, + "\u2191": 603, + "\u2192": 987, + "\u2193": 603, + "\xb0": 400, + "\xb1": 549, + "\u2033": 411, + "\u2265": 549, + "\xd7": 549, + "\u221d": 713, + "\u2202": 494, + "\u2022": 460, + "\xf7": 549, + "\u2260": 549, + "\u2261": 549, + "\u2248": 549, + "\u2026": 1000, + "\uf8e6": 603, + "\uf8e7": 1000, + "\u21b5": 658, + "\u2135": 823, + "\u2111": 686, + "\u211c": 795, + "\u2118": 987, + "\u2297": 768, + "\u2295": 768, + "\u2205": 823, + "\u2229": 768, + "\u222a": 768, + "\u2283": 713, + "\u2287": 713, + "\u2284": 713, + "\u2282": 713, + "\u2286": 713, + "\u2208": 713, + "\u2209": 713, + "\u2220": 768, + "\u2207": 713, + "\uf6da": 790, + "\uf6d9": 790, + "\uf6db": 890, + "\u220f": 823, + "\u221a": 549, + "\u22c5": 250, + "\xac": 713, + "\u2227": 603, + "\u2228": 603, + "\u21d4": 1042, + "\u21d0": 987, + "\u21d1": 603, + "\u21d2": 987, + "\u21d3": 603, + "\u25ca": 494, + "\u2329": 329, + "\uf8e8": 790, + "\uf8e9": 790, + "\uf8ea": 786, + "\u2211": 713, + "\uf8eb": 384, + "\uf8ec": 384, + "\uf8ed": 384, + "\uf8ee": 384, + "\uf8ef": 384, + "\uf8f0": 384, + "\uf8f1": 494, + "\uf8f2": 494, + "\uf8f3": 494, + "\uf8f4": 494, + "\u232a": 329, + "\u222b": 274, + "\u2320": 686, + "\uf8f5": 686, + "\u2321": 686, + "\uf8f6": 384, + "\uf8f7": 384, + "\uf8f8": 384, + "\uf8f9": 384, + "\uf8fa": 384, + "\uf8fb": 384, + "\uf8fc": 494, + "\uf8fd": 494, + "\uf8fe": 494, + "\uf8ff": 790, + }, + ), + # Generated from Times-Bold.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights + # Reserved. Times is a trademark of Linotype-Hell AG and/or its subsidiaries. + "Times-Bold": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Times-Bold", + family="Times", + weight="Bold", + ascent=683, + descent=-217, + cap_height=676, + x_height=461, + italic_angle=0, + flags=34, + bbox=(-168.0, -218.0, 1000.0, 935.0), + ), + character_widths={ + " ": 250, + "default": 500, + "!": 333, + '"': 555, + "#": 500, + "$": 500, + "%": 1000, + "&": 833, + "\u2019": 333, + "(": 333, + ")": 333, + "*": 500, + "+": 570, + ",": 250, + "-": 333, + ".": 250, + "/": 278, + "0": 500, + "1": 500, + "2": 500, + "3": 500, + "4": 500, + "5": 500, + "6": 500, + "7": 500, + "8": 500, + "9": 500, + ":": 333, + ";": 333, + "<": 570, + "=": 570, + ">": 570, + "?": 500, + "@": 930, + "A": 722, + "B": 667, + "C": 722, + "D": 722, + "E": 667, + "F": 611, + "G": 778, + "H": 778, + "I": 389, + "J": 500, + "K": 778, + "L": 667, + "M": 944, + "N": 722, + "O": 778, + "P": 611, + "Q": 778, + "R": 722, + "S": 556, + "T": 667, + "U": 722, + "V": 722, + "W": 1000, + "X": 722, + "Y": 722, + "Z": 667, + "[": 333, + "\\": 278, + "]": 333, + "^": 581, + "_": 500, + "\u2018": 333, + "a": 500, + "b": 556, + "c": 444, + "d": 556, + "e": 444, + "f": 333, + "g": 500, + "h": 556, + "i": 278, + "j": 333, + "k": 556, + "l": 278, + "m": 833, + "n": 556, + "o": 500, + "p": 556, + "q": 556, + "r": 444, + "s": 389, + "t": 333, + "u": 556, + "v": 500, + "w": 722, + "x": 500, + "y": 500, + "z": 444, + "{": 394, + "|": 220, + "}": 394, + "~": 520, + "\xa1": 333, + "\xa2": 500, + "\xa3": 500, + "\u2044": 167, + "\xa5": 500, + "\u0192": 500, + "\xa7": 500, + "\xa4": 500, + "'": 278, + "\u201c": 500, + "\xab": 500, + "\u2039": 333, + "\u203a": 333, + "\ufb01": 556, + "\ufb02": 556, + "\u2013": 500, + "\u2020": 500, + "\u2021": 500, + "\xb7": 250, + "\xb6": 540, + "\u2022": 350, + "\u201a": 333, + "\u201e": 500, + "\u201d": 500, + "\xbb": 500, + "\u2026": 1000, + "\u2030": 1000, + "\xbf": 500, + "`": 333, + "\xb4": 333, + "\u02c6": 333, + "\u02dc": 333, + "\xaf": 333, + "\u02d8": 333, + "\u02d9": 333, + "\xa8": 333, + "\u02da": 333, + "\xb8": 333, + "\u02dd": 333, + "\u02db": 333, + "\u02c7": 333, + "\u2014": 1000, + "\xc6": 1000, + "\xaa": 300, + "\u0141": 667, + "\xd8": 778, + "\u0152": 1000, + "\xba": 330, + "\xe6": 722, + "\u0131": 278, + "\u0142": 278, + "\xf8": 500, + "\u0153": 722, + "\xdf": 556, + "\xcf": 389, + "\xe9": 444, + "\u0103": 500, + "\u0171": 556, + "\u011b": 444, + "\u0178": 722, + "\xf7": 570, + "\xdd": 722, + "\xc2": 722, + "\xe1": 500, + "\xdb": 722, + "\xfd": 500, + "\u0219": 389, + "\xea": 444, + "\u016e": 722, + "\xdc": 722, + "\u0105": 500, + "\xda": 722, + "\u0173": 556, + "\xcb": 667, + "\u0110": 722, + "\uf6c3": 250, + "\xa9": 747, + "\u0112": 667, + "\u010d": 444, + "\xe5": 500, + "\u0145": 722, + "\u013a": 278, + "\xe0": 500, + "\u0162": 667, + "\u0106": 722, + "\xe3": 500, + "\u0116": 667, + "\u0161": 389, + "\u015f": 389, + "\xed": 278, + "\u25ca": 494, + "\u0158": 722, + "\u0122": 778, + "\xfb": 556, + "\xe2": 500, + "\u0100": 722, + "\u0159": 444, + "\xe7": 444, + "\u017b": 667, + "\xde": 611, + "\u014c": 778, + "\u0154": 722, + "\u015a": 556, + "\u010f": 672, + "\u016a": 722, + "\u016f": 556, + "\xb3": 300, + "\xd2": 778, + "\xc0": 722, + "\u0102": 722, + "\xd7": 570, + "\xfa": 556, + "\u0164": 667, + "\u2202": 494, + "\xff": 500, + "\u0143": 722, + "\xee": 278, + "\xca": 667, + "\xe4": 500, + "\xeb": 444, + "\u0107": 444, + "\u0144": 556, + "\u016b": 556, + "\u0147": 722, + "\xcd": 389, + "\xb1": 570, + "\xa6": 220, + "\xae": 747, + "\u011e": 778, + "\u0130": 389, + "\u2211": 600, + "\xc8": 667, + "\u0155": 444, + "\u014d": 500, + "\u0179": 667, + "\u017d": 667, + "\u2265": 549, + "\xd0": 722, + "\xc7": 722, + "\u013c": 278, + "\u0165": 416, + "\u0119": 444, + "\u0172": 722, + "\xc1": 722, + "\xc4": 722, + "\xe8": 444, + "\u017a": 444, + "\u012f": 278, + "\xd3": 778, + "\xf3": 500, + "\u0101": 500, + "\u015b": 389, + "\xef": 278, + "\xd4": 778, + "\xd9": 722, + "\u2206": 612, + "\xfe": 556, + "\xb2": 300, + "\xd6": 778, + "\xb5": 556, + "\xec": 278, + "\u0151": 500, + "\u0118": 667, + "\u0111": 556, + "\xbe": 750, + "\u015e": 556, + "\u013e": 394, + "\u0136": 778, + "\u0139": 667, + "\u2122": 1000, + "\u0117": 444, + "\xcc": 389, + "\u012a": 389, + "\u013d": 667, + "\xbd": 750, + "\u2264": 549, + "\xf4": 500, + "\xf1": 556, + "\u0170": 722, + "\xc9": 667, + "\u0113": 444, + "\u011f": 500, + "\xbc": 750, + "\u0160": 556, + "\u0218": 556, + "\u0150": 778, + "\xb0": 400, + "\xf2": 500, + "\u010c": 722, + "\xf9": 556, + "\u221a": 549, + "\u010e": 722, + "\u0157": 444, + "\xd1": 722, + "\xf5": 500, + "\u0156": 722, + "\u013b": 667, + "\xc3": 722, + "\u0104": 722, + "\xc5": 722, + "\xd5": 778, + "\u017c": 444, + "\u011a": 667, + "\u012e": 389, + "\u0137": 556, + "\u2212": 570, + "\xce": 389, + "\u0148": 556, + "\u0163": 333, + "\xac": 570, + "\xf6": 500, + "\xfc": 556, + "\u2260": 549, + "\u0123": 500, + "\xf0": 500, + "\u017e": 444, + "\u0146": 556, + "\xb9": 300, + "\u012b": 278, + "\u20ac": 500, + }, + ), + # Generated from Times-BoldItalic.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights + # Reserved. Times is a trademark of Linotype-Hell AG and/or its subsidiaries. + "Times-BoldItalic": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Times-BoldItalic", + family="Times", + weight="Bold", + ascent=683, + descent=-217, + cap_height=669, + x_height=462, + italic_angle=-15, + flags=98, + bbox=(-200.0, -218.0, 996.0, 921.0), + ), + character_widths={ + " ": 250, + "default": 500, + "!": 389, + '"': 555, + "#": 500, + "$": 500, + "%": 833, + "&": 778, + "\u2019": 333, + "(": 333, + ")": 333, + "*": 500, + "+": 570, + ",": 250, + "-": 333, + ".": 250, + "/": 278, + "0": 500, + "1": 500, + "2": 500, + "3": 500, + "4": 500, + "5": 500, + "6": 500, + "7": 500, + "8": 500, + "9": 500, + ":": 333, + ";": 333, + "<": 570, + "=": 570, + ">": 570, + "?": 500, + "@": 832, + "A": 667, + "B": 667, + "C": 667, + "D": 722, + "E": 667, + "F": 667, + "G": 722, + "H": 778, + "I": 389, + "J": 500, + "K": 667, + "L": 611, + "M": 889, + "N": 722, + "O": 722, + "P": 611, + "Q": 722, + "R": 667, + "S": 556, + "T": 611, + "U": 722, + "V": 667, + "W": 889, + "X": 667, + "Y": 611, + "Z": 611, + "[": 333, + "\\": 278, + "]": 333, + "^": 570, + "_": 500, + "\u2018": 333, + "a": 500, + "b": 500, + "c": 444, + "d": 500, + "e": 444, + "f": 333, + "g": 500, + "h": 556, + "i": 278, + "j": 278, + "k": 500, + "l": 278, + "m": 778, + "n": 556, + "o": 500, + "p": 500, + "q": 500, + "r": 389, + "s": 389, + "t": 278, + "u": 556, + "v": 444, + "w": 667, + "x": 500, + "y": 444, + "z": 389, + "{": 348, + "|": 220, + "}": 348, + "~": 570, + "\xa1": 389, + "\xa2": 500, + "\xa3": 500, + "\u2044": 167, + "\xa5": 500, + "\u0192": 500, + "\xa7": 500, + "\xa4": 500, + "'": 278, + "\u201c": 500, + "\xab": 500, + "\u2039": 333, + "\u203a": 333, + "\ufb01": 556, + "\ufb02": 556, + "\u2013": 500, + "\u2020": 500, + "\u2021": 500, + "\xb7": 250, + "\xb6": 500, + "\u2022": 350, + "\u201a": 333, + "\u201e": 500, + "\u201d": 500, + "\xbb": 500, + "\u2026": 1000, + "\u2030": 1000, + "\xbf": 500, + "`": 333, + "\xb4": 333, + "\u02c6": 333, + "\u02dc": 333, + "\xaf": 333, + "\u02d8": 333, + "\u02d9": 333, + "\xa8": 333, + "\u02da": 333, + "\xb8": 333, + "\u02dd": 333, + "\u02db": 333, + "\u02c7": 333, + "\u2014": 1000, + "\xc6": 944, + "\xaa": 266, + "\u0141": 611, + "\xd8": 722, + "\u0152": 944, + "\xba": 300, + "\xe6": 722, + "\u0131": 278, + "\u0142": 278, + "\xf8": 500, + "\u0153": 722, + "\xdf": 500, + "\xcf": 389, + "\xe9": 444, + "\u0103": 500, + "\u0171": 556, + "\u011b": 444, + "\u0178": 611, + "\xf7": 570, + "\xdd": 611, + "\xc2": 667, + "\xe1": 500, + "\xdb": 722, + "\xfd": 444, + "\u0219": 389, + "\xea": 444, + "\u016e": 722, + "\xdc": 722, + "\u0105": 500, + "\xda": 722, + "\u0173": 556, + "\xcb": 667, + "\u0110": 722, + "\uf6c3": 250, + "\xa9": 747, + "\u0112": 667, + "\u010d": 444, + "\xe5": 500, + "\u0145": 722, + "\u013a": 278, + "\xe0": 500, + "\u0162": 611, + "\u0106": 667, + "\xe3": 500, + "\u0116": 667, + "\u0161": 389, + "\u015f": 389, + "\xed": 278, + "\u25ca": 494, + "\u0158": 667, + "\u0122": 722, + "\xfb": 556, + "\xe2": 500, + "\u0100": 667, + "\u0159": 389, + "\xe7": 444, + "\u017b": 611, + "\xde": 611, + "\u014c": 722, + "\u0154": 667, + "\u015a": 556, + "\u010f": 608, + "\u016a": 722, + "\u016f": 556, + "\xb3": 300, + "\xd2": 722, + "\xc0": 667, + "\u0102": 667, + "\xd7": 570, + "\xfa": 556, + "\u0164": 611, + "\u2202": 494, + "\xff": 444, + "\u0143": 722, + "\xee": 278, + "\xca": 667, + "\xe4": 500, + "\xeb": 444, + "\u0107": 444, + "\u0144": 556, + "\u016b": 556, + "\u0147": 722, + "\xcd": 389, + "\xb1": 570, + "\xa6": 220, + "\xae": 747, + "\u011e": 722, + "\u0130": 389, + "\u2211": 600, + "\xc8": 667, + "\u0155": 389, + "\u014d": 500, + "\u0179": 611, + "\u017d": 611, + "\u2265": 549, + "\xd0": 722, + "\xc7": 667, + "\u013c": 278, + "\u0165": 366, + "\u0119": 444, + "\u0172": 722, + "\xc1": 667, + "\xc4": 667, + "\xe8": 444, + "\u017a": 389, + "\u012f": 278, + "\xd3": 722, + "\xf3": 500, + "\u0101": 500, + "\u015b": 389, + "\xef": 278, + "\xd4": 722, + "\xd9": 722, + "\u2206": 612, + "\xfe": 500, + "\xb2": 300, + "\xd6": 722, + "\xb5": 576, + "\xec": 278, + "\u0151": 500, + "\u0118": 667, + "\u0111": 500, + "\xbe": 750, + "\u015e": 556, + "\u013e": 382, + "\u0136": 667, + "\u0139": 611, + "\u2122": 1000, + "\u0117": 444, + "\xcc": 389, + "\u012a": 389, + "\u013d": 611, + "\xbd": 750, + "\u2264": 549, + "\xf4": 500, + "\xf1": 556, + "\u0170": 722, + "\xc9": 667, + "\u0113": 444, + "\u011f": 500, + "\xbc": 750, + "\u0160": 556, + "\u0218": 556, + "\u0150": 722, + "\xb0": 400, + "\xf2": 500, + "\u010c": 667, + "\xf9": 556, + "\u221a": 549, + "\u010e": 722, + "\u0157": 389, + "\xd1": 722, + "\xf5": 500, + "\u0156": 667, + "\u013b": 611, + "\xc3": 667, + "\u0104": 667, + "\xc5": 667, + "\xd5": 722, + "\u017c": 389, + "\u011a": 667, + "\u012e": 389, + "\u0137": 500, + "\u2212": 606, + "\xce": 389, + "\u0148": 556, + "\u0163": 278, + "\xac": 606, + "\xf6": 500, + "\xfc": 556, + "\u2260": 549, + "\u0123": 500, + "\xf0": 500, + "\u017e": 389, + "\u0146": 556, + "\xb9": 300, + "\u012b": 278, + "\u20ac": 500, + }, + ), + # Generated from Times-Italic.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights + # Reserved. Times is a trademark of Linotype-Hell AG and/or its subsidiaries. + "Times-Italic": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Times-Italic", + family="Times", + weight="Medium", + ascent=683, + descent=-217, + cap_height=653, + x_height=441, + italic_angle=-15.5, + flags=98, + bbox=(-169.0, -217.0, 1010.0, 883.0), + ), + character_widths={ + " ": 250, + "default": 500, + "!": 333, + '"': 420, + "#": 500, + "$": 500, + "%": 833, + "&": 778, + "\u2019": 333, + "(": 333, + ")": 333, + "*": 500, + "+": 675, + ",": 250, + "-": 333, + ".": 250, + "/": 278, + "0": 500, + "1": 500, + "2": 500, + "3": 500, + "4": 500, + "5": 500, + "6": 500, + "7": 500, + "8": 500, + "9": 500, + ":": 333, + ";": 333, + "<": 675, + "=": 675, + ">": 675, + "?": 500, + "@": 920, + "A": 611, + "B": 611, + "C": 667, + "D": 722, + "E": 611, + "F": 611, + "G": 722, + "H": 722, + "I": 333, + "J": 444, + "K": 667, + "L": 556, + "M": 833, + "N": 667, + "O": 722, + "P": 611, + "Q": 722, + "R": 611, + "S": 500, + "T": 556, + "U": 722, + "V": 611, + "W": 833, + "X": 611, + "Y": 556, + "Z": 556, + "[": 389, + "\\": 278, + "]": 389, + "^": 422, + "_": 500, + "\u2018": 333, + "a": 500, + "b": 500, + "c": 444, + "d": 500, + "e": 444, + "f": 278, + "g": 500, + "h": 500, + "i": 278, + "j": 278, + "k": 444, + "l": 278, + "m": 722, + "n": 500, + "o": 500, + "p": 500, + "q": 500, + "r": 389, + "s": 389, + "t": 278, + "u": 500, + "v": 444, + "w": 667, + "x": 444, + "y": 444, + "z": 389, + "{": 400, + "|": 275, + "}": 400, + "~": 541, + "\xa1": 389, + "\xa2": 500, + "\xa3": 500, + "\u2044": 167, + "\xa5": 500, + "\u0192": 500, + "\xa7": 500, + "\xa4": 500, + "'": 214, + "\u201c": 556, + "\xab": 500, + "\u2039": 333, + "\u203a": 333, + "\ufb01": 500, + "\ufb02": 500, + "\u2013": 500, + "\u2020": 500, + "\u2021": 500, + "\xb7": 250, + "\xb6": 523, + "\u2022": 350, + "\u201a": 333, + "\u201e": 556, + "\u201d": 556, + "\xbb": 500, + "\u2026": 889, + "\u2030": 1000, + "\xbf": 500, + "`": 333, + "\xb4": 333, + "\u02c6": 333, + "\u02dc": 333, + "\xaf": 333, + "\u02d8": 333, + "\u02d9": 333, + "\xa8": 333, + "\u02da": 333, + "\xb8": 333, + "\u02dd": 333, + "\u02db": 333, + "\u02c7": 333, + "\u2014": 889, + "\xc6": 889, + "\xaa": 276, + "\u0141": 556, + "\xd8": 722, + "\u0152": 944, + "\xba": 310, + "\xe6": 667, + "\u0131": 278, + "\u0142": 278, + "\xf8": 500, + "\u0153": 667, + "\xdf": 500, + "\xcf": 333, + "\xe9": 444, + "\u0103": 500, + "\u0171": 500, + "\u011b": 444, + "\u0178": 556, + "\xf7": 675, + "\xdd": 556, + "\xc2": 611, + "\xe1": 500, + "\xdb": 722, + "\xfd": 444, + "\u0219": 389, + "\xea": 444, + "\u016e": 722, + "\xdc": 722, + "\u0105": 500, + "\xda": 722, + "\u0173": 500, + "\xcb": 611, + "\u0110": 722, + "\uf6c3": 250, + "\xa9": 760, + "\u0112": 611, + "\u010d": 444, + "\xe5": 500, + "\u0145": 667, + "\u013a": 278, + "\xe0": 500, + "\u0162": 556, + "\u0106": 667, + "\xe3": 500, + "\u0116": 611, + "\u0161": 389, + "\u015f": 389, + "\xed": 278, + "\u25ca": 471, + "\u0158": 611, + "\u0122": 722, + "\xfb": 500, + "\xe2": 500, + "\u0100": 611, + "\u0159": 389, + "\xe7": 444, + "\u017b": 556, + "\xde": 611, + "\u014c": 722, + "\u0154": 611, + "\u015a": 500, + "\u010f": 544, + "\u016a": 722, + "\u016f": 500, + "\xb3": 300, + "\xd2": 722, + "\xc0": 611, + "\u0102": 611, + "\xd7": 675, + "\xfa": 500, + "\u0164": 556, + "\u2202": 476, + "\xff": 444, + "\u0143": 667, + "\xee": 278, + "\xca": 611, + "\xe4": 500, + "\xeb": 444, + "\u0107": 444, + "\u0144": 500, + "\u016b": 500, + "\u0147": 667, + "\xcd": 333, + "\xb1": 675, + "\xa6": 275, + "\xae": 760, + "\u011e": 722, + "\u0130": 333, + "\u2211": 600, + "\xc8": 611, + "\u0155": 389, + "\u014d": 500, + "\u0179": 556, + "\u017d": 556, + "\u2265": 549, + "\xd0": 722, + "\xc7": 667, + "\u013c": 278, + "\u0165": 300, + "\u0119": 444, + "\u0172": 722, + "\xc1": 611, + "\xc4": 611, + "\xe8": 444, + "\u017a": 389, + "\u012f": 278, + "\xd3": 722, + "\xf3": 500, + "\u0101": 500, + "\u015b": 389, + "\xef": 278, + "\xd4": 722, + "\xd9": 722, + "\u2206": 612, + "\xfe": 500, + "\xb2": 300, + "\xd6": 722, + "\xb5": 500, + "\xec": 278, + "\u0151": 500, + "\u0118": 611, + "\u0111": 500, + "\xbe": 750, + "\u015e": 500, + "\u013e": 300, + "\u0136": 667, + "\u0139": 556, + "\u2122": 980, + "\u0117": 444, + "\xcc": 333, + "\u012a": 333, + "\u013d": 611, + "\xbd": 750, + "\u2264": 549, + "\xf4": 500, + "\xf1": 500, + "\u0170": 722, + "\xc9": 611, + "\u0113": 444, + "\u011f": 500, + "\xbc": 750, + "\u0160": 500, + "\u0218": 500, + "\u0150": 722, + "\xb0": 400, + "\xf2": 500, + "\u010c": 667, + "\xf9": 500, + "\u221a": 453, + "\u010e": 722, + "\u0157": 389, + "\xd1": 667, + "\xf5": 500, + "\u0156": 611, + "\u013b": 556, + "\xc3": 611, + "\u0104": 611, + "\xc5": 611, + "\xd5": 722, + "\u017c": 389, + "\u011a": 611, + "\u012e": 333, + "\u0137": 444, + "\u2212": 675, + "\xce": 333, + "\u0148": 500, + "\u0163": 278, + "\xac": 675, + "\xf6": 500, + "\xfc": 500, + "\u2260": 549, + "\u0123": 500, + "\xf0": 500, + "\u017e": 389, + "\u0146": 500, + "\xb9": 300, + "\u012b": 278, + "\u20ac": 500, + }, + ), + # Generated from Times-Roman.afm + # Copyright (c) 1985, 1987, 1989, 1990, 1993, 1997 Adobe Systems Incorporated. All Rights + # Reserved. Times is a trademark of Linotype-Hell AG and/or its subsidiaries. + "Times-Roman": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="Times-Roman", + family="Times", + weight="Roman", + ascent=683, + descent=-217, + cap_height=662, + x_height=450, + italic_angle=0, + flags=34, + bbox=(-168.0, -218.0, 1000.0, 898.0), + ), + character_widths={ + " ": 250, + "default": 500, + "!": 333, + '"': 408, + "#": 500, + "$": 500, + "%": 833, + "&": 778, + "\u2019": 333, + "(": 333, + ")": 333, + "*": 500, + "+": 564, + ",": 250, + "-": 333, + ".": 250, + "/": 278, + "0": 500, + "1": 500, + "2": 500, + "3": 500, + "4": 500, + "5": 500, + "6": 500, + "7": 500, + "8": 500, + "9": 500, + ":": 278, + ";": 278, + "<": 564, + "=": 564, + ">": 564, + "?": 444, + "@": 921, + "A": 722, + "B": 667, + "C": 667, + "D": 722, + "E": 611, + "F": 556, + "G": 722, + "H": 722, + "I": 333, + "J": 389, + "K": 722, + "L": 611, + "M": 889, + "N": 722, + "O": 722, + "P": 556, + "Q": 722, + "R": 667, + "S": 556, + "T": 611, + "U": 722, + "V": 722, + "W": 944, + "X": 722, + "Y": 722, + "Z": 611, + "[": 333, + "\\": 278, + "]": 333, + "^": 469, + "_": 500, + "\u2018": 333, + "a": 444, + "b": 500, + "c": 444, + "d": 500, + "e": 444, + "f": 333, + "g": 500, + "h": 500, + "i": 278, + "j": 278, + "k": 500, + "l": 278, + "m": 778, + "n": 500, + "o": 500, + "p": 500, + "q": 500, + "r": 333, + "s": 389, + "t": 278, + "u": 500, + "v": 500, + "w": 722, + "x": 500, + "y": 500, + "z": 444, + "{": 480, + "|": 200, + "}": 480, + "~": 541, + "\xa1": 333, + "\xa2": 500, + "\xa3": 500, + "\u2044": 167, + "\xa5": 500, + "\u0192": 500, + "\xa7": 500, + "\xa4": 500, + "'": 180, + "\u201c": 444, + "\xab": 500, + "\u2039": 333, + "\u203a": 333, + "\ufb01": 556, + "\ufb02": 556, + "\u2013": 500, + "\u2020": 500, + "\u2021": 500, + "\xb7": 250, + "\xb6": 453, + "\u2022": 350, + "\u201a": 333, + "\u201e": 444, + "\u201d": 444, + "\xbb": 500, + "\u2026": 1000, + "\u2030": 1000, + "\xbf": 444, + "`": 333, + "\xb4": 333, + "\u02c6": 333, + "\u02dc": 333, + "\xaf": 333, + "\u02d8": 333, + "\u02d9": 333, + "\xa8": 333, + "\u02da": 333, + "\xb8": 333, + "\u02dd": 333, + "\u02db": 333, + "\u02c7": 333, + "\u2014": 1000, + "\xc6": 889, + "\xaa": 276, + "\u0141": 611, + "\xd8": 722, + "\u0152": 889, + "\xba": 310, + "\xe6": 667, + "\u0131": 278, + "\u0142": 278, + "\xf8": 500, + "\u0153": 722, + "\xdf": 500, + "\xcf": 333, + "\xe9": 444, + "\u0103": 444, + "\u0171": 500, + "\u011b": 444, + "\u0178": 722, + "\xf7": 564, + "\xdd": 722, + "\xc2": 722, + "\xe1": 444, + "\xdb": 722, + "\xfd": 500, + "\u0219": 389, + "\xea": 444, + "\u016e": 722, + "\xdc": 722, + "\u0105": 444, + "\xda": 722, + "\u0173": 500, + "\xcb": 611, + "\u0110": 722, + "\uf6c3": 250, + "\xa9": 760, + "\u0112": 611, + "\u010d": 444, + "\xe5": 444, + "\u0145": 722, + "\u013a": 278, + "\xe0": 444, + "\u0162": 611, + "\u0106": 667, + "\xe3": 444, + "\u0116": 611, + "\u0161": 389, + "\u015f": 389, + "\xed": 278, + "\u25ca": 471, + "\u0158": 667, + "\u0122": 722, + "\xfb": 500, + "\xe2": 444, + "\u0100": 722, + "\u0159": 333, + "\xe7": 444, + "\u017b": 611, + "\xde": 556, + "\u014c": 722, + "\u0154": 667, + "\u015a": 556, + "\u010f": 588, + "\u016a": 722, + "\u016f": 500, + "\xb3": 300, + "\xd2": 722, + "\xc0": 722, + "\u0102": 722, + "\xd7": 564, + "\xfa": 500, + "\u0164": 611, + "\u2202": 476, + "\xff": 500, + "\u0143": 722, + "\xee": 278, + "\xca": 611, + "\xe4": 444, + "\xeb": 444, + "\u0107": 444, + "\u0144": 500, + "\u016b": 500, + "\u0147": 722, + "\xcd": 333, + "\xb1": 564, + "\xa6": 200, + "\xae": 760, + "\u011e": 722, + "\u0130": 333, + "\u2211": 600, + "\xc8": 611, + "\u0155": 333, + "\u014d": 500, + "\u0179": 611, + "\u017d": 611, + "\u2265": 549, + "\xd0": 722, + "\xc7": 667, + "\u013c": 278, + "\u0165": 326, + "\u0119": 444, + "\u0172": 722, + "\xc1": 722, + "\xc4": 722, + "\xe8": 444, + "\u017a": 444, + "\u012f": 278, + "\xd3": 722, + "\xf3": 500, + "\u0101": 444, + "\u015b": 389, + "\xef": 278, + "\xd4": 722, + "\xd9": 722, + "\u2206": 612, + "\xfe": 500, + "\xb2": 300, + "\xd6": 722, + "\xb5": 500, + "\xec": 278, + "\u0151": 500, + "\u0118": 611, + "\u0111": 500, + "\xbe": 750, + "\u015e": 556, + "\u013e": 344, + "\u0136": 722, + "\u0139": 611, + "\u2122": 980, + "\u0117": 444, + "\xcc": 333, + "\u012a": 333, + "\u013d": 611, + "\xbd": 750, + "\u2264": 549, + "\xf4": 500, + "\xf1": 500, + "\u0170": 722, + "\xc9": 611, + "\u0113": 444, + "\u011f": 500, + "\xbc": 750, + "\u0160": 556, + "\u0218": 556, + "\u0150": 722, + "\xb0": 400, + "\xf2": 500, + "\u010c": 667, + "\xf9": 500, + "\u221a": 453, + "\u010e": 722, + "\u0157": 333, + "\xd1": 722, + "\xf5": 500, + "\u0156": 667, + "\u013b": 611, + "\xc3": 722, + "\u0104": 722, + "\xc5": 722, + "\xd5": 722, + "\u017c": 444, + "\u011a": 611, + "\u012e": 333, + "\u0137": 500, + "\u2212": 564, + "\xce": 333, + "\u0148": 500, + "\u0163": 278, + "\xac": 564, + "\xf6": 500, + "\xfc": 500, + "\u2260": 549, + "\u0123": 500, + "\xf0": 500, + "\u017e": 444, + "\u0146": 500, + "\xb9": 300, + "\u012b": 278, + "\u20ac": 500, + }, + ), + # Generated from ZapfDingbats.afm + # Copyright (c) 1985, 1987, 1988, 1989, 1997 Adobe Systems Incorporated. All Rights Reserved. + # ITC Zapf Dingbats is a registered trademark of International Typeface Corporation. + "ZapfDingbats": CoreFontMetrics( + font_descriptor=FontDescriptor( + name="ZapfDingbats", + family="ZapfDingbats", + weight="Medium", + ascent=0.0, + descent=0.0, + cap_height=0.0, + x_height=0.0, + italic_angle=0, + flags=4, + bbox=(-1.0, -143.0, 981.0, 820.0), + ), + character_widths={ + " ": 790, + "default": 1580, + "\x01": 974, + "\x02": 961, + "\xca": 974, + "\x03": 980, + "\x04": 719, + "\x05": 789, + "w": 790, + "v": 791, + "u": 690, + "\x0b": 960, + "\x0c": 939, + "\r": 549, + "\x0e": 855, + "\x0f": 911, + "\x10": 933, + "i": 911, + "\x11": 945, + "\x12": 974, + "\x13": 755, + "\x14": 846, + "\x15": 762, + "\x16": 761, + "\x17": 571, + "\x18": 677, + "\x19": 763, + "\x1a": 760, + "\x1b": 759, + "\x1c": 754, + "\x06": 494, + "\x07": 552, + "\x08": 537, + "\t": 577, + "\n": 692, + "\x1d": 786, + "\x1e": 788, + "\x1f": 788, + "!": 793, + '"': 794, + "#": 816, + "$": 823, + "%": 789, + "&": 841, + "'": 823, + "(": 833, + ")": 816, + "*": 831, + "+": 923, + ",": 744, + "-": 723, + ".": 749, + "/": 790, + "0": 792, + "1": 695, + "2": 776, + "3": 768, + "4": 792, + "5": 759, + "6": 707, + "7": 708, + "8": 682, + "9": 701, + ":": 826, + ";": 815, + "<": 789, + "=": 789, + ">": 707, + "?": 687, + "@": 696, + "A": 689, + "B": 786, + "C": 787, + "D": 713, + "E": 791, + "F": 785, + "G": 791, + "H": 873, + "I": 761, + "J": 762, + "\xcb": 762, + "K": 759, + "\xcc": 759, + "L": 892, + "M": 892, + "N": 788, + "O": 784, + "Q": 438, + "R": 138, + "S": 277, + "T": 415, + "a": 392, + "b": 392, + "c": 668, + "d": 668, + "Y": 390, + "Z": 390, + "]": 317, + "^": 317, + "[": 276, + "\\": 276, + "\xcd": 509, + "U": 509, + "\xce": 410, + "V": 410, + "W": 234, + "X": 234, + "_": 334, + "`": 334, + "e": 732, + "f": 544, + "g": 544, + "h": 910, + "j": 667, + "k": 760, + "l": 760, + "p": 776, + "o": 595, + "n": 694, + "m": 626, + "x": 788, + "y": 788, + "z": 788, + "{": 788, + "|": 788, + "}": 788, + "~": 788, + "\x7f": 788, + "\x80": 788, + "\x81": 788, + "\x82": 788, + "\x83": 788, + "\x84": 788, + "\x85": 788, + "\x86": 788, + "\x87": 788, + "\x88": 788, + "\x89": 788, + "\x8a": 788, + "\x8b": 788, + "\x8c": 788, + "\x8d": 788, + "\x8e": 788, + "\x8f": 788, + "\x90": 788, + "\x91": 788, + "\x92": 788, + "\x93": 788, + "\x94": 788, + "\x95": 788, + "\x96": 788, + "\x97": 788, + "\x98": 788, + "\x99": 788, + "\x9a": 788, + "\x9b": 788, + "\x9c": 788, + "\x9d": 788, + "\x9e": 788, + "\x9f": 788, + "\xa0": 894, + "\xa1": 838, + "\xa3": 1016, + "\xa4": 458, + "\xc4": 748, + "\xa5": 924, + "\xc0": 748, + "\xa6": 918, + "\xa7": 927, + "\xa8": 928, + "\xa9": 928, + "\xaa": 834, + "\xab": 873, + "\xac": 828, + "\xad": 924, + "\xa2": 924, + "\xae": 917, + "\xaf": 930, + "\xb0": 931, + "\xb1": 463, + "\xb2": 883, + "\xb3": 836, + "\xc1": 836, + "\xb4": 867, + "\xc7": 867, + "\xb5": 696, + "\xc8": 696, + "\xb6": 874, + "\xc9": 874, + "\xb7": 760, + "\xb8": 946, + "\xc5": 771, + "\xb9": 865, + "\xc2": 771, + "\xc6": 888, + "\xba": 967, + "\xc3": 888, + "\xbb": 831, + "\xbc": 873, + "\xbd": 927, + "\xbe": 970, + "\xbf": 918, + }, + ), +} + + +# Add aliases per table H.3 on pp. 1109-1110 of the PDF 1.7 reference +CORE_FONT_METRICS["Arial"] = CORE_FONT_METRICS["Helvetica"] +CORE_FONT_METRICS["Arial,Italic"] = CORE_FONT_METRICS["Helvetica-Oblique"] +CORE_FONT_METRICS["Arial,Bold"] = CORE_FONT_METRICS["Helvetica-Bold"] +CORE_FONT_METRICS["Arial,BoldItalic"] = CORE_FONT_METRICS["Helvetica-BoldOblique"] +CORE_FONT_METRICS["CourierNew"] = CORE_FONT_METRICS["Courier"] +CORE_FONT_METRICS["CourierNew,Italic"] = CORE_FONT_METRICS["Courier-Oblique"] +CORE_FONT_METRICS["CourierNew,Bold"] = CORE_FONT_METRICS["Courier-Bold"] +CORE_FONT_METRICS["CourierNew,BoldItalic"] = CORE_FONT_METRICS["Courier-BoldOblique"] +CORE_FONT_METRICS["TimesNewRoman"] = CORE_FONT_METRICS["Times-Roman"] +CORE_FONT_METRICS["TimesNewRoman,Italic"] = CORE_FONT_METRICS["Times-Italic"] +CORE_FONT_METRICS["TimesNewRoman,Bold"] = CORE_FONT_METRICS["Times-Bold"] +CORE_FONT_METRICS["TimesNewRoman,BoldItalic"] = CORE_FONT_METRICS["Times-BoldItalic"] diff --git a/app/vendor/pypdf/_codecs/pdfdoc.py b/app/vendor/pypdf/_codecs/pdfdoc.py new file mode 100644 index 0000000..306357a --- /dev/null +++ b/app/vendor/pypdf/_codecs/pdfdoc.py @@ -0,0 +1,264 @@ +# PDFDocEncoding Character Set: Table D.2 of PDF Reference 1.7 +# C.1 Predefined encodings sorted by character name of another PDF reference +# Some indices have '\u0000' although they should have something else: +# 22: should be '\u0017' +_pdfdoc_encoding = [ + "\u0000", + "\u0001", + "\u0002", + "\u0003", + "\u0004", + "\u0005", + "\u0006", + "\u0007", # 0 - 7 + "\u0008", + "\u0009", + "\u000a", + "\u000b", + "\u000c", + "\u000d", + "\u000e", + "\u000f", # 8 - 15 + "\u0010", + "\u0011", + "\u0012", + "\u0013", + "\u0014", + "\u0015", + "\u0000", + "\u0017", # 16 - 23 + "\u02d8", + "\u02c7", + "\u02c6", + "\u02d9", + "\u02dd", + "\u02db", + "\u02da", + "\u02dc", # 24 - 31 + "\u0020", + "\u0021", + "\u0022", + "\u0023", + "\u0024", + "\u0025", + "\u0026", + "\u0027", # 32 - 39 + "\u0028", + "\u0029", + "\u002a", + "\u002b", + "\u002c", + "\u002d", + "\u002e", + "\u002f", # 40 - 47 + "\u0030", + "\u0031", + "\u0032", + "\u0033", + "\u0034", + "\u0035", + "\u0036", + "\u0037", # 48 - 55 + "\u0038", + "\u0039", + "\u003a", + "\u003b", + "\u003c", + "\u003d", + "\u003e", + "\u003f", # 56 - 63 + "\u0040", + "\u0041", + "\u0042", + "\u0043", + "\u0044", + "\u0045", + "\u0046", + "\u0047", # 64 - 71 + "\u0048", + "\u0049", + "\u004a", + "\u004b", + "\u004c", + "\u004d", + "\u004e", + "\u004f", # 72 - 79 + "\u0050", + "\u0051", + "\u0052", + "\u0053", + "\u0054", + "\u0055", + "\u0056", + "\u0057", # 80 - 87 + "\u0058", + "\u0059", + "\u005a", + "\u005b", + "\u005c", + "\u005d", + "\u005e", + "\u005f", # 88 - 95 + "\u0060", + "\u0061", + "\u0062", + "\u0063", + "\u0064", + "\u0065", + "\u0066", + "\u0067", # 96 - 103 + "\u0068", + "\u0069", + "\u006a", + "\u006b", + "\u006c", + "\u006d", + "\u006e", + "\u006f", # 104 - 111 + "\u0070", + "\u0071", + "\u0072", + "\u0073", + "\u0074", + "\u0075", + "\u0076", + "\u0077", # 112 - 119 + "\u0078", + "\u0079", + "\u007a", + "\u007b", + "\u007c", + "\u007d", + "\u007e", + "\u0000", # 120 - 127 + "\u2022", + "\u2020", + "\u2021", + "\u2026", + "\u2014", + "\u2013", + "\u0192", + "\u2044", # 128 - 135 + "\u2039", + "\u203a", + "\u2212", + "\u2030", + "\u201e", + "\u201c", + "\u201d", + "\u2018", # 136 - 143 + "\u2019", + "\u201a", + "\u2122", + "\ufb01", + "\ufb02", + "\u0141", + "\u0152", + "\u0160", # 144 - 151 + "\u0178", + "\u017d", + "\u0131", + "\u0142", + "\u0153", + "\u0161", + "\u017e", + "\u0000", # 152 - 159 + "\u20ac", + "\u00a1", + "\u00a2", + "\u00a3", + "\u00a4", + "\u00a5", + "\u00a6", + "\u00a7", # 160 - 167 + "\u00a8", + "\u00a9", + "\u00aa", + "\u00ab", + "\u00ac", + "\u0000", + "\u00ae", + "\u00af", # 168 - 175 + "\u00b0", + "\u00b1", + "\u00b2", + "\u00b3", + "\u00b4", + "\u00b5", + "\u00b6", + "\u00b7", # 176 - 183 + "\u00b8", + "\u00b9", + "\u00ba", + "\u00bb", + "\u00bc", + "\u00bd", + "\u00be", + "\u00bf", # 184 - 191 + "\u00c0", + "\u00c1", + "\u00c2", + "\u00c3", + "\u00c4", + "\u00c5", + "\u00c6", + "\u00c7", # 192 - 199 + "\u00c8", + "\u00c9", + "\u00ca", + "\u00cb", + "\u00cc", + "\u00cd", + "\u00ce", + "\u00cf", # 200 - 207 + "\u00d0", + "\u00d1", + "\u00d2", + "\u00d3", + "\u00d4", + "\u00d5", + "\u00d6", + "\u00d7", # 208 - 215 + "\u00d8", + "\u00d9", + "\u00da", + "\u00db", + "\u00dc", + "\u00dd", + "\u00de", + "\u00df", # 216 - 223 + "\u00e0", + "\u00e1", + "\u00e2", + "\u00e3", + "\u00e4", + "\u00e5", + "\u00e6", + "\u00e7", # 224 - 231 + "\u00e8", + "\u00e9", + "\u00ea", + "\u00eb", + "\u00ec", + "\u00ed", + "\u00ee", + "\u00ef", # 232 - 239 + "\u00f0", + "\u00f1", + "\u00f2", + "\u00f3", + "\u00f4", + "\u00f5", + "\u00f6", + "\u00f7", # 240 - 247 + "\u00f8", + "\u00f9", + "\u00fa", + "\u00fb", + "\u00fc", + "\u00fd", + "\u00fe", + "\u00ff", # 248 - 255 +] + +assert len(_pdfdoc_encoding) == 256 diff --git a/app/vendor/pypdf/_codecs/std.py b/app/vendor/pypdf/_codecs/std.py new file mode 100644 index 0000000..a6057ff --- /dev/null +++ b/app/vendor/pypdf/_codecs/std.py @@ -0,0 +1,258 @@ +_std_encoding = [ + "\x00", + "\x01", + "\x02", + "\x03", + "\x04", + "\x05", + "\x06", + "\x07", + "\x08", + "\t", + "\n", + "\x0b", + "\x0c", + "\r", + "\x0e", + "\x0f", + "\x10", + "\x11", + "\x12", + "\x13", + "\x14", + "\x15", + "\x16", + "\x17", + "\x18", + "\x19", + "\x1a", + "\x1b", + "\x1c", + "\x1d", + "\x1e", + "\x1f", + " ", + "!", + '"', + "#", + "$", + "%", + "&", + "’", + "(", + ")", + "*", + "+", + ",", + "-", + ".", + "/", + "0", + "1", + "2", + "3", + "4", + "5", + "6", + "7", + "8", + "9", + ":", + ";", + "<", + "=", + ">", + "?", + "@", + "A", + "B", + "C", + "D", + "E", + "F", + "G", + "H", + "I", + "J", + "K", + "L", + "M", + "N", + "O", + "P", + "Q", + "R", + "S", + "T", + "U", + "V", + "W", + "X", + "Y", + "Z", + "[", + "\\", + "]", + "^", + "_", + "‘", + "a", + "b", + "c", + "d", + "e", + "f", + "g", + "h", + "i", + "j", + "k", + "l", + "m", + "n", + "o", + "p", + "q", + "r", + "s", + "t", + "u", + "v", + "w", + "x", + "y", + "z", + "{", + "|", + "}", + "~", + "\x7f", + "\x80", + "\x81", + "\x82", + "\x83", + "\x84", + "\x85", + "\x86", + "\x87", + "\x88", + "\x89", + "\x8a", + "\x8b", + "\x8c", + "\x8d", + "\x8e", + "\x8f", + "\x90", + "\x91", + "\x92", + "\x93", + "\x94", + "\x95", + "\x96", + "\x97", + "\x98", + "\x99", + "\x9a", + "\x9b", + "\x9c", + "\x9d", + "\x9e", + "\x9f", + "\xa0", + "¡", + "¢", + "£", + "⁄", + "¥", + "ƒ", + "§", + "¤", + "'", + "“", + "«", + "‹", + "›", + "fi", + "fl", + "°", + "–", + "†", + "‡", + "·", + "µ", + "¶", + "•", + "‚", + "„", + "”", + "»", + "…", + "‰", + "¾", + "¿", + "À", + "`", + "´", + "ˆ", + "˜", + "¯", + "˘", + "˙", + "¨", + "É", + "˚", + "¸", + "Ì", + "˝", + "˛", + "ˇ", + "—", + "Ñ", + "Ò", + "Ó", + "Ô", + "Õ", + "Ö", + "×", + "Ø", + "Ù", + "Ú", + "Û", + "Ü", + "Ý", + "Þ", + "ß", + "à", + "Æ", + "â", + "ª", + "ä", + "å", + "æ", + "ç", + "Ł", + "Ø", + "Œ", + "º", + "ì", + "í", + "î", + "ï", + "ð", + "æ", + "ò", + "ó", + "ô", + "ı", + "ö", + "÷", + "ł", + "ø", + "œ", + "ß", + "ü", + "ý", + "þ", + "ÿ", +] diff --git a/app/vendor/pypdf/_codecs/symbol.py b/app/vendor/pypdf/_codecs/symbol.py new file mode 100644 index 0000000..4be4b2f --- /dev/null +++ b/app/vendor/pypdf/_codecs/symbol.py @@ -0,0 +1,281 @@ +# Generated from https://www.unicode.org/Public/MAPPINGS/VENDORS/ADOBE/symbol.txt +# +# Name: Adobe Symbol Encoding to Unicode +# Unicode version: 2.0 +# Table version: 1.0 +# Date: 2011 July 12 +# +# Copyright (c) 1991-2011 Unicode, Inc. All Rights reserved. +# +# This file is provided as-is by Unicode, Inc. (The Unicode Consortium). No +# claims are made as to fitness for any particular purpose. No warranties of +# any kind are expressed or implied. The recipient agrees to determine +# applicability of information provided. If this file has been provided on +# magnetic media by Unicode, Inc., the sole remedy for any claim will be +# exchange of defective media within 90 days of receipt. +# +# Unicode, Inc. hereby grants the right to freely use the information +# supplied in this file in the creation of products supporting the +# Unicode Standard, and to make copies of this file in any form for +# internal or external distribution as long as this notice remains +# attached. + +_symbol_encoding = [ + "\u0000", + "\u0001", + "\u0002", + "\u0003", + "\u0004", + "\u0005", + "\u0006", + "\u0007", + "\u0008", + "\u0009", + "\u000A", + "\u000B", + "\u000C", + "\u000D", + "\u000E", + "\u000F", + "\u0010", + "\u0011", + "\u0012", + "\u0013", + "\u0014", + "\u0015", + "\u0016", + "\u0017", + "\u0018", + "\u0019", + "\u001A", + "\u001B", + "\u001C", + "\u001D", + "\u001E", + "\u001F", + "\u00A0", + "\u0021", + "\u2200", + "\u0023", + "\u2203", + "\u0025", + "\u0026", + "\u220B", + "\u0028", + "\u0029", + "\u2217", + "\u002B", + "\u002C", + "\u2212", + "\u002E", + "\u002F", + "\u0030", + "\u0031", + "\u0032", + "\u0033", + "\u0034", + "\u0035", + "\u0036", + "\u0037", + "\u0038", + "\u0039", + "\u003A", + "\u003B", + "\u003C", + "\u003D", + "\u003E", + "\u003F", + "\u2245", + "\u0391", + "\u0392", + "\u03A7", + "\u2206", + "\u0395", + "\u03A6", + "\u0393", + "\u0397", + "\u0399", + "\u03D1", + "\u039A", + "\u039B", + "\u039C", + "\u039D", + "\u039F", + "\u03A0", + "\u0398", + "\u03A1", + "\u03A3", + "\u03A4", + "\u03A5", + "\u03C2", + "\u2126", + "\u039E", + "\u03A8", + "\u0396", + "\u005B", + "\u2234", + "\u005D", + "\u22A5", + "\u005F", + "\uF8E5", + "\u03B1", + "\u03B2", + "\u03C7", + "\u03B4", + "\u03B5", + "\u03C6", + "\u03B3", + "\u03B7", + "\u03B9", + "\u03D5", + "\u03BA", + "\u03BB", + "\u03BC", + "\u03BD", + "\u03BF", + "\u03C0", + "\u03B8", + "\u03C1", + "\u03C3", + "\u03C4", + "\u03C5", + "\u03D6", + "\u03C9", + "\u03BE", + "\u03C8", + "\u03B6", + "\u007B", + "\u007C", + "\u007D", + "\u223C", + "\u007F", + "\u0080", + "\u0081", + "\u0082", + "\u0083", + "\u0084", + "\u0085", + "\u0086", + "\u0087", + "\u0088", + "\u0089", + "\u008A", + "\u008B", + "\u008C", + "\u008D", + "\u008E", + "\u008F", + "\u0090", + "\u0091", + "\u0092", + "\u0093", + "\u0094", + "\u0095", + "\u0096", + "\u0097", + "\u0098", + "\u0099", + "\u009A", + "\u009B", + "\u009C", + "\u009D", + "\u009E", + "\u009F", + "\u20AC", + "\u03D2", + "\u2032", + "\u2264", + "\u2215", + "\u221E", + "\u0192", + "\u2663", + "\u2666", + "\u2665", + "\u2660", + "\u2194", + "\u2190", + "\u2191", + "\u2192", + "\u2193", + "\u00B0", + "\u00B1", + "\u2033", + "\u2265", + "\u00D7", + "\u221D", + "\u2202", + "\u2022", + "\u00F7", + "\u2260", + "\u2261", + "\u2248", + "\u2026", + "\uF8E6", + "\uF8E7", + "\u21B5", + "\u2135", + "\u2111", + "\u211C", + "\u2118", + "\u2297", + "\u2295", + "\u2205", + "\u2229", + "\u222A", + "\u2283", + "\u2287", + "\u2284", + "\u2282", + "\u2286", + "\u2208", + "\u2209", + "\u2220", + "\u2207", + "\uF6DA", + "\uF6D9", + "\uF6DB", + "\u220F", + "\u221A", + "\u22C5", + "\u00AC", + "\u2227", + "\u2228", + "\u21D4", + "\u21D0", + "\u21D1", + "\u21D2", + "\u21D3", + "\u25CA", + "\u2329", + "\uF8E8", + "\uF8E9", + "\uF8EA", + "\u2211", + "\uF8EB", + "\uF8EC", + "\uF8ED", + "\uF8EE", + "\uF8EF", + "\uF8F0", + "\uF8F1", + "\uF8F2", + "\uF8F3", + "\uF8F4", + "\u00F0", + "\u232A", + "\u222B", + "\u2320", + "\uF8F5", + "\u2321", + "\uF8F6", + "\uF8F7", + "\uF8F8", + "\uF8F9", + "\uF8FA", + "\uF8FB", + "\uF8FC", + "\uF8FD", + "\uF8FE", + "\u00FF", +] +assert len(_symbol_encoding) == 256 diff --git a/app/vendor/pypdf/_codecs/zapfding.py b/app/vendor/pypdf/_codecs/zapfding.py new file mode 100644 index 0000000..4ee981d --- /dev/null +++ b/app/vendor/pypdf/_codecs/zapfding.py @@ -0,0 +1,281 @@ +# Generated from https://www.unicode.org/Public/MAPPINGS/VENDORS/ADOBE/zdingbat.txt +# +# Name: Adobe Zapf Dingbats Encoding to Unicode +# Unicode version: 2.0 +# Table version: 1.0 +# Date: 2011 July 12 +# +# Copyright (c) 1991-2011 Unicode, Inc. All Rights reserved. +# +# This file is provided as-is by Unicode, Inc. (The Unicode Consortium). No +# claims are made as to fitness for any particular purpose. No warranties of +# any kind are expressed or implied. The recipient agrees to determine +# applicability of information provided. If this file has been provided on +# magnetic media by Unicode, Inc., the sole remedy for any claim will be +# exchange of defective media within 90 days of receipt. +# +# Unicode, Inc. hereby grants the right to freely use the information +# supplied in this file in the creation of products supporting the +# Unicode Standard, and to make copies of this file in any form for +# internal or external distribution as long as this notice remains +# attached. + +_zapfding_encoding = [ + "\u0000", + "\u0001", + "\u0002", + "\u0003", + "\u0004", + "\u0005", + "\u0006", + "\u0007", + "\u0008", + "\u0009", + "\u000A", + "\u000B", + "\u000C", + "\u000D", + "\u000E", + "\u000F", + "\u0010", + "\u0011", + "\u0012", + "\u0013", + "\u0014", + "\u0015", + "\u0016", + "\u0017", + "\u0018", + "\u0019", + "\u001A", + "\u001B", + "\u001C", + "\u001D", + "\u001E", + "\u001F", + "\u00A0", + "\u2701", + "\u2702", + "\u2703", + "\u2704", + "\u260E", + "\u2706", + "\u2707", + "\u2708", + "\u2709", + "\u261B", + "\u261E", + "\u270C", + "\u270D", + "\u270E", + "\u270F", + "\u2710", + "\u2711", + "\u2712", + "\u2713", + "\u2714", + "\u2715", + "\u2716", + "\u2717", + "\u2718", + "\u2719", + "\u271A", + "\u271B", + "\u271C", + "\u271D", + "\u271E", + "\u271F", + "\u2720", + "\u2721", + "\u2722", + "\u2723", + "\u2724", + "\u2725", + "\u2726", + "\u2727", + "\u2605", + "\u2729", + "\u272A", + "\u272B", + "\u272C", + "\u272D", + "\u272E", + "\u272F", + "\u2730", + "\u2731", + "\u2732", + "\u2733", + "\u2734", + "\u2735", + "\u2736", + "\u2737", + "\u2738", + "\u2739", + "\u273A", + "\u273B", + "\u273C", + "\u273D", + "\u273E", + "\u273F", + "\u2740", + "\u2741", + "\u2742", + "\u2743", + "\u2744", + "\u2745", + "\u2746", + "\u2747", + "\u2748", + "\u2749", + "\u274A", + "\u274B", + "\u25CF", + "\u274D", + "\u25A0", + "\u274F", + "\u2750", + "\u2751", + "\u2752", + "\u25B2", + "\u25BC", + "\u25C6", + "\u2756", + "\u25D7", + "\u2758", + "\u2759", + "\u275A", + "\u275B", + "\u275C", + "\u275D", + "\u275E", + "\u007F", + "\uF8D7", + "\uF8D8", + "\uF8D9", + "\uF8DA", + "\uF8DB", + "\uF8DC", + "\uF8DD", + "\uF8DE", + "\uF8DF", + "\uF8E0", + "\uF8E1", + "\uF8E2", + "\uF8E3", + "\uF8E4", + "\u008E", + "\u008F", + "\u0090", + "\u0091", + "\u0092", + "\u0093", + "\u0094", + "\u0095", + "\u0096", + "\u0097", + "\u0098", + "\u0099", + "\u009A", + "\u009B", + "\u009C", + "\u009D", + "\u009E", + "\u009F", + "\u00A0", + "\u2761", + "\u2762", + "\u2763", + "\u2764", + "\u2765", + "\u2766", + "\u2767", + "\u2663", + "\u2666", + "\u2665", + "\u2660", + "\u2460", + "\u2461", + "\u2462", + "\u2463", + "\u2464", + "\u2465", + "\u2466", + "\u2467", + "\u2468", + "\u2469", + "\u2776", + "\u2777", + "\u2778", + "\u2779", + "\u277A", + "\u277B", + "\u277C", + "\u277D", + "\u277E", + "\u277F", + "\u2780", + "\u2781", + "\u2782", + "\u2783", + "\u2784", + "\u2785", + "\u2786", + "\u2787", + "\u2788", + "\u2789", + "\u278A", + "\u278B", + "\u278C", + "\u278D", + "\u278E", + "\u278F", + "\u2790", + "\u2791", + "\u2792", + "\u2793", + "\u2794", + "\u2192", + "\u2194", + "\u2195", + "\u2798", + "\u2799", + "\u279A", + "\u279B", + "\u279C", + "\u279D", + "\u279E", + "\u279F", + "\u27A0", + "\u27A1", + "\u27A2", + "\u27A3", + "\u27A4", + "\u27A5", + "\u27A6", + "\u27A7", + "\u27A8", + "\u27A9", + "\u27AA", + "\u27AB", + "\u27AC", + "\u27AD", + "\u27AE", + "\u27AF", + "\u00F0", + "\u27B1", + "\u27B2", + "\u27B3", + "\u27B4", + "\u27B5", + "\u27B6", + "\u27B7", + "\u27B8", + "\u27B9", + "\u27BA", + "\u27BB", + "\u27BC", + "\u27BD", + "\u27BE", + "\u00FF", +] +assert len(_zapfding_encoding) == 256 diff --git a/app/vendor/pypdf/_configuration.py b/app/vendor/pypdf/_configuration.py new file mode 100644 index 0000000..3418dbe --- /dev/null +++ b/app/vendor/pypdf/_configuration.py @@ -0,0 +1,296 @@ +"""Configuration value handling.""" +import dataclasses +import functools +import shutil +from collections.abc import Iterator +from contextlib import contextmanager +from contextvars import ContextVar +from typing import Any, Literal, Optional, Union + + +@functools.cache +def _determine_jbig2dec_binary() -> Optional[str]: + """ + Small utility function to determine the `jbig2dec` binary path only once. + + Returns: + The corresponding binary path if available. + """ + return shutil.which("jbig2dec") + + +@dataclasses.dataclass(frozen=True) +class Configuration: + """ + Configuration used while processing PDF files. + + These limits mostly protect against excessive resource consumption caused by malformed or malicious PDF files. + + .. note:: + + Avoid relying on specific implementation details of this class. The main public API consists of the values + being considered read-only for each instance with the overwrite method to create a new instance with the + specified values changed. + """ + + maximum_declared_stream_length: int = 75_000_000 + """The maximum allowed declared ``/Length`` value for streams.""" + array_based_stream_maximum_output_length: int = 75_000_000 + """The maximum allowed output length for array-based streams.""" + + jbig2_maximum_output_length: int = 75_000_000 + """ + The maximum allowed number of uncompressed bytes during decompression when using the ``/JBIG2Decode`` filter + (JBIG2 images). + """ + lzw_maximum_output_length: int = 75_000_000 + """ + The maximum allowed number of uncompressed bytes during decompression when using the ``/LZWDecode`` filter + (LZW compression). + """ + run_length_maximum_output_length: int = 75_000_000 + """ + The maximum allowed number of uncompressed bytes during decompression when using the ``/RunLengthDecode`` filter + (run-length compression). + """ + zlib_maximum_output_length: int = 75_000_000 + """ + The maximum allowed number of uncompressed bytes during decompression when using the ``/FlateDecode`` filter + (zlib compression). + """ + zlib_maximum_recovery_input_length: int = 5_000_000 + """ + The maximum allowed number of bytes to attempt the recovery with when using the ``/FlateDecode`` filter + (zlib compression). + """ + + flate_maximum_columns: int = 250_000 + """The maximum allowed number of columns when using the ``/FlateDecode`` filter.""" + flate_maximum_row_length: int = 4_000_000 + """The maximum allowed row length when using the ``/FlateDecode`` filter.""" + + image_maximum_buffer_size: int = 75_000_000 + """The maximum allowed number of bytes to allocate for images.""" + + xmp_maximum_input_length: int = 5_000_000 + """The maximum allowed actual decompressed stream length in bytes for XMP data.""" + xmp_maximum_element_count: int = 100_000 + """The maximum allowed number of elements for XMP data.""" + + outline_maximum_entries: int = 100_000 + """The maximum allowed number of outline entries.""" + outline_maximum_depth: int = 100 + """The maximum allowed depth for outline entries.""" + + page_tree_maximum_entries: int = 100_000 + """The maximum allowed number of entries in the page tree.""" + page_tree_maximum_depth: int = 100 + """The maximum allowed depth of the page tree.""" + + xform_maximum_invocations_per_extraction: int = 5_000 + """The maximum allowed number of ``/XObject`` forms per page during text extraction.""" + + jbig2dec_binary: Optional[str] = dataclasses.field(default_factory=_determine_jbig2dec_binary) + """ + The location of the ``jbig2dec`` binary. + + This either is a string holding the path to the binary, or ``None`` if the binary could not be found + or the binary should not be called. + """ + + page_merge_box: Literal["cropbox", "trimbox"] = "cropbox" # pypdf <= 3.4.0 used "trimbox" + """The page box to use during merge.""" + + disable_legacy_handling: bool = False + """ + Temporary configuration value for applications which do not rely on old overwrites and thus + are able to skip legacy handling. + + This disables looking for changes of the old constants during each initialization of the reader + class. Please note that while this reduces the required overhead, it will disable all related + deprecation warnings/errors. Setting this flag while legacy constants are intentionally being + modified is unsupported/incorrect. + + After the deprecation period, this configuration value will be removed with its own deprecation + period as well. + """ + + def with_overwrites(self, **kwargs: Any) -> "Configuration": + """ + Creates a new configuration with the specified values overwritten. + + Args: + **kwargs: Configuration field names and their new values. + + Returns: + A new configuration with the specified values overwritten. + """ + return dataclasses.replace(self, **kwargs) + + +# A default configuration without any value changes. +DEFAULT_CONFIGURATION = Configuration() + +# The current configuration. +CURRENT_CONFIGURATION: ContextVar[Configuration] = ContextVar( + "pypdf_configuration", + default=DEFAULT_CONFIGURATION, +) + + +def get_configuration() -> Configuration: + """ + Get the current configuration. + + Returns: + The current configuration. + """ + return CURRENT_CONFIGURATION.get() + + +def _build_configuration( + configuration: Optional[Configuration], + overwrites: dict[str, Any], +) -> Configuration: + """ + Utility function to build a new configuration from the given values. + + Args: + configuration: A possible explicit configuration to use instead of the current configuration. + overwrites: Entries to overwrite. + + Returns: + The generated configuration. + """ + base = configuration if configuration is not None else get_configuration() + return base.with_overwrites(**overwrites) + + +def overwrite_configuration( + configuration: Optional[Configuration] = None, + **overwrites: Any, +) -> Configuration: + """ + Overwrite the configuration for the current execution context. + + Args: + configuration: A direct configuration instance to use. + **overwrites: Configuration parameters to overwrite in the current or given configuration. + + Returns: + The new configuration. + """ + new_configuration = _build_configuration(configuration, overwrites) + CURRENT_CONFIGURATION.set(new_configuration) + return new_configuration + + +@contextmanager +def apply_configuration( + configuration: Optional[Configuration] = None, + **overwrites: Any, +) -> Iterator[Configuration]: + """ + Temporarily overwrite the configuration. + + Args: + configuration: A direct configuration instance to use. + **overwrites: Configuration parameters to overwrite in the current or given configuration. + + Returns: + The new configuration. + """ + new_configuration = _build_configuration(configuration, overwrites) + token = CURRENT_CONFIGURATION.set(new_configuration) + try: + yield new_configuration + finally: + CURRENT_CONFIGURATION.reset(token) + + +# Map the new field names to their legacy names. +LEGACY_NAME_MAPPING = { + "maximum_declared_stream_length": "MAX_DECLARED_STREAM_LENGTH", + "array_based_stream_maximum_output_length": "MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH", + "jbig2_maximum_output_length": "JBIG2_MAX_OUTPUT_LENGTH", + "lzw_maximum_output_length": "LZW_MAX_OUTPUT_LENGTH", + "run_length_maximum_output_length": "RUN_LENGTH_MAX_OUTPUT_LENGTH", + "zlib_maximum_output_length": "ZLIB_MAX_OUTPUT_LENGTH", + "zlib_maximum_recovery_input_length": "ZLIB_MAX_RECOVERY_INPUT_LENGTH", + "flate_maximum_columns": "FLATE_MAX_COLUMNS", + "flate_maximum_row_length": "FLATE_MAX_ROW_LENGTH", + "image_maximum_buffer_size": "FLATE_MAX_BUFFER_SIZE", + "xmp_maximum_input_length": "XMP_MAX_INPUT_LENGTH", + "xmp_maximum_element_count": "XMP_MAX_ELEMENT_COUNT", + "jbig2dec_binary": "JBIG2DEC_BINARY", + "page_merge_box": "MERGE_CROP_BOX", +} + +# Record which legacy overwrites have already emitted a warning in the current process/session. +WARNED_LEGACY_OVERWRITES: set[str] = set() + + +def apply_legacy_configuration() -> Configuration: + """ + Overwrite the current configuration with the legacy configuration if it has been changed. + + Returns: + The new configuration which has been applied. + """ + configuration = get_configuration() + if configuration.disable_legacy_handling: + return configuration + + from pypdf._page import MERGE_CROP_BOX # noqa: PLC0415 + from pypdf.filters import ( # noqa: PLC0415 + FLATE_MAX_BUFFER_SIZE, + FLATE_MAX_COLUMNS, + FLATE_MAX_ROW_LENGTH, + JBIG2_MAX_OUTPUT_LENGTH, + JBIG2DEC_BINARY, + LZW_MAX_OUTPUT_LENGTH, + MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH, + MAX_DECLARED_STREAM_LENGTH, + RUN_LENGTH_MAX_OUTPUT_LENGTH, + ZLIB_MAX_OUTPUT_LENGTH, + ZLIB_MAX_RECOVERY_INPUT_LENGTH, + ) + from pypdf.xmp import XMP_MAX_ELEMENT_COUNT, XMP_MAX_INPUT_LENGTH # noqa: PLC0415 + + legacy_values = { + "MAX_DECLARED_STREAM_LENGTH": MAX_DECLARED_STREAM_LENGTH, + "MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH": MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH, + "JBIG2_MAX_OUTPUT_LENGTH": JBIG2_MAX_OUTPUT_LENGTH, + "LZW_MAX_OUTPUT_LENGTH": LZW_MAX_OUTPUT_LENGTH, + "RUN_LENGTH_MAX_OUTPUT_LENGTH": RUN_LENGTH_MAX_OUTPUT_LENGTH, + "ZLIB_MAX_OUTPUT_LENGTH": ZLIB_MAX_OUTPUT_LENGTH, + "ZLIB_MAX_RECOVERY_INPUT_LENGTH": ZLIB_MAX_RECOVERY_INPUT_LENGTH, + "FLATE_MAX_COLUMNS": FLATE_MAX_COLUMNS, + "FLATE_MAX_ROW_LENGTH": FLATE_MAX_ROW_LENGTH, + "FLATE_MAX_BUFFER_SIZE": FLATE_MAX_BUFFER_SIZE, + "XMP_MAX_INPUT_LENGTH": XMP_MAX_INPUT_LENGTH, + "XMP_MAX_ELEMENT_COUNT": XMP_MAX_ELEMENT_COUNT, + "JBIG2DEC_BINARY": JBIG2DEC_BINARY, + "MERGE_CROP_BOX": MERGE_CROP_BOX, + } + overwrites: dict[str, Union[int, str, None]] = {} + + for field_name, legacy_name in LEGACY_NAME_MAPPING.items(): + legacy_value = legacy_values[legacy_name] + + if legacy_value != getattr(DEFAULT_CONFIGURATION, field_name): + if legacy_name not in WARNED_LEGACY_OVERWRITES: + from pypdf._utils import deprecate_with_replacement # noqa: PLC0415 + + deprecate_with_replacement( + old_name=legacy_name, + new_name=f"Configuration.{field_name}", + removed_in="7.0.0", + ) + WARNED_LEGACY_OVERWRITES.add(legacy_name) + + overwrites[field_name] = legacy_value + + if not overwrites: + return configuration + return overwrite_configuration(**overwrites) # type: ignore[arg-type] diff --git a/app/vendor/pypdf/_crypt_providers/__init__.py b/app/vendor/pypdf/_crypt_providers/__init__.py new file mode 100644 index 0000000..e11eecd --- /dev/null +++ b/app/vendor/pypdf/_crypt_providers/__init__.py @@ -0,0 +1,80 @@ +# Copyright (c) 2023, exiledkingcc +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from pypdf._crypt_providers._base import CryptBase, CryptIdentity + +try: + from pypdf._crypt_providers._cryptography import ( + CryptAES, + CryptRC4, + aes_cbc_decrypt, + aes_cbc_encrypt, + aes_ecb_decrypt, + aes_ecb_encrypt, + crypt_provider, + rc4_decrypt, + rc4_encrypt, + ) +except ImportError: + try: + from pypdf._crypt_providers._pycryptodome import ( # type: ignore[assignment] + CryptAES, + CryptRC4, + aes_cbc_decrypt, + aes_cbc_encrypt, + aes_ecb_decrypt, + aes_ecb_encrypt, + crypt_provider, + rc4_decrypt, + rc4_encrypt, + ) + except ImportError: + from pypdf._crypt_providers._fallback import ( # type: ignore[assignment] + CryptAES, + CryptRC4, + aes_cbc_decrypt, + aes_cbc_encrypt, + aes_ecb_decrypt, + aes_ecb_encrypt, + crypt_provider, + rc4_decrypt, + rc4_encrypt, + ) + +__all__ = [ + "CryptAES", + "CryptBase", + "CryptIdentity", + "CryptRC4", + "aes_cbc_decrypt", + "aes_cbc_encrypt", + "aes_ecb_decrypt", + "aes_ecb_encrypt", + "crypt_provider", + "rc4_decrypt", + "rc4_encrypt", +] diff --git a/app/vendor/pypdf/_crypt_providers/_base.py b/app/vendor/pypdf/_crypt_providers/_base.py new file mode 100644 index 0000000..d66dd4c --- /dev/null +++ b/app/vendor/pypdf/_crypt_providers/_base.py @@ -0,0 +1,38 @@ +# Copyright (c) 2023, exiledkingcc +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +class CryptBase: + def encrypt(self, data: bytes) -> bytes: # pragma: no cover + return data + + def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: # pragma: no cover + return data + + +class CryptIdentity(CryptBase): + pass diff --git a/app/vendor/pypdf/_crypt_providers/_cryptography.py b/app/vendor/pypdf/_crypt_providers/_cryptography.py new file mode 100644 index 0000000..28e83e0 --- /dev/null +++ b/app/vendor/pypdf/_crypt_providers/_cryptography.py @@ -0,0 +1,184 @@ +# Copyright (c) 2023, exiledkingcc +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import secrets + +from cryptography import __version__ +from cryptography.exceptions import UnsupportedAlgorithm +from cryptography.hazmat.primitives.ciphers.algorithms import AES +from cryptography.hazmat.primitives.ciphers.base import Cipher +from cryptography.hazmat.primitives.ciphers.modes import CBC, ECB +from cryptography.hazmat.primitives.padding import PKCS7 + +from pypdf._crypt_providers._base import CryptBase +from pypdf._utils import logger_warning +from pypdf.errors import PdfStreamError + +try: + # 43.0.0: https://cryptography.io/en/latest/changelog/#v43-0-0 + from cryptography.hazmat.decrepit.ciphers.algorithms import ARC4 +except ImportError: + from cryptography.hazmat.primitives.ciphers.algorithms import ARC4 + +crypt_provider = ("cryptography", __version__) + + +class CryptRC4(CryptBase): + # Assume OpenSSL provides RC4; flipped to False the first time it rejects + # the cipher (e.g. legacy provider disabled), so the failing attempt is + # paid once. + _is_rc4_supported = True + + def __init__(self, key: bytes) -> None: + self._key = key + self._fallback = None + if CryptRC4._is_rc4_supported: + self.cipher = Cipher(ARC4(key), mode=None) + else: + self._fallback = self._pure_python_rc4(key) + + @staticmethod + def _pure_python_rc4(key: bytes) -> CryptBase: + from pypdf._crypt_providers import _fallback # noqa: PLC0415 + + return _fallback.CryptRC4(key) + + @classmethod + def _disable_rc4(cls, key: bytes) -> CryptBase: + if cls._is_rc4_supported: + logger_warning( + ( + "RC4 is not supported by the current OpenSSL build; " + "falling back to the pure-Python RC4 implementation." + ), + source=__name__, + ) + cls._is_rc4_supported = False + return cls._pure_python_rc4(key) + + def encrypt(self, data: bytes) -> bytes: + if self._fallback is not None: + return self._fallback.encrypt(data) + try: + encryptor = self.cipher.encryptor() + return encryptor.update(data) + encryptor.finalize() + except UnsupportedAlgorithm: + self._fallback = self._disable_rc4(self._key) + return self._fallback.encrypt(data) + + def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: + if self._fallback is not None: + return self._fallback.decrypt(data, strict=strict) + try: + decryptor = self.cipher.decryptor() + return decryptor.update(data) + decryptor.finalize() + except UnsupportedAlgorithm: + self._fallback = self._disable_rc4(self._key) + return self._fallback.decrypt(data, strict=strict) + + +class CryptAES(CryptBase): + def __init__(self, key: bytes) -> None: + self.alg = AES(key) + + def encrypt(self, data: bytes) -> bytes: + iv = secrets.token_bytes(16) + padder = PKCS7(128).padder() + padded_data = padder.update(data) + padder.finalize() + + cipher = Cipher(self.alg, CBC(iv)) + encryptor = cipher.encryptor() + return iv + encryptor.update(padded_data) + encryptor.finalize() + + def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: + iv = data[:16] + data = data[16:] + # for empty encrypted data + if not data: + return data + + if not strict and len(data) % 16 != 0: + logger_warning("Adding missing padding.", source=__name__) + padder = PKCS7(128).padder() + data = padder.update(data) + padder.finalize() + + cipher = Cipher(self.alg, CBC(iv)) + decryptor = cipher.decryptor() + try: + padded_data = decryptor.update(data) + decryptor.finalize() + except ValueError as exception: + # Only raised in strict mode. Non-strict mode fixes padding. + raise PdfStreamError(exception) + + unpadder = PKCS7(128).unpadder() + try: + return unpadder.update(padded_data) + unpadder.finalize() + except ValueError as exception: + if strict: + raise PdfStreamError(exception) + logger_warning("Ignoring padding error: %(exception)s", source=__name__, exception=exception) + return padded_data[: -padded_data[-1]] + + +def rc4_encrypt(key: bytes, data: bytes) -> bytes: + if CryptRC4._is_rc4_supported: + try: + encryptor = Cipher(ARC4(key), mode=None).encryptor() + return encryptor.update(data) + encryptor.finalize() + except UnsupportedAlgorithm: + return CryptRC4._disable_rc4(key).encrypt(data) + return CryptRC4._pure_python_rc4(key).encrypt(data) + + +def rc4_decrypt(key: bytes, data: bytes) -> bytes: + if CryptRC4._is_rc4_supported: + try: + decryptor = Cipher(ARC4(key), mode=None).decryptor() + return decryptor.update(data) + decryptor.finalize() + except UnsupportedAlgorithm: + return CryptRC4._disable_rc4(key).decrypt(data) + return CryptRC4._pure_python_rc4(key).decrypt(data) + + +def aes_ecb_encrypt(key: bytes, data: bytes) -> bytes: + encryptor = Cipher(AES(key), mode=ECB()).encryptor() + return encryptor.update(data) + encryptor.finalize() + + +def aes_ecb_decrypt(key: bytes, data: bytes) -> bytes: + decryptor = Cipher(AES(key), mode=ECB()).decryptor() + return decryptor.update(data) + decryptor.finalize() + + +def aes_cbc_encrypt(key: bytes, iv: bytes, data: bytes) -> bytes: + encryptor = Cipher(AES(key), mode=CBC(iv)).encryptor() + return encryptor.update(data) + encryptor.finalize() + + +def aes_cbc_decrypt(key: bytes, iv: bytes, data: bytes) -> bytes: + decryptor = Cipher(AES(key), mode=CBC(iv)).decryptor() + return decryptor.update(data) + decryptor.finalize() diff --git a/app/vendor/pypdf/_crypt_providers/_fallback.py b/app/vendor/pypdf/_crypt_providers/_fallback.py new file mode 100644 index 0000000..2b7d327 --- /dev/null +++ b/app/vendor/pypdf/_crypt_providers/_fallback.py @@ -0,0 +1,93 @@ +# Copyright (c) 2023, exiledkingcc +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from pypdf._crypt_providers._base import CryptBase +from pypdf.errors import DependencyError + +_DEPENDENCY_ERROR_STR = "cryptography>=3.1 is required for AES algorithm" + + +crypt_provider = ("local_crypt_fallback", "0.0.0") + + +class CryptRC4(CryptBase): + def __init__(self, key: bytes) -> None: + self.s = bytearray(range(256)) + j = 0 + for i in range(256): + j = (j + self.s[i] + key[i % len(key)]) % 256 + self.s[i], self.s[j] = self.s[j], self.s[i] + + def encrypt(self, data: bytes) -> bytes: + s = bytearray(self.s) + out = [0 for _ in range(len(data))] + i, j = 0, 0 + for k in range(len(data)): + i = (i + 1) % 256 + j = (j + s[i]) % 256 + s[i], s[j] = s[j], s[i] + x = s[(s[i] + s[j]) % 256] + out[k] = data[k] ^ x + return bytes(out) + + def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: + return self.encrypt(data) + + +class CryptAES(CryptBase): + def __init__(self, key: bytes) -> None: + pass + + def encrypt(self, data: bytes) -> bytes: + raise DependencyError(_DEPENDENCY_ERROR_STR) + + def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: + raise DependencyError(_DEPENDENCY_ERROR_STR) + + +def rc4_encrypt(key: bytes, data: bytes) -> bytes: + return CryptRC4(key).encrypt(data) + + +def rc4_decrypt(key: bytes, data: bytes) -> bytes: + return CryptRC4(key).decrypt(data) + + +def aes_ecb_encrypt(key: bytes, data: bytes) -> bytes: + raise DependencyError(_DEPENDENCY_ERROR_STR) + + +def aes_ecb_decrypt(key: bytes, data: bytes) -> bytes: + raise DependencyError(_DEPENDENCY_ERROR_STR) + + +def aes_cbc_encrypt(key: bytes, iv: bytes, data: bytes) -> bytes: + raise DependencyError(_DEPENDENCY_ERROR_STR) + + +def aes_cbc_decrypt(key: bytes, iv: bytes, data: bytes) -> bytes: + raise DependencyError(_DEPENDENCY_ERROR_STR) diff --git a/app/vendor/pypdf/_crypt_providers/_pycryptodome.py b/app/vendor/pypdf/_crypt_providers/_pycryptodome.py new file mode 100644 index 0000000..b5efba2 --- /dev/null +++ b/app/vendor/pypdf/_crypt_providers/_pycryptodome.py @@ -0,0 +1,110 @@ +# Copyright (c) 2023, exiledkingcc +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import secrets + +from Crypto import __version__ +from Crypto.Cipher import AES, ARC4 +from Crypto.Util.Padding import pad, unpad + +from pypdf._crypt_providers._base import CryptBase +from pypdf._utils import logger_warning +from pypdf.errors import PdfStreamError + +crypt_provider = ("pycryptodome", __version__) + + +class CryptRC4(CryptBase): + def __init__(self, key: bytes) -> None: + self.key = key + + def encrypt(self, data: bytes) -> bytes: + return ARC4.ARC4Cipher(self.key).encrypt(data) + + def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: + return ARC4.ARC4Cipher(self.key).decrypt(data) + + +class CryptAES(CryptBase): + def __init__(self, key: bytes) -> None: + self.key = key + + def encrypt(self, data: bytes) -> bytes: + iv = secrets.token_bytes(16) + padded_data = pad(data, 16) + aes = AES.new(self.key, AES.MODE_CBC, iv) + return iv + aes.encrypt(padded_data) + + def decrypt(self, data: bytes, *, strict: bool = True) -> bytes: + iv = data[:16] + data = data[16:] + # for empty encrypted data + if not data: + return data + + if not strict and len(data) % 16 != 0: + logger_warning("Adding missing padding.", source=__name__) + data = pad(data, 16) + + aes = AES.new(self.key, AES.MODE_CBC, iv) + try: + padded_data = aes.decrypt(data) + except ValueError as exception: + # Only raised in strict mode. Non-strict mode fixes padding. + raise PdfStreamError(exception) + + try: + return unpad(padded_data, 16) + except ValueError as exception: + if strict: + raise PdfStreamError(exception) + logger_warning("Ignoring padding error: %(exception)s", source=__name__, exception=exception) + return padded_data[: -padded_data[-1]] + + +def rc4_encrypt(key: bytes, data: bytes) -> bytes: + return ARC4.ARC4Cipher(key).encrypt(data) + + +def rc4_decrypt(key: bytes, data: bytes) -> bytes: + return ARC4.ARC4Cipher(key).decrypt(data) + + +def aes_ecb_encrypt(key: bytes, data: bytes) -> bytes: + return AES.new(key, AES.MODE_ECB).encrypt(data) + + +def aes_ecb_decrypt(key: bytes, data: bytes) -> bytes: + return AES.new(key, AES.MODE_ECB).decrypt(data) + + +def aes_cbc_encrypt(key: bytes, iv: bytes, data: bytes) -> bytes: + return AES.new(key, AES.MODE_CBC, iv).encrypt(data) + + +def aes_cbc_decrypt(key: bytes, iv: bytes, data: bytes) -> bytes: + return AES.new(key, AES.MODE_CBC, iv).decrypt(data) diff --git a/app/vendor/pypdf/_doc_common.py b/app/vendor/pypdf/_doc_common.py new file mode 100644 index 0000000..4353280 --- /dev/null +++ b/app/vendor/pypdf/_doc_common.py @@ -0,0 +1,1646 @@ +# Copyright (c) 2006, Mathieu Fenniak +# Copyright (c) 2007, Ashish Kulkarni +# Copyright (c) 2024, Pubpub-ZZ +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import struct +from abc import ABC, abstractmethod +from collections.abc import Iterable, Iterator, Mapping, Sequence +from datetime import datetime +from typing import ( + Any, + NoReturn, + Optional, + Union, + cast, +) + +from ._configuration import get_configuration +from ._encryption import Encryption +from ._page import PageObject, _VirtualList +from ._page_labels import index2label as page_index2page_label +from ._utils import ( + _TraversalState, + deprecation_with_replacement, + logger_warning, + parse_iso8824_date, +) +from .constants import CatalogAttributes as CA +from .constants import ( + CheckboxRadioButtonAttributes, + Core, + GoToActionArguments, + PagesAttributes, + UserAccessPermissions, +) +from .constants import DocumentInformationAttributes as DI +from .constants import FieldDictionaryAttributes as FA +from .constants import PageAttributes as PG +from .errors import LimitReachedError, PdfReadError, PyPdfError +from .filters import _decompress_with_limit +from .generic import ( + ArrayObject, + BooleanObject, + ByteStringObject, + Destination, + DictionaryObject, + EncodedStreamObject, + Field, + Fit, + FloatObject, + IndirectObject, + NameObject, + NullObject, + PdfObject, + TextStringObject, + TreeObject, + ViewerPreferences, + create_string_object, + is_null_or_none, +) +from .generic._files import EmbeddedFile +from .types import OutlineType, PagemodeType +from .xmp import XmpInformation + + +def convert_to_int(d: bytes, size: int) -> Union[int, tuple[Any, ...]]: + if size > 8: + raise PdfReadError("Invalid size in convert_to_int") + d = b"\x00\x00\x00\x00\x00\x00\x00\x00" + d + d = d[-8:] + return cast(int, struct.unpack(">Q", d)[0]) + + +class DocumentInformation(DictionaryObject): + """ + A class representing the basic document metadata provided in a PDF File. + This class is accessible through + :py:class:`PdfReader.metadata`. + + All text properties of the document metadata have + *two* properties, e.g. author and author_raw. The non-raw property will + always return a ``TextStringObject``, making it ideal for a case where the + metadata is being displayed. The raw property can sometimes return a + ``ByteStringObject``, if pypdf was unable to decode the string's text + encoding; this requires additional safety in the caller and therefore is not + as commonly accessed. + """ + + def __init__(self) -> None: + DictionaryObject.__init__(self) + + def _get_text(self, key: str) -> Optional[str]: + retval = self.get(key, None) + if isinstance(retval, TextStringObject): + return retval + if isinstance(retval, ByteStringObject): + return str(retval) + return None + + @property + def title(self) -> Optional[str]: + """ + Read-only property accessing the document's title. + + Returns a ``TextStringObject`` or ``None`` if the title is not + specified. + """ + return ( + self._get_text(DI.TITLE) or self.get(DI.TITLE).get_object() # type: ignore[union-attr] + if self.get(DI.TITLE) + else None + ) + + @property + def title_raw(self) -> Optional[str]: + """The "raw" version of title; can return a ``ByteStringObject``.""" + return self.get(DI.TITLE) + + @property + def author(self) -> Optional[str]: + """ + Read-only property accessing the document's author. + + Returns a ``TextStringObject`` or ``None`` if the author is not + specified. + """ + return self._get_text(DI.AUTHOR) + + @property + def author_raw(self) -> Optional[str]: + """The "raw" version of author; can return a ``ByteStringObject``.""" + return self.get(DI.AUTHOR) + + @property + def subject(self) -> Optional[str]: + """ + Read-only property accessing the document's subject. + + Returns a ``TextStringObject`` or ``None`` if the subject is not + specified. + """ + return self._get_text(DI.SUBJECT) + + @property + def subject_raw(self) -> Optional[str]: + """The "raw" version of subject; can return a ``ByteStringObject``.""" + return self.get(DI.SUBJECT) + + @property + def creator(self) -> Optional[str]: + """ + Read-only property accessing the document's creator. + + If the document was converted to PDF from another format, this is the + name of the application (e.g. OpenOffice) that created the original + document from which it was converted. Returns a ``TextStringObject`` or + ``None`` if the creator is not specified. + """ + return self._get_text(DI.CREATOR) + + @property + def creator_raw(self) -> Optional[str]: + """The "raw" version of creator; can return a ``ByteStringObject``.""" + return self.get(DI.CREATOR) + + @property + def producer(self) -> Optional[str]: + """ + Read-only property accessing the document's producer. + + If the document was converted to PDF from another format, this is the + name of the application (for example, macOS Quartz) that converted it to + PDF. Returns a ``TextStringObject`` or ``None`` if the producer is not + specified. + """ + return self._get_text(DI.PRODUCER) + + @property + def producer_raw(self) -> Optional[str]: + """The "raw" version of producer; can return a ``ByteStringObject``.""" + return self.get(DI.PRODUCER) + + @property + def creation_date(self) -> Optional[datetime]: + """Read-only property accessing the document's creation date.""" + return parse_iso8824_date(self._get_text(DI.CREATION_DATE)) + + @property + def creation_date_raw(self) -> Optional[str]: + """ + The "raw" version of creation date; can return a ``ByteStringObject``. + + Typically in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix + is the offset from UTC. + """ + return self.get(DI.CREATION_DATE) + + @property + def modification_date(self) -> Optional[datetime]: + """ + Read-only property accessing the document's modification date. + + The date and time the document was most recently modified. + """ + return parse_iso8824_date(self._get_text(DI.MOD_DATE)) + + @property + def modification_date_raw(self) -> Optional[str]: + """ + The "raw" version of modification date; can return a + ``ByteStringObject``. + + Typically, in the format ``D:YYYYMMDDhhmmss[+Z-]hh'mm`` where the suffix + is the offset from UTC. + """ + return self.get(DI.MOD_DATE) + + @property + def keywords(self) -> Optional[str]: + """ + Read-only property accessing the document's keywords. + + Returns a ``TextStringObject`` or ``None`` if keywords are not + specified. + """ + return self._get_text(DI.KEYWORDS) + + @property + def keywords_raw(self) -> Optional[str]: + """The "raw" version of keywords; can return a ``ByteStringObject``.""" + return self.get(DI.KEYWORDS) + + +class PdfDocCommon(ABC): + """ + Common functions from PdfWriter and PdfReader objects. + + This root class is strongly abstracted. + """ + + strict: bool = False # default + + flattened_pages: Optional[list[PageObject]] = None + + _encryption: Optional[Encryption] = None + + _readonly: bool = False + + @property + @abstractmethod + def root_object(self) -> DictionaryObject: + ... # pragma: no cover + + @property + @abstractmethod + def pdf_header(self) -> str: + ... # pragma: no cover + + @abstractmethod + def get_object( + self, indirect_reference: Union[int, IndirectObject] + ) -> Optional[PdfObject]: + ... # pragma: no cover + + @abstractmethod + def _replace_object(self, indirect: IndirectObject, obj: PdfObject) -> PdfObject: + ... # pragma: no cover + + @property + @abstractmethod + def _info(self) -> Optional[DictionaryObject]: + ... # pragma: no cover + + @property + def metadata(self) -> Optional[DocumentInformation]: + """ + Retrieve the PDF file's document information dictionary, if it exists. + + Note that some PDF files use metadata streams instead of document + information dictionaries, and these metadata streams will not be + accessed by this function. + """ + retval = DocumentInformation() + if self._info is None: + return None + retval.update(self._info) + return retval + + @property + @abstractmethod + def xmp_metadata(self) -> Optional[XmpInformation]: + ... # pragma: no cover + + @property + def viewer_preferences(self) -> Optional[ViewerPreferences]: + """Returns the existing ViewerPreferences as an overloaded dictionary.""" + o = self.root_object.get(CA.VIEWER_PREFERENCES, None) + if o is None: + return None + o = o.get_object() + if not isinstance(o, DictionaryObject): + logger_warning( + "Viewer preferences are not a dictionary: %(preferences)s", + source=__name__, + preferences=o, + ) + return None + if not isinstance(o, ViewerPreferences): + o = ViewerPreferences(o) + if hasattr(o, "indirect_reference") and o.indirect_reference is not None: + self._replace_object(o.indirect_reference, o) + else: + self.root_object[NameObject(CA.VIEWER_PREFERENCES)] = o + return o + + def get_num_pages(self) -> int: + """ + Calculate the number of pages in this PDF file. + + Returns: + The number of pages of the parsed PDF file. + + Raises: + PdfReadError: If restrictions prevent this action. + + """ + # Flattened pages will not work on an encrypted PDF; + # the PDF file's page count is used in this case. Otherwise, + # the original method (flattened page count) is used. + if self.is_encrypted: + return self.root_object["/Pages"]["/Count"] # type: ignore[no-any-return, index] + if self.flattened_pages is None: + self._flatten(self._readonly) + assert self.flattened_pages is not None + return len(self.flattened_pages) + + def get_page(self, page_number: int) -> PageObject: + """ + Retrieve a page by number from this PDF file. + Most of the time ``.pages[page_number]`` is preferred. + + Args: + page_number: The page number to retrieve + (pages begin at zero) + + Returns: + A :class:`PageObject` instance. + + """ + if self.flattened_pages is None: + self._flatten(self._readonly) + assert self.flattened_pages is not None, "mypy" + return self.flattened_pages[page_number] + + def _get_page_in_node( + self, + page_number: int, + ) -> tuple[DictionaryObject, int]: + """ + Retrieve the node and position within the /Kids containing the page. + If page_number is greater than the number of pages, it returns the top node, -1. + """ + top = cast(DictionaryObject, self.root_object["/Pages"]) + visited: set[int] = set() + + def recursive_call( + _node: DictionaryObject, mi: int + ) -> tuple[Optional[PdfObject], int]: + _node_id = id(_node) + if _node_id in visited: + raise LimitReachedError("Detected cycle in /Pages hierarchy when retrieving page.") + visited.add(_node_id) + ma = cast(int, _node.get("/Count", 1)) # default 1 for /Page types + if _node.get("/Type") == "/Page": + if page_number == mi: + return _node, -1 + return None, mi + 1 + if (page_number - mi) >= ma: # not in nodes below + if _node == top: + return top, -1 + return None, mi + ma + for _idx, kid in enumerate(cast(ArrayObject, _node["/Kids"])): + kid = cast(DictionaryObject, kid.get_object()) + n, i = recursive_call(kid, mi) + if n is not None: # page has just been found ... + if i < 0: # ... just below! + return _node, _idx + # ... at lower levels + return n, i + mi = i + raise PyPdfError("Unexpectedly cannot find the node.") + + node, idx = recursive_call(top, 0) + assert isinstance(node, DictionaryObject), "mypy" + return node, idx + + @property + def named_destinations(self) -> dict[str, Destination]: + """A read-only dictionary which maps names to destinations.""" + return self._get_named_destinations() + + def get_named_dest_root(self) -> ArrayObject: + named_dest = ArrayObject() + if CA.NAMES in self.root_object and isinstance( + self.root_object[CA.NAMES], DictionaryObject + ): + names = cast(DictionaryObject, self.root_object[CA.NAMES]) + if CA.DESTS in names and isinstance(names[CA.DESTS], DictionaryObject): + # §3.6.3 Name Dictionary (PDF spec 1.7) + dests = cast(DictionaryObject, names[CA.DESTS]) + if CA.NAMES in dests: + # §7.9.6, entries in a name tree node dictionary + named_dest = cast(ArrayObject, dests[CA.NAMES]) + else: + named_dest = ArrayObject() + dests[NameObject(CA.NAMES)] = named_dest + elif hasattr(self, "_add_object"): + dests = DictionaryObject() + dests_ref = self._add_object(dests) + names[NameObject(CA.DESTS)] = dests_ref + dests[NameObject(CA.NAMES)] = named_dest + + elif hasattr(self, "_add_object"): + names = DictionaryObject() + names_ref = self._add_object(names) + self.root_object[NameObject(CA.NAMES)] = names_ref + dests = DictionaryObject() + dests_ref = self._add_object(dests) + names[NameObject(CA.DESTS)] = dests_ref + dests[NameObject(CA.NAMES)] = named_dest + + return named_dest + + ## common + def _get_named_destinations( + self, + *, + tree: Optional[DictionaryObject] = None, + retval: Optional[dict[str, Destination]] = None, + visited: Optional[set[int]] = None, + ) -> dict[str, Destination]: + """ + Retrieve the named destinations present in the document. + + Args: + tree: The current tree. + retval: The previously retrieved destinations for nested calls. + visited: Already known/visited objects. + + Returns: + A dictionary which maps names to destinations. + + """ + if visited is None: + visited = set() + if retval is None: + retval = {} + catalog = self.root_object + + # get the name tree + candidate: Optional[PdfObject] = None + if CA.DESTS in catalog: + candidate = catalog[CA.DESTS].get_object() + elif CA.NAMES in catalog: + names = catalog[CA.NAMES].get_object() + if isinstance(names, DictionaryObject) and CA.DESTS in names: + candidate = names[CA.DESTS].get_object() + if candidate is not None and not isinstance(candidate, DictionaryObject): + logger_warning( + "Destination tree is not a dictionary: %(tree)s", + source=__name__, + tree=candidate, + ) + return retval + if candidate is not None: + tree = candidate + + if is_null_or_none(tree): + return retval + assert tree is not None, "mypy" + + tree_id = id(tree) + if tree_id in visited: + logger_warning("Detected cycle in destination tree.", source=__name__) + return retval + visited.add(tree_id) + + if PagesAttributes.KIDS in tree: + kids = tree[PagesAttributes.KIDS].get_object() + if not isinstance(kids, ArrayObject): + logger_warning( + "Destination tree kids are not an array: %(kids)s", + source=__name__, + kids=kids, + ) + return retval + # recurse down the tree + for kid in kids: + self._get_named_destinations(tree=kid.get_object(), retval=retval, visited=visited) + # §7.9.6, entries in a name tree node dictionary + elif CA.NAMES in tree: # /Kids and /Names are exclusives (§7.9.6) + names = tree[CA.NAMES].get_object() + if not isinstance(names, ArrayObject): + logger_warning( + "Destination tree names are not an array: %(names)s", + source=__name__, + names=names, + ) + return retval + i = 0 + while i < len(names): + key = names[i].get_object() + i += 1 + if not isinstance(key, (bytes, str)): + continue + try: + value = names[i].get_object() + except IndexError: + break + i += 1 + if isinstance(value, DictionaryObject): + if "/D" in value: + value = value["/D"] + else: + continue + dest = self._build_destination(key, value) + if dest is not None: + retval[cast(str, dest["/Title"])] = dest + # Remain backwards-compatible. + retval[str(key)] = dest + else: # case where /Dests is in the document's catalog dictionary (PDF 1.7 specs, §2 about PDF 1.1) + for k__, v__ in tree.items(): + val = v__.get_object() + if isinstance(val, DictionaryObject): + if "/D" in val: + val = val["/D"].get_object() + else: + continue + dest = self._build_destination(k__, val) + if dest is not None: + retval[k__] = dest + return retval + + # A select group of relevant field attributes. For the complete list, + # see §12.3.2 of the PDF 1.7 or PDF 2.0 specification. + + def get_fields( + self, + tree: Optional[DictionaryObject] = None, + retval: Optional[dict[Any, Any]] = None, + fileobj: Optional[Any] = None, + stack: Optional[list[PdfObject]] = None, + ) -> Optional[dict[str, Any]]: + """ + Extract field data if this PDF contains interactive form fields. + + The *tree*, *retval*, *stack* parameters are for recursive use. + + Args: + tree: Current object to parse. + retval: In-progress list of fields. + fileobj: A file object (usually a text file) to write + a report to on all interactive form fields found. + stack: List of already parsed objects. + + Returns: + A dictionary where each key is a field name, and each + value is a :class:`Field` object. By + default, the mapping name is used for keys. + ``None`` if form data could not be located. + + """ + field_attributes = FA.attributes_dict() + field_attributes.update(CheckboxRadioButtonAttributes.attributes_dict()) + if retval is None: + retval = {} + catalog = self.root_object + stack = [] + # get the AcroForm tree + if CA.ACRO_FORM in catalog: + entry = catalog[CA.ACRO_FORM] + acro_form = None if entry is None else entry.get_object() + if acro_form is not None and not isinstance( + acro_form, DictionaryObject + ): + logger_warning( + "AcroForm is not a dictionary: %(acro_form)s", + source=__name__, + acro_form=acro_form, + ) + return None + tree = acro_form + else: + return None + if tree is None: + return retval + assert stack is not None + if "/Fields" in tree: + fields = tree["/Fields"].get_object() + if not isinstance(fields, ArrayObject): + logger_warning( + "AcroForm /Fields is not an array: %(fields)s", + source=__name__, + fields=fields, + ) + return retval + for f in fields: + field = f.get_object() + self._build_field(field, retval, fileobj, field_attributes, stack) + elif any(attr in tree for attr in field_attributes): + # Tree is a field + self._build_field(tree, retval, fileobj, field_attributes, stack) + return retval + + def _get_qualified_field_name( + self, + *, + parent: DictionaryObject, + visited: Optional[set[int]] = None + ) -> str: + if visited is None: + visited = set() + parent_id = id(parent) + if parent_id in visited: + raise LimitReachedError("Detected cycle in /Parent hierarchy when retrieving qualified field name.") + visited.add(parent_id) + + if "/TM" in parent: + return cast(str, parent["/TM"]) + if "/Parent" in parent: + return ( + self._get_qualified_field_name( + parent=cast(DictionaryObject, parent["/Parent"]), + visited=visited, + ) + + "." + + cast(str, parent.get("/T", "")) + ) + return cast(str, parent.get("/T", "")) + + @staticmethod + def _normal_appearance(appearance: Any) -> DictionaryObject: + """Return the /N normal-appearance sub-dictionary of an /AP entry.""" + appearance = appearance.get_object() + if not isinstance(appearance, DictionaryObject): + raise PdfReadError(f"Expected appearance dictionary, got {appearance!r}") + normal = appearance.get("/N") + if normal is not None: + normal = normal.get_object() + if not isinstance(normal, DictionaryObject): + raise PdfReadError(f"Expected /N appearance dictionary, got {normal!r}") + return normal + + def _build_field( + self, + field: Union[TreeObject, DictionaryObject], + retval: dict[Any, Any], + fileobj: Any, + field_attributes: Any, + stack: list[PdfObject], + ) -> None: + if not isinstance(field, DictionaryObject): + logger_warning( + "Form field is not a dictionary: %(field)s", + source=__name__, + field=field, + ) + return + if all(attr not in field for attr in ("/T", "/TM")): + return + key = self._get_qualified_field_name(parent=field) + if fileobj: + self._write_field(fileobj, field, field_attributes) + fileobj.write("\n") + retval[key] = Field(field) + obj = retval[key].indirect_reference.get_object() # to get the full object + if obj.get(FA.FT, "") == "/Ch" and obj.get(NameObject(FA.Opt)): + retval[key][NameObject("/_States_")] = obj[NameObject(FA.Opt)] + if obj.get(FA.FT, "") == "/Btn" and "/AP" in obj: + # Checkbox + normal = self._normal_appearance(obj["/AP"]) + retval[key][NameObject("/_States_")] = ArrayObject(list(normal.keys())) + if "/Off" not in retval[key]["/_States_"]: + retval[key][NameObject("/_States_")].append(NameObject("/Off")) + elif obj.get(FA.FT, "") == "/Btn" and obj.get(FA.Ff, 0) & FA.FfBits.Radio != 0: + states: list[str] = [] + retval[key][NameObject("/_States_")] = ArrayObject(states) + for k in obj.get(FA.Kids, {}): + k = k.get_object() + if "/AP" not in k: + raise PdfReadError(f"Button field kid missing /AP: {k!r}") + normal = self._normal_appearance(k["/AP"]) + for s in list(normal.keys()): + if s not in states: + states.append(s) + retval[key][NameObject("/_States_")] = ArrayObject(states) + if ( + obj.get(FA.Ff, 0) & FA.FfBits.NoToggleToOff != 0 + and "/Off" in retval[key]["/_States_"] + ): + del retval[key]["/_States_"][retval[key]["/_States_"].index("/Off")] + # at last for order + self._check_kids(field, retval, fileobj, stack) + + def _check_kids( + self, + tree: Union[TreeObject, DictionaryObject], + retval: Any, + fileobj: Any, + stack: list[PdfObject], + ) -> None: + if tree in stack: + logger_warning( + "%(field_name)s already parsed", + source=__name__, + field_name=self._get_qualified_field_name(parent=tree), + ) + return + stack.append(tree) + if PagesAttributes.KIDS in tree: + # recurse down the tree + for kid in tree[PagesAttributes.KIDS]: # type: ignore[attr-defined] + kid = kid.get_object() + self.get_fields(kid, retval, fileobj, stack) + + def _write_field(self, fileobj: Any, field: Any, field_attributes: Any) -> None: + field_attributes_tuple = FA.attributes() + field_attributes_tuple = ( + field_attributes_tuple + CheckboxRadioButtonAttributes.attributes() + ) + + for attr in field_attributes_tuple: + if attr in ( + FA.Kids, + FA.AA, + ): + continue + attr_name = field_attributes[attr] + try: + if attr == FA.FT: + # Make the field type value clearer + types = { + "/Btn": "Button", + "/Tx": "Text", + "/Ch": "Choice", + "/Sig": "Signature", + } + if field[attr] in types: + fileobj.write(f"{attr_name}: {types[field[attr]]}\n") + elif attr == FA.Parent: + # Let's just write the name of the parent + try: + name = field[attr][FA.TM] + except KeyError: + name = field[attr][FA.T] + fileobj.write(f"{attr_name}: {name}\n") + else: + fileobj.write(f"{attr_name}: {field[attr]}\n") + except KeyError: + # Field attribute is N/A or unknown, so don't write anything + pass + + def get_form_text_fields(self, full_qualified_name: bool = False) -> dict[str, Any]: + """ + Retrieve form fields from the document with textual data. + + Args: + full_qualified_name: to get full name + + Returns: + A dictionary. The key is the name of the form field, + the value is the content of the field. + + If the document contains multiple form fields with the same name, the + second and following will get the suffix .2, .3, ... + + """ + + def indexed_key(k: str, fields: dict[Any, Any]) -> str: + if k not in fields: + return k + return ( + k + + "." + + str(sum(1 for kk in fields if kk.startswith(k + ".")) + 2) + ) + + # Retrieve document form fields + form_fields = self.get_fields() + if form_fields is None: + return {} + ff = {} + for field, value in form_fields.items(): + if value.get("/FT") == "/Tx": + if full_qualified_name: + ff[field] = value.get("/V") + else: + ff[indexed_key(cast(str, value.get("/T", field)), ff)] = value.get("/V") + return ff + + def get_pages_showing_field( + self, field: Union[Field, PdfObject, IndirectObject] + ) -> list[PageObject]: + """ + Provides list of pages where the field is called. + + Args: + field: Field Object, PdfObject or IndirectObject referencing a Field + + Returns: + List of pages: + - Empty list: + The field has no widgets attached + (either hidden field or ancestor field). + - Single page list: + Page where the widget is present + (most common). + - Multi-page list: + Field with multiple kids widgets + (example: radio buttons, field repeated on multiple pages). + + """ + try: + # to cope with all types + field = cast(DictionaryObject, field.indirect_reference.get_object()) # type: ignore[union-attr] + except Exception as exc: + raise ValueError("Field type is invalid") from exc + if is_null_or_none(field.get_inherited(key="/FT", default=None)): + raise ValueError("Field is not valid") + ret = [] + if field.get("/Subtype", "") == "/Widget": + if "/P" in field: + ret = [field["/P"].get_object()] + else: + ret = [ + p + for p in self.pages + if field.indirect_reference in p.get("/Annots", "") + ] + else: + kids = field.get("/Kids", ()) + for k in kids: + k = k.get_object() + if (k.get("/Subtype", "") == "/Widget") and ("/T" not in k): + # Kid that is just a widget, not a field: + if "/P" in k: + ret += [k["/P"].get_object()] + else: + ret += [ + p + for p in self.pages + if k.indirect_reference in p.get("/Annots", "") + ] + return [ + x + if isinstance(x, PageObject) + else (self.pages[self._get_page_number_by_indirect(x.indirect_reference)]) # type: ignore[index, union-attr] + for x in ret + ] + + @property + def open_destination( + self, + ) -> Union[Destination, TextStringObject, ByteStringObject, None]: + """ + Property to access the opening destination (``/OpenAction`` entry in + the PDF catalog). It returns ``None`` if the entry does not exist + or is not set. + + Raises: + Exception: If a destination is invalid. + + """ + if "/OpenAction" not in self.root_object: + return None + oa: Any = self.root_object["/OpenAction"] + if isinstance(oa, bytes): # pragma: no cover + oa = oa.decode() + if isinstance(oa, str): + return create_string_object(oa) + if isinstance(oa, ArrayObject): + try: + page, typ, *array = oa + fit = Fit(typ, tuple(array)) + return Destination("OpenAction", page, fit) + except Exception as exc: + raise Exception(f"Invalid Destination {oa}: {exc}") + else: + return None + + @open_destination.setter + def open_destination(self, dest: Union[str, Destination, PageObject, None]) -> None: + raise NotImplementedError("No setter for open_destination") + + @property + def outline(self) -> OutlineType: + """ + Read-only property for the outline present in the document + (i.e., a collection of 'outline items' which are also known as + 'bookmarks'). + """ + return self._get_outline() + + def _get_outline( + self, + *, + node: Optional[DictionaryObject] = None, + outline: Optional[Any] = None, + visited: Optional[set[int]] = None, + depth: int = 0, + traversal_state: Optional[_TraversalState] = None + ) -> OutlineType: + if traversal_state is None: + traversal_state = _TraversalState() + + if outline is None: + outline = [] + catalog = self.root_object + + # get the outline dictionary and named destinations + if Core.OUTLINES in catalog: + lines = catalog[Core.OUTLINES].get_object() + + if isinstance(lines, NullObject): + return outline + + if not isinstance(lines, DictionaryObject): + logger_warning( + "Outlines are not a dictionary: %(lines)s", + source=__name__, + lines=lines, + ) + return outline + + # §12.3.3 Document outline, entries in the outline dictionary + if "/First" in lines: + node = cast(DictionaryObject, lines["/First"]) + self._named_destinations = self._get_named_destinations() + + if node is None: + return outline + + configuration = get_configuration() + if depth > configuration.outline_maximum_depth: + raise LimitReachedError(f"Maximum outline depth reached: {depth} > {configuration.outline_maximum_depth}.") + + # see if there are any more outline items + if visited is None: + visited = set() + while True: + node_id = id(node) + if node_id in visited: + logger_warning("Detected cycle in outline structure for %(node)s", source=__name__, node=node) + break + visited.add(node_id) + traversal_state.entry_count += 1 + if traversal_state.entry_count > configuration.outline_maximum_entries: + raise LimitReachedError( + f"Maximum outline entry limit reached: " + f"{traversal_state.entry_count} > {configuration.outline_maximum_entries}." + ) + + if not isinstance(node, DictionaryObject): + logger_warning( + "Outline node is not a dictionary: %(node)s", + source=__name__, + node=node, + ) + break + + outline_obj = self._build_outline_item(node) + if outline_obj: + outline.append(outline_obj) + + # check for sub-outline + if "/First" in node: + sub_outline: list[Any] = [] + # Pass a copy to allow multiple outer entries to reference the same inner one. + inner_visited = visited.copy() + self._get_outline( + node=cast(DictionaryObject, node["/First"]), + outline=sub_outline, + visited=inner_visited, + depth=depth + 1, + traversal_state=traversal_state, + ) + if sub_outline: + outline.append(sub_outline) + + if "/Next" not in node: + break + node = cast(DictionaryObject, node["/Next"]) + + return outline + + @property + def threads(self) -> Optional[ArrayObject]: + """ + Read-only property for the list of threads. + + See §12.4.3 from the PDF 1.7 or 2.0 specification. + + It is an array of dictionaries with "/F" (the first bead in the thread) + and "/I" (a thread information dictionary containing information about + the thread, such as its title, author, and creation date) properties or + None if there are no articles. + + Since PDF 2.0 it can also contain an indirect reference to a metadata + stream containing information about the thread, such as its title, + author, and creation date. + """ + catalog = self.root_object + if Core.THREADS in catalog: + return cast("ArrayObject", catalog[Core.THREADS]) + return None + + @abstractmethod + def _get_page_number_by_indirect( + self, indirect_reference: Union[int, NullObject, IndirectObject, None] + ) -> Optional[int]: + ... # pragma: no cover + + def get_page_number(self, page: PageObject) -> Optional[int]: + """ + Retrieve page number of a given PageObject. + + Args: + page: The page to get page number. Should be + an instance of :class:`PageObject` + + Returns: + The page number or None if page is not found + + """ + return self._get_page_number_by_indirect(page.indirect_reference) + + def get_destination_page_number(self, destination: Destination) -> Optional[int]: + """ + Retrieve page number of a given Destination object. + + Args: + destination: The destination to get page number. + + Returns: + The page number or None if page is not found + + """ + return self._get_page_number_by_indirect(destination.page) + + def _build_destination( + self, + title: Union[str, bytes], + array: Optional[ArrayObject], + ) -> Destination: + page, typ = None, None + # A valid destination is an array of at least a page and a fit type. + # Anything else (a name, a bare number, a NullObject, None, ...) cannot + # be unpacked below, so treat it as a missing destination. + if not isinstance(array, ArrayObject) or len(array) < 2: + page = NullObject() + return Destination(title, page, Fit.fit()) + page, typ, *array = array # type: ignore[assignment] + try: + return Destination(title, page, Fit(fit_type=typ, fit_args=array)) + except PdfReadError: + logger_warning("Unknown destination: %(title)r %(array)s", source=__name__, title=title, array=array) + if self.strict: + raise + # create a link to first Page + tmp = self.pages[0].indirect_reference + indirect_reference = NullObject() if tmp is None else tmp + return Destination(title, indirect_reference, Fit.fit()) + + def _build_outline_item(self, node: DictionaryObject) -> Optional[Destination]: + dest, title, outline_item = None, None, None + + # title required for valid outline + # §12.3.3, entries in an outline item dictionary + try: + title = cast("str", node["/Title"]) + except KeyError: + if self.strict: + raise PdfReadError(f"Outline Entry Missing /Title attribute: {node!r}") + title = "" + + if "/A" in node: + # Action, PDF 1.7 and PDF 2.0 §12.6 (only type GoTo supported) + action = cast(DictionaryObject, node["/A"]) + action_type = cast(NameObject, action[GoToActionArguments.S]) + if action_type == "/GoTo": + if GoToActionArguments.D in action: + dest = action[GoToActionArguments.D] + elif self.strict: + raise PdfReadError(f"Outline Action Missing /D attribute: {node!r}") + elif "/Dest" in node: + # Destination, PDF 1.7 and PDF 2.0 §12.3.2 + dest = node["/Dest"] + # if array was referenced in another object, will be a dict w/ key "/D" + if isinstance(dest, DictionaryObject) and "/D" in dest: + dest = dest["/D"] + + if isinstance(dest, ArrayObject): + outline_item = self._build_destination(title, dest) + elif isinstance(dest, str): + # named destination, addresses NameObject Issue #193 + # TODO: Keep named destination instead of replacing it? + try: + outline_item = self._build_destination( + title, self._named_destinations[dest].dest_array + ) + except KeyError: + # named destination not found in Name Dict + outline_item = self._build_destination(title, None) + elif dest is None: + # outline item not required to have destination or action + # PDFv1.7 Table 153 + outline_item = self._build_destination(title, dest) + else: + if self.strict: + raise PdfReadError(f"Unexpected destination {dest!r}") + logger_warning( + "Removed unexpected destination %(dest)r from destination", + source=__name__, + dest=dest, + ) + outline_item = self._build_destination(title, None) + + # if outline item created, add color, format, and child count if present + if outline_item: + if "/C" in node: + # Color of outline item font in (R, G, B) with values ranging 0.0-1.0 + color = node["/C"] + if isinstance(color, list): + outline_item[NameObject("/C")] = ArrayObject(FloatObject(c) for c in color) + else: + logger_warning( + "Ignoring non-array outline color %(color)r", + source=__name__, + color=color, + ) + if "/F" in node: + # specifies style characteristics bold and/or italic + # with 1=italic, 2=bold, 3=both + outline_item[NameObject("/F")] = node["/F"] + if "/Count" in node: + # absolute value = num. visible children + # with positive = open/unfolded, negative = closed/folded + outline_item[NameObject("/Count")] = node["/Count"] + # if count is 0 we will consider it as open (to have available is_open) + outline_item[NameObject("/%is_open%")] = BooleanObject( + node.get("/Count", 0) >= 0 + ) + outline_item.node = node + try: + outline_item.indirect_reference = node.indirect_reference + except AttributeError: + pass + return outline_item + + @property + def pages(self) -> Sequence[PageObject]: + """ + Property that emulates a list of :class:`PageObject`. + This property allows to get a page or a range of pages. + + The returned object supports indexing, slicing, ``len()``, iteration and + (for PdfWriter) ``del``, but it is not a :class:`list` - pages are looked + up on demand rather than materialised up front, so list-only operations + such as ``append()`` or concatenation with ``+`` are not available. + + Note: + For PdfWriter only: Provides the capability to remove a page/range of + page from the list (using the del operator). Remember: Only the page + entry is removed, as the objects beneath can be used elsewhere. A + solution to completely remove them - if they are not used anywhere - is + to write to a buffer/temporary file and then load it into a new + PdfWriter. + + """ + return _VirtualList(self.get_num_pages, self.get_page) + + @property + def page_labels(self) -> list[str]: + """ + A list of labels for the pages in this document. + + This property is read-only. The labels are in the order that the pages + appear in the document. + """ + return [page_index2page_label(self, i) for i in range(len(self.pages))] + + @property + def page_layout(self) -> Optional[str]: + """ + Get the page layout currently being used. + + .. list-table:: Valid ``layout`` values + :widths: 50 200 + + * - /NoLayout + - Layout explicitly not specified + * - /SinglePage + - Show one page at a time + * - /OneColumn + - Show one column at a time + * - /TwoColumnLeft + - Show pages in two columns, odd-numbered pages on the left + * - /TwoColumnRight + - Show pages in two columns, odd-numbered pages on the right + * - /TwoPageLeft + - Show two pages at a time, odd-numbered pages on the left + * - /TwoPageRight + - Show two pages at a time, odd-numbered pages on the right + """ + try: + return cast(NameObject, self.root_object[CA.PAGE_LAYOUT]) + except KeyError: + return None + + @property + def page_mode(self) -> Optional[PagemodeType]: + """ + Get the page mode currently being used. + + .. list-table:: Valid ``mode`` values + :widths: 50 200 + + * - /UseNone + - Do not show outline or thumbnails panels + * - /UseOutlines + - Show outline (aka bookmarks) panel + * - /UseThumbs + - Show page thumbnails panel + * - /FullScreen + - Fullscreen view + * - /UseOC + - Show Optional Content Group (OCG) panel + * - /UseAttachments + - Show attachments panel + """ + try: + return self.root_object["/PageMode"] # type: ignore[return-value] + except KeyError: + return None + + def _flatten( + self, + list_only: bool = False, + pages: Union[DictionaryObject, PageObject, None] = None, + inherit: Optional[dict[str, Any]] = None, + indirect_reference: Optional[IndirectObject] = None, + visited: Optional[set[int]] = None, + depth: int = 0, + traversal_state: Optional[_TraversalState] = None, + ) -> None: + """ + Process the document pages to ease searching. + + Attributes of a page may inherit from ancestor nodes + in the page tree. Flattening means moving + any inheritance data into descendant nodes, + effectively removing the inheritance dependency. + + Note: It is distinct from another use of "flattening" applied to PDFs. + Flattening a PDF also means combining all the contents into one single layer + and making the file less editable. + + Args: + list_only: Will only list the pages within _flatten_pages. + pages: + inherit: + indirect_reference: Used recursively to flatten the /Pages object. + visited: Set of id() values on the active page-tree traversal path. + Detects multi-hop cycles such as A→B→C→A that the single-parent + check misses. + depth: Current page-tree traversal depth. + traversal_state: State shared across the complete traversal. + + """ + if inherit is None: + inherit = {} + if visited is None: + visited = set() + if traversal_state is None: + traversal_state = _TraversalState() + configuration = get_configuration() + if depth > configuration.page_tree_maximum_depth: + raise LimitReachedError( + f"Maximum page tree depth reached: {depth} > {configuration.page_tree_maximum_depth}." + ) + if is_null_or_none(pages): + # Fix issue 327: set flattened_pages attribute only for + # decrypted file + catalog = self.root_object + pages = catalog.get("/Pages").get_object() # type: ignore[union-attr] + if not isinstance(pages, DictionaryObject): + raise PdfReadError("Invalid object in /Pages") + self.flattened_pages = [] + assert pages is not None, "mypy" + + # Get the current node_type + if PagesAttributes.TYPE in pages: + node_type = cast(str, pages[PagesAttributes.TYPE]) + # if the page tree node has no /Type, consider as a page if /Kids is also missing + elif PagesAttributes.KIDS not in pages: + # Without /Type, only accept it as a page if it carries a structural page key. + if self.strict and not any(key in pages for key in (PG.CONTENTS, PG.MEDIABOX, PG.PARENT)): + raise PdfReadError(f"Non-page object reached through /Kids: {pages!r}") + node_type = "/Page" + else: + node_type = "/Pages" + + # Flatten that type of node + if node_type == "/Pages": + self._flatten_page_tree_node(pages, list_only, inherit, visited, depth, traversal_state) + elif node_type == "/Page": + self._flatten_leaf_page(pages, list_only, inherit, indirect_reference) + + def _flatten_page_tree_node( + self, + pages: DictionaryObject, + list_only: bool, + inherit: dict[str, Any], + visited: set[int], + depth: int, + traversal_state: _TraversalState, + ) -> None: + """ + Handle an intermediate ``/Pages`` node: propagate its inheritable + attributes into ``inherit`` and recurse into each entry of ``/Kids``. + + Cyclic references and the configured page-tree entry limit are enforced here. + """ + inheritable_page_attributes = ( + NameObject(PG.RESOURCES), + NameObject(PG.MEDIABOX), + NameObject(PG.CROPBOX), + NameObject(PG.ROTATE), + ) + for attr in inheritable_page_attributes: + if attr in pages: + inherit[attr] = pages[attr] + + kids = pages.get(PagesAttributes.KIDS, ArrayObject()).get_object() + if isinstance(kids, NullObject): + kids = ArrayObject() + elif not isinstance(kids, ArrayObject): + raise PdfReadError(f"Expected /Kids to be an array, got {type(kids).__name__}.") + + configuration = get_configuration() + pages_reference = getattr(pages, "indirect_reference", object()) + for page in kids: + if getattr(page, "indirect_reference", object()) == pages_reference: + raise PdfReadError("Detected cyclic page references.") + + additional_arguments = {} + if isinstance(page, IndirectObject): + additional_arguments["indirect_reference"] = page + obj = page.get_object() + if not is_null_or_none(obj) and not isinstance(obj, DictionaryObject): + logger_warning( + "Ignoring page tree entry that is not a dictionary: %(entry)s", + source=__name__, + entry=obj, + ) + continue + if not obj: + # damaged file may have invalid child in /Pages + continue + obj_id = id(obj) + if obj_id in visited: + raise PdfReadError("Detected cyclic page references.") + traversal_state.entry_count += 1 + if traversal_state.entry_count > configuration.page_tree_maximum_entries: + raise LimitReachedError( + "Maximum page tree entry limit reached: " + f"{traversal_state.entry_count} > {configuration.page_tree_maximum_entries}." + ) + visited.add(obj_id) + try: + self._flatten( + list_only, + cast(DictionaryObject, obj), + inherit.copy(), + visited=visited, + depth=depth + 1, + traversal_state=traversal_state, + **additional_arguments, + ) + finally: + visited.remove(obj_id) + + def _flatten_leaf_page( + self, + page: DictionaryObject, + list_only: bool, + inherit: dict[str, Any], + indirect_reference: Optional[IndirectObject], + ) -> None: + """Build the :class:`PageObject` for a leaf ``/Page`` node and append it to ``flattened_pages``.""" + page_obj = PageObject(self, indirect_reference) + if not list_only: + page_obj.update(page) + for attr, value in inherit.items(): + # if the page has its own value, it does not inherit the parent's value + if attr not in page_obj: + page_obj[attr] = value + + # TODO: Could flattened_pages be None at this point? + self.flattened_pages.append(page_obj) # type: ignore[union-attr] + + def remove_page( + self, + page: Union[int, PageObject, IndirectObject], + clean: bool = False, + ) -> None: + """ + Remove page from pages list. + + Args: + page: + * :class:`int`: Page number to be removed. + * :class:`~pypdf._page.PageObject`: page to be removed. If the page appears many times + only the first one will be removed. + * :class:`~pypdf.generic.IndirectObject`: Reference to page to be removed. + + clean: replace PageObject with NullObject to prevent annotations + or destinations to reference a detached page. + + """ + if self.flattened_pages is None: + self._flatten(self._readonly) + assert self.flattened_pages is not None + if isinstance(page, IndirectObject): + p = page.get_object() + if not isinstance(p, PageObject): + logger_warning("IndirectObject is not referencing a page", source=__name__) + return + page = p + + if not isinstance(page, int): + try: + page = self.flattened_pages.index(page) + except ValueError: + logger_warning("Cannot find page in pages", source=__name__) + return + if not (0 <= page < len(self.flattened_pages)): + logger_warning("Page number is out of range", source=__name__) + return + + ind = self.pages[page].indirect_reference + # `pages` is typed as a Sequence because it is not a list, but the + # concrete _VirtualList does implement deletion. + del cast(_VirtualList, self.pages)[page] + if clean and ind is not None: + self._replace_object(ind, NullObject()) + + def _get_indirect_object(self, num: int, gen: int) -> Optional[PdfObject]: + """ + Used to ease development. + + This is equivalent to generic.IndirectObject(num,gen,self).get_object() + + Args: + num: The object number of the indirect object. + gen: The generation number of the indirect object. + + Returns: + A PdfObject + + """ + return IndirectObject(num, gen, self).get_object() + + def decode_permissions( + self, permissions_code: int + ) -> NoReturn: # pragma: no cover + """Take the permissions as an integer, return the allowed access.""" + deprecation_with_replacement( + old_name="decode_permissions", + new_name="user_access_permissions", + removed_in="5.0.0", + ) + + @property + def user_access_permissions(self) -> Optional[UserAccessPermissions]: + """ + Get the user access permissions for encrypted documents. + Returns None if not encrypted. + + .. warning:: + + For AES-256 encrypted documents (R=5/R=6), the returned + permissions are derived from the ``/P`` field, which is + only trustworthy if the ``/Perms`` integrity check passed. + Check :attr:`are_permissions_valid` to verify. + """ + if self._encryption is None: + return None + return UserAccessPermissions(self._encryption.P) + + @property + def are_permissions_valid(self) -> Optional[bool]: + """ + Whether the ``/Perms`` integrity check passed for this document. + + For AES-256 encrypted documents (R=5/R=6), the ``/Perms`` field + is an encrypted copy of the permissions that can be verified + independently. Returns ``False`` if this check fails (the ``/P`` + permissions may have been tampered with). + + Returns ``None`` if the document is not encrypted or has not yet + been decrypted via :meth:`decrypt()`. + Returns ``True`` for non-AES-256 encryption (no ``/Perms`` to check). + """ + if self._encryption is None: + return None + if not self._encryption.is_decrypted(): + return None + return self._encryption._are_permissions_valid + + @property + @abstractmethod + def is_encrypted(self) -> bool: + """ + Read-only boolean property showing whether this PDF file is encrypted. + + Note that this property, if true, will remain true even after the + :meth:`decrypt()` method is called. + """ + ... # pragma: no cover + + @property + def xfa(self) -> Optional[dict[str, Any]]: + retval: dict[str, Any] = {} + catalog = self.root_object + + if "/AcroForm" not in catalog or not catalog["/AcroForm"]: + return None + + tree = cast(TreeObject, catalog["/AcroForm"]) + + if "/XFA" in tree: + fields = tree["/XFA"].get_object() + if not isinstance(fields, ArrayObject): + logger_warning( + "XFA entry is not an array: %(fields)s", + source=__name__, + fields=fields, + ) + return retval + i = iter(fields) + for f in i: + tag = f + f = next(i, None) + if isinstance(f, IndirectObject): + field = cast(Optional[EncodedStreamObject], f.get_object()) + if field: + es = _decompress_with_limit(field._data) + retval[tag] = es + return retval + + @property + def attachments(self) -> Mapping[str, list[bytes]]: + """Mapping of attachment filenames to their content.""" + entries: dict[str, list[EmbeddedFile]] = {} + + for entry in self.attachment_list: + for name in entry._names: + entries.setdefault(name, []).append(entry) + + return LazyDict( + { + name: (self._get_attachment_contents, attachment_entries) + for name, attachment_entries in entries.items() + } + ) + + @classmethod + def _get_attachment_contents(cls, entries: list[EmbeddedFile]) -> list[bytes]: + return [entry.content for entry in entries] + + @property + def attachment_list(self) -> Iterator[EmbeddedFile]: + """Iterable of attachment objects.""" + yield from EmbeddedFile._load(self.root_object, strict=self.strict) + + @abstractmethod + def _repr_mimebundle_( + self, + include: Union[Iterable[str], None] = None, + exclude: Union[Iterable[str], None] = None, + ) -> dict[str, Any]: + """ + Integration into Jupyter Notebooks. + + This method returns a dictionary that maps a mime-type to its + representation. + + .. seealso:: + + https://ipython.readthedocs.io/en/stable/config/integrating.html + """ + ... # pragma: no cover + + +class LazyDict(Mapping[Any, Any]): + def __init__(self, *args: Any, **kwargs: Any) -> None: + self._raw_dict = dict(*args, **kwargs) + + def __getitem__(self, key: str) -> Any: + func, arg = self._raw_dict.__getitem__(key) + return func(arg) + + def __iter__(self) -> Iterator[Any]: + return iter(self._raw_dict) + + def __len__(self) -> int: + return len(self._raw_dict) + + def __str__(self) -> str: + return f"LazyDict(keys={list(self.keys())})" diff --git a/app/vendor/pypdf/_encryption.py b/app/vendor/pypdf/_encryption.py new file mode 100644 index 0000000..08a71b3 --- /dev/null +++ b/app/vendor/pypdf/_encryption.py @@ -0,0 +1,1305 @@ +# Copyright (c) 2022, exiledkingcc +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import hashlib +import secrets +import stringprep +import struct +import unicodedata +from enum import Enum, IntEnum +from typing import Any, Optional, Union, cast + +from pypdf._crypt_providers import ( + CryptAES, + CryptBase, + CryptIdentity, + CryptRC4, + aes_cbc_decrypt, + aes_cbc_encrypt, + aes_ecb_decrypt, + aes_ecb_encrypt, + rc4_decrypt, + rc4_encrypt, +) + +from ._utils import logger_warning +from .generic import ( + ArrayObject, + ByteStringObject, + DictionaryObject, + NameObject, + NumberObject, + PdfObject, + StreamObject, + TextStringObject, + create_string_object, +) + + +class CryptFilter: + def __init__( + self, + stm_crypt: CryptBase, + str_crypt: CryptBase, + ef_crypt: CryptBase, + ) -> None: + self.stm_crypt = stm_crypt + self.str_crypt = str_crypt + self.ef_crypt = ef_crypt + + def encrypt_object(self, obj: PdfObject) -> PdfObject: + if isinstance(obj, ByteStringObject): + data = self.str_crypt.encrypt(obj.original_bytes) + obj = ByteStringObject(data) + elif isinstance(obj, TextStringObject): + data = self.str_crypt.encrypt(obj.get_encoded_bytes()) + obj = ByteStringObject(data) + elif isinstance(obj, StreamObject): + obj2 = StreamObject() + obj2.update(obj) + obj2.set_data(self.stm_crypt.encrypt(obj._data)) + for key, value in obj.items(): # Don't forget the Stream dict. + obj2[key] = self.encrypt_object(value) + obj = obj2 + elif isinstance(obj, DictionaryObject): + obj2 = DictionaryObject() # type: ignore[assignment] + for key, value in obj.items(): + obj2[key] = self.encrypt_object(value) + obj = obj2 + elif isinstance(obj, ArrayObject): + obj = ArrayObject(self.encrypt_object(x) for x in obj) + return obj + + def decrypt_object(self, obj: PdfObject, *, strict: bool = True) -> PdfObject: + if isinstance(obj, (ByteStringObject, TextStringObject)): + data = self.str_crypt.decrypt(obj.original_bytes, strict=strict) + obj = create_string_object(data) + elif isinstance(obj, StreamObject): + obj._data = self.stm_crypt.decrypt(obj._data, strict=strict) + for key, value in obj.items(): # Don't forget the Stream dict. + obj[key] = self.decrypt_object(value, strict=strict) + elif isinstance(obj, DictionaryObject): + for key, value in obj.items(): + obj[key] = self.decrypt_object(value, strict=strict) + elif isinstance(obj, ArrayObject): + for i in range(len(obj)): + obj[i] = self.decrypt_object(obj[i], strict=strict) + return obj + + +_PADDING = ( + b"\x28\xbf\x4e\x5e\x4e\x75\x8a\x41\x64\x00\x4e\x56\xff\xfa\x01\x08" + b"\x2e\x2e\x00\xb6\xd0\x68\x3e\x80\x2f\x0c\xa9\xfe\x64\x53\x69\x7a" +) + + +def _padding(data: bytes) -> bytes: + return (data + _PADDING)[:32] + + +# Module-level constant for SASLprep prohibited character checks (RFC 4013 §2.3) +_SASLPREP_PROHIBITED_CHECKS = ( + (stringprep.in_table_c12, "non-ASCII space character"), + (stringprep.in_table_c21, "ASCII control character"), + (stringprep.in_table_c22, "non-ASCII control character"), + (stringprep.in_table_c3, "private use character"), + (stringprep.in_table_c4, "non-character code point"), + (stringprep.in_table_c5, "surrogate code point"), + (stringprep.in_table_c6, "inappropriate for plain text"), + (stringprep.in_table_c7, "inappropriate for canonical representation"), + (stringprep.in_table_c8, "change display properties/deprecated"), + (stringprep.in_table_c9, "tagging character"), + (stringprep.in_table_a1, "unassigned code point"), +) + + +class AlgV4: + @staticmethod + def compute_key( + password: bytes, + rev: int, + key_size: int, + o_entry: bytes, + P: int, + id1_entry: bytes, + metadata_encrypted: bool, + ) -> bytes: + """ + Algorithm 2: Computing an encryption key. + + a) Pad or truncate the password string to exactly 32 bytes. If the + password string is more than 32 bytes long, + use only its first 32 bytes; if it is less than 32 bytes long, pad it + by appending the required number of + additional bytes from the beginning of the following padding string: + < 28 BF 4E 5E 4E 75 8A 41 64 00 4E 56 FF FA 01 08 + 2E 2E 00 B6 D0 68 3E 80 2F 0C A9 FE 64 53 69 7A > + That is, if the password string is n bytes long, append + the first 32 - n bytes of the padding string to the end + of the password string. If the password string is empty + (zero-length), meaning there is no user password, + substitute the entire padding string in its place. + b) Initialize the MD5 hash function and pass the result of step (a) + as input to this function. + c) Pass the value of the encryption dictionary’s O entry to the + MD5 hash function. ("Algorithm 3: Computing + the encryption dictionary’s O (owner password) value" shows how the + O value is computed.) + d) Convert the integer value of the P entry to a 32-bit unsigned binary + number and pass these bytes to the + MD5 hash function, low-order byte first. + e) Pass the first element of the file’s file identifier array (the value + of the ID entry in the document’s trailer + dictionary; see Table 15) to the MD5 hash function. + f) (Security handlers of revision 4 or greater) If document metadata is + not being encrypted, pass 4 bytes with + the value 0xFFFFFFFF to the MD5 hash function. + g) Finish the hash. + h) (Security handlers of revision 3 or greater) Do the following + 50 times: Take the output from the previous + MD5 hash and pass the first n bytes of the output as input into a new + MD5 hash, where n is the number of + bytes of the encryption key as defined by the value of the encryption + dictionary’s Length entry. + i) Set the encryption key to the first n bytes of the output from the + final MD5 hash, where n shall always be 5 + for security handlers of revision 2 but, for security handlers of + revision 3 or greater, shall depend on the + value of the encryption dictionary’s Length entry. + + Args: + password: The encryption secret as a bytes-string + rev: The encryption revision (see PDF standard) + key_size: The size of the key in bytes + o_entry: The owner entry + P: A set of flags specifying which operations shall be permitted + when the document is opened with user access. If bit 2 is set to 1, + all other bits are ignored and all operations are permitted. + If bit 2 is set to 0, permission for operations are based on the + values of the remaining flags defined in Table 24. + id1_entry: + metadata_encrypted: A boolean indicating if the metadata is encrypted. + + Returns: + The u_hash digest of length key_size + + """ + a = _padding(password) + u_hash = hashlib.md5(a) + u_hash.update(o_entry) + u_hash.update(struct.pack("= 4 and not metadata_encrypted: + u_hash.update(b"\xff\xff\xff\xff") + u_hash_digest = u_hash.digest() + length = key_size // 8 + if rev >= 3: + for _ in range(50): + u_hash_digest = hashlib.md5(u_hash_digest[:length]).digest() + return u_hash_digest[:length] + + @staticmethod + def compute_O_value_key(owner_password: bytes, rev: int, key_size: int) -> bytes: + """ + Algorithm 3: Computing the encryption dictionary’s O (owner password) value. + + a) Pad or truncate the owner password string as described in step (a) + of "Algorithm 2: Computing an encryption key". + If there is no owner password, use the user password instead. + b) Initialize the MD5 hash function and pass the result of step (a) as + input to this function. + c) (Security handlers of revision 3 or greater) Do the following 50 times: + Take the output from the previous + MD5 hash and pass it as input into a new MD5 hash. + d) Create an RC4 encryption key using the first n bytes of the output + from the final MD5 hash, where n shall + always be 5 for security handlers of revision 2 but, for security + handlers of revision 3 or greater, shall + depend on the value of the encryption dictionary’s Length entry. + e) Pad or truncate the user password string as described in step (a) of + "Algorithm 2: Computing an encryption key". + f) Encrypt the result of step (e), using an RC4 encryption function with + the encryption key obtained in step (d). + g) (Security handlers of revision 3 or greater) Do the following 19 times: + Take the output from the previous + invocation of the RC4 function and pass it as input to a new + invocation of the function; use an encryption + key generated by taking each byte of the encryption key obtained in + step (d) and performing an XOR + (exclusive or) operation between that byte and the single-byte value + of the iteration counter (from 1 to 19). + h) Store the output from the final invocation of the RC4 function as + the value of the O entry in the encryption dictionary. + + Args: + owner_password: + rev: The encryption revision (see PDF standard) + key_size: The size of the key in bytes + + Returns: + The RC4 key + + """ + a = _padding(owner_password) + o_hash_digest = hashlib.md5(a).digest() + + if rev >= 3: + for _ in range(50): + o_hash_digest = hashlib.md5(o_hash_digest).digest() + + return o_hash_digest[: key_size // 8] + + @staticmethod + def compute_O_value(rc4_key: bytes, user_password: bytes, rev: int) -> bytes: + """ + See :func:`compute_O_value_key`. + + Args: + rc4_key: + user_password: + rev: The encryption revision (see PDF standard) + + Returns: + The RC4 encrypted + + """ + a = _padding(user_password) + rc4_enc = rc4_encrypt(rc4_key, a) + if rev >= 3: + for i in range(1, 20): + key = bytes(x ^ i for x in rc4_key) + rc4_enc = rc4_encrypt(key, rc4_enc) + return rc4_enc + + @staticmethod + def compute_U_value(key: bytes, rev: int, id1_entry: bytes) -> bytes: + """ + Algorithm 4: Computing the encryption dictionary’s U (user password) value. + + (Security handlers of revision 2) + + a) Create an encryption key based on the user password string, as + described in "Algorithm 2: Computing an encryption key". + b) Encrypt the 32-byte padding string shown in step (a) of + "Algorithm 2: Computing an encryption key", using an RC4 encryption + function with the encryption key from the preceding step. + c) Store the result of step (b) as the value of the U entry in the + encryption dictionary. + + Args: + key: + rev: The encryption revision (see PDF standard) + id1_entry: + + Returns: + The value + + """ + if rev <= 2: + return rc4_encrypt(key, _PADDING) + + """ + Algorithm 5: Computing the encryption dictionary’s U (user password) value. + + (Security handlers of revision 3 or greater) + + a) Create an encryption key based on the user password string, as + described in "Algorithm 2: Computing an encryption key". + b) Initialize the MD5 hash function and pass the 32-byte padding string + shown in step (a) of "Algorithm 2: + Computing an encryption key" as input to this function. + c) Pass the first element of the file’s file identifier array (the value + of the ID entry in the document’s trailer + dictionary; see Table 15) to the hash function and finish the hash. + d) Encrypt the 16-byte result of the hash, using an RC4 encryption + function with the encryption key from step (a). + e) Do the following 19 times: Take the output from the previous + invocation of the RC4 function and pass it as input to a new + invocation of the function; use an encryption key generated by + taking each byte of the original encryption key obtained in + step (a) and performing an XOR (exclusive or) operation between that + byte and the single-byte value of the iteration counter (from 1 to 19). + f) Append 16 bytes of arbitrary padding to the output from the final + invocation of the RC4 function and store the 32-byte result as the + value of the U entry in the encryption dictionary. + """ + u_hash = hashlib.md5(_PADDING) + u_hash.update(id1_entry) + rc4_enc = rc4_encrypt(key, u_hash.digest()) + for i in range(1, 20): + rc4_key = bytes(x ^ i for x in key) + rc4_enc = rc4_encrypt(rc4_key, rc4_enc) + return _padding(rc4_enc) + + @staticmethod + def verify_user_password( + user_password: bytes, + rev: int, + key_size: int, + o_entry: bytes, + u_entry: bytes, + P: int, + id1_entry: bytes, + metadata_encrypted: bool, + ) -> bytes: + """ + Algorithm 6: Authenticating the user password. + + a) Perform all but the last step of "Algorithm 4: Computing the + encryption dictionary’s U (user password) value (Security handlers of + revision 2)" or "Algorithm 5: Computing the encryption dictionary’s U + (user password) value (Security handlers of revision 3 or greater)" + using the supplied password string. + b) If the result of step (a) is equal to the value of the encryption + dictionary’s U entry (comparing on the first 16 bytes in the case of + security handlers of revision 3 or greater), the password supplied is + the correct user password. The key obtained in step (a) (that is, in + the first step of "Algorithm 4: Computing the encryption + dictionary’s U (user password) value + (Security handlers of revision 2)" or + "Algorithm 5: Computing the encryption dictionary’s U (user password) + value (Security handlers of revision 3 or greater)") shall be used + to decrypt the document. + + Args: + user_password: The user password as a bytes stream + rev: The encryption revision (see PDF standard) + key_size: The size of the key in bytes + o_entry: The owner entry + u_entry: The user entry + P: A set of flags specifying which operations shall be permitted + when the document is opened with user access. If bit 2 is set to 1, + all other bits are ignored and all operations are permitted. + If bit 2 is set to 0, permission for operations are based on the + values of the remaining flags defined in Table 24. + id1_entry: + metadata_encrypted: A boolean indicating if the metadata is encrypted. + + Returns: + The key + + """ + key = AlgV4.compute_key( + user_password, rev, key_size, o_entry, P, id1_entry, metadata_encrypted + ) + u_value = AlgV4.compute_U_value(key, rev, id1_entry) + if rev >= 3: + u_value = u_value[:16] + u_entry = u_entry[:16] + if u_value != u_entry: + key = b"" + return key + + @staticmethod + def verify_owner_password( + owner_password: bytes, + rev: int, + key_size: int, + o_entry: bytes, + u_entry: bytes, + P: int, + id1_entry: bytes, + metadata_encrypted: bool, + ) -> bytes: + """ + Algorithm 7: Authenticating the owner password. + + a) Compute an encryption key from the supplied password string, as + described in steps (a) to (d) of + "Algorithm 3: Computing the encryption dictionary’s O (owner password) + value". + b) (Security handlers of revision 2 only) Decrypt the value of the + encryption dictionary’s O entry, using an RC4 + encryption function with the encryption key computed in step (a). + (Security handlers of revision 3 or greater) Do the following 20 times: + Decrypt the value of the encryption dictionary’s O entry (first iteration) + or the output from the previous iteration (all subsequent iterations), + using an RC4 encryption function with a different encryption key at + each iteration. The key shall be generated by taking the original key + (obtained in step (a)) and performing an XOR (exclusive or) operation + between each byte of the key and the single-byte value of the + iteration counter (from 19 to 0). + c) The result of step (b) purports to be the user password. + Authenticate this user password using + "Algorithm 6: Authenticating the user password". + If it is correct, the password supplied is the correct owner password. + + Args: + owner_password: + rev: The encryption revision (see PDF standard) + key_size: The size of the key in bytes + o_entry: The owner entry + u_entry: The user entry + P: A set of flags specifying which operations shall be permitted + when the document is opened with user access. If bit 2 is set to 1, + all other bits are ignored and all operations are permitted. + If bit 2 is set to 0, permission for operations are based on the + values of the remaining flags defined in Table 24. + id1_entry: + metadata_encrypted: A boolean indicating if the metadata is encrypted. + + Returns: + bytes + + """ + rc4_key = AlgV4.compute_O_value_key(owner_password, rev, key_size) + + if rev <= 2: + user_password = rc4_decrypt(rc4_key, o_entry) + else: + user_password = o_entry + for i in range(19, -1, -1): + key = bytes(x ^ i for x in rc4_key) + user_password = rc4_decrypt(key, user_password) + return AlgV4.verify_user_password( + user_password, + rev, + key_size, + o_entry, + u_entry, + P, + id1_entry, + metadata_encrypted, + ) + + +class AlgV5: + @staticmethod + def verify_owner_password( + R: int, password: bytes, o_value: bytes, oe_value: bytes, u_value: bytes + ) -> bytes: + """ + Algorithm 3.2a Computing an encryption key. + + To understand the algorithm below, it is necessary to treat the O and U + strings in the Encrypt dictionary as made up of three sections. + The first 32 bytes are a hash value (explained below). The next 8 bytes + are called the Validation Salt. The final 8 bytes are called the Key Salt. + + 1. The password string is generated from Unicode input by processing the + input string with the SASLprep (IETF RFC 4013) profile of + stringprep (IETF RFC 3454), and then converting to a UTF-8 + representation. + 2. Truncate the UTF-8 representation to 127 bytes if it is longer than + 127 bytes. + 3. Test the password against the owner key by computing the SHA-256 hash + of the UTF-8 password concatenated with the 8 bytes of owner + Validation Salt, concatenated with the 48-byte U string. If the + 32-byte result matches the first 32 bytes of the O string, this is + the owner password. + Compute an intermediate owner key by computing the SHA-256 hash of + the UTF-8 password concatenated with the 8 bytes of owner Key Salt, + concatenated with the 48-byte U string. The 32-byte result is the + key used to decrypt the 32-byte OE string using AES-256 in CBC mode + with no padding and an initialization vector of zero. + The 32-byte result is the file encryption key. + 4. Test the password against the user key by computing the SHA-256 hash + of the UTF-8 password concatenated with the 8 bytes of user + Validation Salt. If the 32 byte result matches the first 32 bytes of + the U string, this is the user password. + Compute an intermediate user key by computing the SHA-256 hash of the + UTF-8 password concatenated with the 8 bytes of user Key Salt. + The 32-byte result is the key used to decrypt the 32-byte + UE string using AES-256 in CBC mode with no padding and an + initialization vector of zero. The 32-byte result is the file + encryption key. + 5. Decrypt the 16-byte Perms string using AES-256 in ECB mode with an + initialization vector of zero and the file encryption key as the key. + Verify that bytes 9-11 of the result are the characters ‘a’, ‘d’, ‘b’. + Bytes 0-3 of the decrypted Perms entry, treated as a little-endian + integer, are the user permissions. + They should match the value in the P key. + + Args: + R: A number specifying which revision of the standard security + handler shall be used to interpret this dictionary + password: The owner password + o_value: A 32-byte string, based on both the owner and user passwords, + that shall be used in computing the encryption key and in + determining whether a valid owner password was entered + oe_value: + u_value: A 32-byte string, based on the user password, that shall be + used in determining whether to prompt the user for a password and, + if so, whether a valid user or owner password was entered. + + Returns: + The key + + """ + password = password[:127] + if ( + AlgV5.calculate_hash(R, password, o_value[32:40], u_value[:48]) + != o_value[:32] + ): + return b"" + iv = bytes(0 for _ in range(16)) + tmp_key = AlgV5.calculate_hash(R, password, o_value[40:48], u_value[:48]) + return aes_cbc_decrypt(tmp_key, iv, oe_value) + + @staticmethod + def verify_user_password( + R: int, password: bytes, u_value: bytes, ue_value: bytes + ) -> bytes: + """ + See :func:`verify_owner_password`. + + Args: + R: A number specifying which revision of the standard security + handler shall be used to interpret this dictionary + password: The user password + u_value: A 32-byte string, based on the user password, that shall be + used in determining whether to prompt the user for a password + and, if so, whether a valid user or owner password was entered. + ue_value: + + Returns: + bytes + + """ + password = password[:127] + if AlgV5.calculate_hash(R, password, u_value[32:40], b"") != u_value[:32]: + return b"" + iv = bytes(0 for _ in range(16)) + tmp_key = AlgV5.calculate_hash(R, password, u_value[40:48], b"") + return aes_cbc_decrypt(tmp_key, iv, ue_value) + + @staticmethod + def calculate_hash(R: int, password: bytes, salt: bytes, udata: bytes) -> bytes: + # https://github.com/qpdf/qpdf/blob/main/libqpdf/QPDF_encryption.cc + k = hashlib.sha256(password + salt + udata).digest() + if R < 6: + return k + count = 0 + while True: + count += 1 + k1 = password + k + udata + e = aes_cbc_encrypt(k[:16], k[16:32], k1 * 64) + hash_fn = ( + hashlib.sha256, + hashlib.sha384, + hashlib.sha512, + )[sum(e[:16]) % 3] + k = hash_fn(e).digest() + if count >= 64 and e[-1] <= count - 32: + break + return k[:32] + + @staticmethod + def verify_perms( + key: bytes, perms: bytes, p: int, metadata_encrypted: bool + ) -> bool: + """ + See :func:`verify_owner_password` and :func:`compute_perms_value`. + + Args: + key: The owner password + perms: + p: A set of flags specifying which operations shall be permitted + when the document is opened with user access. + If bit 2 is set to 1, all other bits are ignored and all + operations are permitted. + If bit 2 is set to 0, permission for operations are based on + the values of the remaining flags defined in Table 24. + metadata_encrypted: + + Returns: + A boolean + + """ + b8 = b"T" if metadata_encrypted else b"F" + p1 = struct.pack(" dict[Any, Any]: + user_password = user_password[:127] + owner_password = owner_password[:127] + u_value, ue_value = AlgV5.compute_U_value(R, user_password, key) + o_value, oe_value = AlgV5.compute_O_value(R, owner_password, key, u_value) + perms = AlgV5.compute_Perms_value(key, p, metadata_encrypted) + return { + "/U": u_value, + "/UE": ue_value, + "/O": o_value, + "/OE": oe_value, + "/Perms": perms, + } + + @staticmethod + def compute_U_value(R: int, password: bytes, key: bytes) -> tuple[bytes, bytes]: + """ + Algorithm 3.8 Computing the encryption dictionary’s U (user password) + and UE (user encryption key) values. + + 1. Generate 16 random bytes of data using a strong random number generator. + The first 8 bytes are the User Validation Salt. The second 8 bytes + are the User Key Salt. Compute the 32-byte SHA-256 hash of the + password concatenated with the User Validation Salt. The 48-byte + string consisting of the 32-byte hash followed by the User + Validation Salt followed by the User Key Salt is stored as the U key. + 2. Compute the 32-byte SHA-256 hash of the password concatenated with + the User Key Salt. Using this hash as the key, encrypt the file + encryption key using AES-256 in CBC mode with no padding and an + initialization vector of zero. The resulting 32-byte string is stored + as the UE key. + + Args: + R: + password: + key: + + Returns: + A tuple (u-value, ue value) + + """ + random_bytes = secrets.token_bytes(16) + val_salt = random_bytes[:8] + key_salt = random_bytes[8:] + u_value = AlgV5.calculate_hash(R, password, val_salt, b"") + val_salt + key_salt + + tmp_key = AlgV5.calculate_hash(R, password, key_salt, b"") + iv = bytes(0 for _ in range(16)) + ue_value = aes_cbc_encrypt(tmp_key, iv, key) + return u_value, ue_value + + @staticmethod + def compute_O_value( + R: int, password: bytes, key: bytes, u_value: bytes + ) -> tuple[bytes, bytes]: + """ + Algorithm 3.9 Computing the encryption dictionary’s O (owner password) + and OE (owner encryption key) values. + + 1. Generate 16 random bytes of data using a strong random number + generator. The first 8 bytes are the Owner Validation Salt. The + second 8 bytes are the Owner Key Salt. Compute the 32-byte SHA-256 + hash of the password concatenated with the Owner Validation Salt and + then concatenated with the 48-byte U string as generated in + Algorithm 3.8. The 48-byte string consisting of the 32-byte hash + followed by the Owner Validation Salt followed by the Owner Key Salt + is stored as the O key. + 2. Compute the 32-byte SHA-256 hash of the password concatenated with + the Owner Key Salt and then concatenated with the 48-byte U string as + generated in Algorithm 3.8. Using this hash as the key, + encrypt the file encryption key using AES-256 in CBC mode with + no padding and an initialization vector of zero. + The resulting 32-byte string is stored as the OE key. + + Args: + R: + password: + key: + u_value: A 32-byte string, based on the user password, that shall be + used in determining whether to prompt the user for a password + and, if so, whether a valid user or owner password was entered. + + Returns: + A tuple (O value, OE value) + + """ + random_bytes = secrets.token_bytes(16) + val_salt = random_bytes[:8] + key_salt = random_bytes[8:] + o_value = ( + AlgV5.calculate_hash(R, password, val_salt, u_value) + val_salt + key_salt + ) + tmp_key = AlgV5.calculate_hash(R, password, key_salt, u_value[:48]) + iv = bytes(0 for _ in range(16)) + oe_value = aes_cbc_encrypt(tmp_key, iv, key) + return o_value, oe_value + + @staticmethod + def compute_Perms_value(key: bytes, p: int, metadata_encrypted: bool) -> bytes: + """ + Algorithm 3.10 Computing the encryption dictionary’s Perms + (permissions) value. + + 1. Extend the permissions (contents of the P integer) to 64 bits by + setting the upper 32 bits to all 1’s. + (This allows for future extension without changing the format.) + 2. Record the 8 bytes of permission in the bytes 0-7 of the block, + low order byte first. + 3. Set byte 8 to the ASCII value ' T ' or ' F ' according to the + EncryptMetadata Boolean. + 4. Set bytes 9-11 to the ASCII characters ' a ', ' d ', ' b '. + 5. Set bytes 12-15 to 4 bytes of random data, which will be ignored. + 6. Encrypt the 16-byte block using AES-256 in ECB mode with an + initialization vector of zero, using the file encryption key as the + key. The result (16 bytes) is stored as the Perms string, and checked + for validity when the file is opened. + + Args: + key: + p: A set of flags specifying which operations shall be permitted + when the document is opened with user access. If bit 2 is set to 1, + all other bits are ignored and all operations are permitted. + If bit 2 is set to 0, permission for operations are based on the + values of the remaining flags defined in Table 24. + metadata_encrypted: A boolean indicating if the metadata is encrypted. + + Returns: + The perms value + + """ + b8 = b"T" if metadata_encrypted else b"F" + rr = secrets.token_bytes(4) + data = struct.pack(" str: + """ + Apply the SASLprep profile (RFC 4013) of stringprep (RFC 3454). + + This normalizes Unicode passwords for PDF 2.0 (AES-256, revision 5/6) + encryption as required by the PDF specification. + + Args: + password: The Unicode password string to normalize. + + Returns: + The SASLprep-normalized string. + + Raises: + ValueError: If the password contains prohibited characters + or fails the bidirectional character check. + + """ + # Mapping (RFC 4013 §2.1) + # - Map characters in table B.1 to nothing + # - Map characters in table C.1.2 (non-ASCII spaces) to U+0020 (SPACE) + mapped: list[str] = [] + for character in password: + if stringprep.in_table_b1(character): + continue # map to nothing + if stringprep.in_table_c12(character): + mapped.append(" ") # map to SPACE + else: + mapped.append(character) + password = "".join(mapped) + + # Normalization (RFC 4013 §2.2) — Unicode NFKC + password = unicodedata.normalize("NFKC", password) + + for character in password: + for check, description in _SASLPREP_PROHIBITED_CHECKS: + if check(character): + raise ValueError( + f"SASLprep: {description} U+{ord(character):04X}" + ) + + # Bidirectional check (RFC 4013 §2.4, RFC 3454 §6) + has_r_and_al = False + has_l = False + for character in password: + if stringprep.in_table_d1(character): + has_r_and_al = True + if stringprep.in_table_d2(character): + has_l = True + + if has_r_and_al: + if has_l: + raise ValueError( + "SASLprep: string with RandALCat characters must not " + "contain LCat characters" + ) + if not stringprep.in_table_d1(password[0]) or not stringprep.in_table_d1( + password[-1] + ): + raise ValueError( + "SASLprep: string with RandALCat characters must start " + "and end with RandALCat characters" + ) + + return password + + +class EncryptAlgorithm(tuple, Enum): # type: ignore # noqa: SLOT001 + # V, R, Length + RC4_40 = (1, 2, 40) + RC4_128 = (2, 3, 128) + AES_128 = (4, 4, 128) + AES_256_R5 = (5, 5, 256) + AES_256 = (5, 6, 256) + + +class EncryptionValues: + O: bytes # noqa: E741 + U: bytes + OE: bytes + UE: bytes + Perms: bytes + + +class Encryption: + """ + Collects and manages parameters for PDF document encryption and decryption. + + Args: + V: A code specifying the algorithm to be used in encrypting and + decrypting the document. + R: The revision of the standard security handler. + Length: The length of the encryption key in bits. + P: A set of flags specifying which operations shall be permitted + when the document is opened with user access + entry: The encryption dictionary object. + EncryptMetadata: Whether to encrypt metadata in the document. + first_id_entry: The first 16 bytes of the file's original ID. + StmF: The name of the crypt filter that shall be used by default + when decrypting streams. + StrF: The name of the crypt filter that shall be used when decrypting + all strings in the document. + EFF: The name of the crypt filter that shall be used when + encrypting embedded file streams that do not have their own + crypt filter specifier. + values: Additional encryption parameters. + + """ + + def __init__( + self, + V: int, + R: int, + Length: int, + P: int, + entry: DictionaryObject, + EncryptMetadata: bool, + first_id_entry: bytes, + StmF: str, + StrF: str, + EFF: str, + values: Optional[EncryptionValues], + ) -> None: + # §7.6.2, entries common to all encryption dictionaries + # use same name as keys of encryption dictionaries entries + self.V = V + self.R = R + self.Length = Length # key_size + self.P = (P + 0x100000000) % 0x100000000 # maybe P < 0 + self.EncryptMetadata = EncryptMetadata + self.id1_entry = first_id_entry + self.StmF = StmF + self.StrF = StrF + self.EFF = EFF + self.values: EncryptionValues = values or EncryptionValues() + + self._password_type = PasswordType.NOT_DECRYPTED + self._key: Optional[bytes] = None + self._are_permissions_valid: bool = True + + def is_decrypted(self) -> bool: + return self._password_type != PasswordType.NOT_DECRYPTED + + def encrypt_object(self, obj: PdfObject, idnum: int, generation: int) -> PdfObject: + # skip calculate key + if not self._is_encryption_object(obj): + return obj + + cf = self._make_crypt_filter(idnum, generation) + return cf.encrypt_object(obj) + + def decrypt_object(self, obj: PdfObject, idnum: int, generation: int, *, strict: bool = True) -> PdfObject: + # skip calculate key + if not self._is_encryption_object(obj): + return obj + + cf = self._make_crypt_filter(idnum, generation) + return cf.decrypt_object(obj, strict=strict) + + @staticmethod + def _is_encryption_object(obj: PdfObject) -> bool: + return isinstance( + obj, + ( + ByteStringObject, + TextStringObject, + StreamObject, + ArrayObject, + DictionaryObject, + ), + ) + + def _make_crypt_filter(self, idnum: int, generation: int) -> CryptFilter: + """ + Algorithm 1: Encryption of data using the RC4 or AES algorithms. + + a) Obtain the object number and generation number from the object + identifier of the string or stream to be encrypted + (see 7.3.10, "Indirect Objects"). If the string is a direct object, + use the identifier of the indirect object containing it. + b) For all strings and streams without crypt filter specifier; treating + the object number and generation number as binary integers, extend + the original n-byte encryption key to n + 5 bytes by appending the + low-order 3 bytes of the object number and the low-order 2 bytes of + the generation number in that order, low-order byte first. + (n is 5 unless the value of V in the encryption dictionary is greater + than 1, in which case n is the value of Length divided by 8.) + If using the AES algorithm, extend the encryption key an additional + 4 bytes by adding the value “sAlT”, which corresponds to the + hexadecimal values 0x73, 0x41, 0x6C, 0x54. (This addition is done for + backward compatibility and is not intended to provide additional + security.) + c) Initialize the MD5 hash function and pass the result of step (b) as + input to this function. + d) Use the first (n + 5) bytes, up to a maximum of 16, of the output + from the MD5 hash as the key for the RC4 or AES symmetric key + algorithms, along with the string or stream data to be encrypted. + If using the AES algorithm, the Cipher Block Chaining (CBC) mode, + which requires an initialization vector, is used. The block size + parameter is set to 16 bytes, and the initialization vector is a + 16-byte random number that is stored as the first 16 bytes of the + encrypted stream or string. + + Algorithm 3.1a: Encryption of data using the AES-256 algorithm. + + Note: Algorithm 3.1a does not use MD5 key derivation, so AES-256 + encrypted files can be read on FIPS-enabled systems where MD5 is blocked. + + 1. Use the 32-byte file encryption key for the AES-256 symmetric key + algorithm, along with the string or stream data to be encrypted. + Use the AES algorithm in Cipher Block Chaining (CBC) mode, which + requires an initialization vector. The block size parameter is set to + 16 bytes, and the initialization vector is a 16-byte random number + that is stored as the first 16 bytes of the encrypted stream or string. + The output is the encrypted data to be stored in the PDF file. + """ + pack1 = (idnum & 0xFFFFFF).to_bytes(3, "little") + pack2 = (generation & 0xFFFF).to_bytes(2, "little") + + assert self._key + key = self._key + + # Algorithm 1 (V <= 4): MD5 key derivation. Algorithm 3.1a (V >= 5): key used directly. + if self.V <= 4: + n = 5 if self.V == 1 else self.Length // 8 + key_data = key[:n] + pack1 + pack2 + key_hash = hashlib.md5(key_data) + rc4_key = key_hash.digest()[: min(n + 5, 16)] + + # for AES-128 + key_hash.update(b"sAlT") + aes128_key = key_hash.digest()[: min(n + 5, 16)] + else: + rc4_key = b"" + aes128_key = b"" + + # for AES-256 + aes256_key = key + + stm_crypt = self._get_crypt(self.StmF, rc4_key, aes128_key, aes256_key) + str_crypt = self._get_crypt(self.StrF, rc4_key, aes128_key, aes256_key) + ef_crypt = self._get_crypt(self.EFF, rc4_key, aes128_key, aes256_key) + + return CryptFilter(stm_crypt, str_crypt, ef_crypt) + + @staticmethod + def _get_crypt( + method: str, rc4_key: bytes, aes128_key: bytes, aes256_key: bytes + ) -> CryptBase: + if method == "/AESV2": + return CryptAES(aes128_key) + if method == "/AESV3": + return CryptAES(aes256_key) + if method == "/Identity": + return CryptIdentity() + + return CryptRC4(rc4_key) + + def _encode_password( + self, password: Union[bytes, str], *, strict: bool = False + ) -> bytes: + if isinstance(password, str): + if self.V >= 5: + # PDF 2.0 §7.6.4.3.3: AES-256 (R5/R6) requires SASLprep + # normalization followed by UTF-8 encoding. + try: + password = _saslprep(password) + except (ValueError, IndexError) as e: + if strict: + raise ValueError( + f"Password SASLprep normalization failed: {e}" + ) from e + logger_warning( + "SASLprep normalization failed, using plain UTF-8: %(err)s", + source=__name__, + err=str(e), + ) + pwd = password.encode("utf-8") + else: + try: + pwd = password.encode("latin-1") + except Exception: + pwd = password.encode("utf-8") + else: + pwd = password + return pwd + + def verify( + self, password: Union[bytes, str], *, strict: bool = False + ) -> PasswordType: + pwd = self._encode_password(password, strict=strict) + key, rc = self.verify_v4(pwd) if self.V <= 4 else self.verify_v5(pwd) + if rc != PasswordType.NOT_DECRYPTED: + self._password_type = rc + self._key = key + return rc + + def verify_v4(self, password: bytes) -> tuple[bytes, PasswordType]: + # verify owner password first + key = AlgV4.verify_owner_password( + password, + self.R, + self.Length, + self.values.O, + self.values.U, + self.P, + self.id1_entry, + self.EncryptMetadata, + ) + if key: + return key, PasswordType.OWNER_PASSWORD + key = AlgV4.verify_user_password( + password, + self.R, + self.Length, + self.values.O, + self.values.U, + self.P, + self.id1_entry, + self.EncryptMetadata, + ) + if key: + return key, PasswordType.USER_PASSWORD + return b"", PasswordType.NOT_DECRYPTED + + def verify_v5(self, password: bytes) -> tuple[bytes, PasswordType]: + # verify owner password first + key = AlgV5.verify_owner_password( + self.R, password, self.values.O, self.values.OE, self.values.U + ) + rc = PasswordType.OWNER_PASSWORD + if not key: + key = AlgV5.verify_user_password( + self.R, password, self.values.U, self.values.UE + ) + rc = PasswordType.USER_PASSWORD + if not key: + return b"", PasswordType.NOT_DECRYPTED + + # verify Perms + self._are_permissions_valid = AlgV5.verify_perms(key, self.values.Perms, self.P, self.EncryptMetadata) + if not self._are_permissions_valid: + logger_warning("ignore '/Perms' verify failed", source=__name__) + return key, rc + + def write_entry( + self, + user_password: str, + owner_password: Optional[str], + *, + strict: bool = False, + ) -> DictionaryObject: + user_pwd = self._encode_password(user_password, strict=strict) + owner_pwd = ( + self._encode_password(owner_password, strict=strict) + if owner_password + else None + ) + if owner_pwd is None: + owner_pwd = user_pwd + + if self.V <= 4: + self.compute_values_v4(user_pwd, owner_pwd) + else: + self._key = secrets.token_bytes(self.Length // 8) + values = AlgV5.generate_values( + self.R, user_pwd, owner_pwd, self._key, self.P, self.EncryptMetadata + ) + self.values.O = values["/O"] + self.values.U = values["/U"] + self.values.OE = values["/OE"] + self.values.UE = values["/UE"] + self.values.Perms = values["/Perms"] + + dict_obj = DictionaryObject() + dict_obj[NameObject("/V")] = NumberObject(self.V) + dict_obj[NameObject("/R")] = NumberObject(self.R) + dict_obj[NameObject("/Length")] = NumberObject(self.Length) + dict_obj[NameObject("/P")] = NumberObject(self.P) + dict_obj[NameObject("/Filter")] = NameObject("/Standard") + # ignore /EncryptMetadata + + dict_obj[NameObject("/O")] = ByteStringObject(self.values.O) + dict_obj[NameObject("/U")] = ByteStringObject(self.values.U) + + if self.V >= 4: + # TODO: allow different method + std_cf = DictionaryObject() + std_cf[NameObject("/AuthEvent")] = NameObject("/DocOpen") + std_cf[NameObject("/CFM")] = NameObject(self.StmF) + std_cf[NameObject("/Length")] = NumberObject(self.Length // 8) + cf = DictionaryObject() + cf[NameObject("/StdCF")] = std_cf + dict_obj[NameObject("/CF")] = cf + dict_obj[NameObject("/StmF")] = NameObject("/StdCF") + dict_obj[NameObject("/StrF")] = NameObject("/StdCF") + # ignore EFF + # dict_obj[NameObject("/EFF")] = NameObject("/StdCF") + + if self.V >= 5: + dict_obj[NameObject("/OE")] = ByteStringObject(self.values.OE) + dict_obj[NameObject("/UE")] = ByteStringObject(self.values.UE) + dict_obj[NameObject("/Perms")] = ByteStringObject(self.values.Perms) + return dict_obj + + def compute_values_v4(self, user_password: bytes, owner_password: bytes) -> None: + rc4_key = AlgV4.compute_O_value_key(owner_password, self.R, self.Length) + o_value = AlgV4.compute_O_value(rc4_key, user_password, self.R) + + key = AlgV4.compute_key( + user_password, + self.R, + self.Length, + o_value, + self.P, + self.id1_entry, + self.EncryptMetadata, + ) + u_value = AlgV4.compute_U_value(key, self.R, self.id1_entry) + + self._key = key + self.values.O = o_value + self.values.U = u_value + + @staticmethod + def read(encryption_entry: DictionaryObject, first_id_entry: bytes) -> "Encryption": + if encryption_entry.get("/Filter") != "/Standard": + raise NotImplementedError( + "only Standard PDF encryption handler is available" + ) + if "/SubFilter" in encryption_entry: + raise NotImplementedError("/SubFilter NOT supported") + + stm_filter = "/V2" + str_filter = "/V2" + ef_filter = "/V2" + + alg_ver = encryption_entry.get("/V", 0) + if alg_ver not in (1, 2, 3, 4, 5): + raise NotImplementedError(f"Encryption V={alg_ver} NOT supported") + if alg_ver >= 4: + filters = encryption_entry["/CF"] + + stm_filter = encryption_entry.get("/StmF", "/Identity") + str_filter = encryption_entry.get("/StrF", "/Identity") + ef_filter = encryption_entry.get("/EFF", stm_filter) + + if stm_filter != "/Identity": + stm_filter = filters[stm_filter]["/CFM"] # type: ignore[index] + if str_filter != "/Identity": + str_filter = filters[str_filter]["/CFM"] # type: ignore[index] + if ef_filter != "/Identity": + ef_filter = filters[ef_filter]["/CFM"] # type: ignore[index] + + allowed_methods = ("/Identity", "/V2", "/AESV2", "/AESV3") + if stm_filter not in allowed_methods: + raise NotImplementedError(f"StmF Method {stm_filter} NOT supported!") + if str_filter not in allowed_methods: + raise NotImplementedError(f"StrF Method {str_filter} NOT supported!") + if ef_filter not in allowed_methods: + raise NotImplementedError(f"EFF Method {ef_filter} NOT supported!") + + alg_rev = cast(int, encryption_entry["/R"]) + perm_flags = cast(int, encryption_entry["/P"]) + key_bits = encryption_entry.get("/Length", 40) + if alg_ver == 4 and stm_filter == "/AESV2": + cf_dict = cast(DictionaryObject, filters[encryption_entry["/StmF"]]) # type: ignore[index] + # CF /Length is in bytes (default 16 for AES-128), convert to bits + key_bits = cast(int, cf_dict.get("/Length", 16)) * 8 + encrypt_metadata = encryption_entry.get("/EncryptMetadata") + encrypt_metadata = ( + encrypt_metadata.value if encrypt_metadata is not None else True + ) + values = EncryptionValues() + values.O = cast(ByteStringObject, encryption_entry["/O"]).original_bytes + values.U = cast(ByteStringObject, encryption_entry["/U"]).original_bytes + values.OE = encryption_entry.get("/OE", ByteStringObject()).original_bytes + values.UE = encryption_entry.get("/UE", ByteStringObject()).original_bytes + values.Perms = encryption_entry.get("/Perms", ByteStringObject()).original_bytes + return Encryption( + V=alg_ver, + R=alg_rev, + Length=key_bits, + P=perm_flags, + EncryptMetadata=encrypt_metadata, + first_id_entry=first_id_entry, + values=values, + StrF=str_filter, + StmF=stm_filter, + EFF=ef_filter, + entry=encryption_entry, # Dummy entry for the moment; will get removed + ) + + @staticmethod + def make( + alg: EncryptAlgorithm, permissions: int, first_id_entry: bytes + ) -> "Encryption": + alg_ver, alg_rev, key_bits = alg + + stm_filter, str_filter, ef_filter = "/V2", "/V2", "/V2" + + if alg == EncryptAlgorithm.AES_128: + stm_filter, str_filter, ef_filter = "/AESV2", "/AESV2", "/AESV2" + elif alg in (EncryptAlgorithm.AES_256_R5, EncryptAlgorithm.AES_256): + stm_filter, str_filter, ef_filter = "/AESV3", "/AESV3", "/AESV3" + + return Encryption( + V=alg_ver, + R=alg_rev, + Length=key_bits, + P=permissions, + EncryptMetadata=True, + first_id_entry=first_id_entry, + values=None, + StrF=str_filter, + StmF=stm_filter, + EFF=ef_filter, + entry=DictionaryObject(), # Dummy entry for the moment; will get removed + ) diff --git a/app/vendor/pypdf/_font.py b/app/vendor/pypdf/_font.py new file mode 100644 index 0000000..7141af4 --- /dev/null +++ b/app/vendor/pypdf/_font.py @@ -0,0 +1,884 @@ +from __future__ import annotations + +import unicodedata +from collections.abc import Sequence +from dataclasses import dataclass, field +from typing import TYPE_CHECKING, Any, ClassVar, cast + +from pypdf.generic import ( + ArrayObject, + DictionaryObject, + FloatObject, + IndirectObject, + NameObject, + NumberObject, + PdfObject, + StreamObject, + TextStringObject, +) + +from ._cmap import get_encoding +from ._codecs import encoding_dict_from_named_encoding +from ._codecs.adobe_glyphs import adobe_glyphs +from ._utils import logger_warning +from .constants import FontFlags +from .errors import LimitReachedError, PdfReadError + +if TYPE_CHECKING: + from fontTools.ttLib.tables._h_e_a_d import table__h_e_a_d + from fontTools.ttLib.tables._p_o_s_t import table__p_o_s_t + from fontTools.ttLib.tables.O_S_2f_2 import table_O_S_2f_2 + + from ._writer import PdfWriter + +try: + from io import BytesIO + + from fontTools.ttLib import TTFont, TTLibError + HAS_FONTTOOLS = True +except ImportError: + HAS_FONTTOOLS = False + + +# Limits. +MAX_CID_WIDTH_ENTRY_COUNT = 65_536 +MAX_WIDTH_ENTRY_COUNT = 100_000 +# For a simple font, character codes are one-byte values, 0-255, so /Widths can have at most 256 meaningful entries. +MAX_SIMPLE_FONT_WIDTH_ENTRY_COUNT = 256 + + +# Some constants from truetype font tables that we use: +HEADER_MACSTYLE_ITALIC = 0x02 +OS2_FSSELECTION_ITALIC = 0x01 +OS2_PANOSE_BFAMILYTYPE_SCRIPT = 3 +OS2_PANOSE_BFAMILYTYPE_DECORATIVE = 4 +OS2_PANOSE_BFAMILYTYPE_PICTORIAL = 5 +OS2_PANOSE_BPROPORTION_MONOSPACED = 9 +OS2_SFAMILYSCLASS_SCRIPTS = 10 +OS2_SFAMILYSCLASS_SYMBOLIC = 12 + + +# Groups of CMap mappings cannot exceed 100 entries (Adobe CMap and CIDFont Files Specification 1.0, p. 49). +CMAP_MAX_ENTRIES_PER_GROUP = 100 + + +@dataclass(frozen=True) +class FontDescriptor: + """ + Represents the FontDescriptor dictionary as defined in the PDF specification. + This contains both descriptive and metric information. + + The defaults are derived from the mean values of the 14 core fonts, rounded + to 100. + """ + + _DEFAULT_BBOX: ClassVar[tuple[float, float, float, float]] = (-100.0, -200.0, 1000.0, 900.0) + + name: str = "Unknown" + family: str = "Unknown" + weight: str = "Unknown" + + ascent: float = 700.0 + descent: float = -200.0 + cap_height: float = 600.0 + x_height: float = 500.0 + italic_angle: float = 0.0 # Non-italic + flags: int = 32 # Non-serif, non-symbolic, not fixed width + bbox: tuple[float, float, float, float] = _DEFAULT_BBOX + font_file: StreamObject | None = None + + def as_font_descriptor_resource(self) -> DictionaryObject: + font_descriptor_resource = DictionaryObject({ + NameObject("/Type"): NameObject("/FontDescriptor"), + NameObject("/FontName"): NameObject(f"/{self.name}"), + NameObject("/Flags"): NumberObject(self.flags), + NameObject("/FontBBox"): ArrayObject([FloatObject(n) for n in self.bbox]), + NameObject("/ItalicAngle"): FloatObject(self.italic_angle), + NameObject("/Ascent"): FloatObject(self.ascent), + NameObject("/Descent"): FloatObject(self.descent), + NameObject("/CapHeight"): FloatObject(self.cap_height), + NameObject("/XHeight"): FloatObject(self.x_height), + }) + + if self.font_file: + # Add the stream. For now, we assume a TrueType font (FontFile2) + font_descriptor_resource[NameObject("/FontFile2")] = self.font_file + + return font_descriptor_resource + + +@dataclass(frozen=True) +class CoreFontMetrics: + font_descriptor: FontDescriptor + character_widths: dict[str, float] + + +@dataclass +class Font: + """ + A font object for use during text extraction and for producing + text appearance streams. + + Attributes: + name: Font name, derived from ``font["/BaseFont"]`` + character_map: The font's character map + encoding: Font encoding + sub_type: The font type, such as Type1, TrueType, or Type3. + font_descriptor: Font metrics, including a mapping of characters to widths + character_widths: A mapping of characters to widths + space_width: The width of a space, or an approximation + interpretable: Default True. If False, the font glyphs cannot + be translated to characters, e.g. Type3 fonts that do not define + a ``/ToUnicode`` mapping. + + """ + + name: str + encoding: str | dict[int, str] + character_map: dict[Any, Any] = field(default_factory=dict) + sub_type: str = "Unknown" + font_descriptor: FontDescriptor = field(default_factory=FontDescriptor) + character_widths: dict[str, float] = field(default_factory=lambda: {"default": 500}) + space_width: float | int = 250 + space_char: str = " " + interpretable: bool = True + + @staticmethod + def _collect_tt_t1_character_widths( + pdf_font_dict: DictionaryObject, + char_map: dict[Any, Any], + encoding: str | dict[int, str], + current_widths: dict[str, float] + ) -> None: + """Parses a TrueType or Type1 font's /Widths array from a font dictionary and updates character widths""" + widths_array = cast(ArrayObject, pdf_font_dict["/Widths"]) + Font.__check_entry_count( + len(widths_array), MAX_SIMPLE_FONT_WIDTH_ENTRY_COUNT + ) + first_char = pdf_font_dict.get("/FirstChar", 0) + for idx, width in enumerate(widths_array): + current_widths[chr(idx + first_char)] = int(width) + + @staticmethod + def __check_range_length(start: int, end: int) -> None: + if end < start: + raise LimitReachedError( + f"Invalid CID width range: {start}..{end}." + ) + + count = end - start + if count > MAX_CID_WIDTH_ENTRY_COUNT: + raise LimitReachedError(f"CID width range too large: {count} > {MAX_CID_WIDTH_ENTRY_COUNT}.") + + @staticmethod + def __check_entry_count(count: int, limit: int = MAX_WIDTH_ENTRY_COUNT) -> None: + if count > limit: + raise LimitReachedError(f"Too many character widths: {count} > {limit}.") + + @staticmethod + def _collect_cid_character_widths(d_font: DictionaryObject, current_widths: dict[str, float]) -> None: + """Parses the /W array from a DescendantFont dictionary and updates character widths.""" + # /W width definitions have two valid formats which can be mixed and matched: + # (1) A character start index followed by a list of widths, e.g. + # `45 [500 600 700]` applies widths 500, 600, 700 to characters 45-47. + # (2) A character start index, a character stop index, and a width, e.g. + # `45 65 500` applies width 500 to characters 45-65. + skip_count = 0 + entry_count = 0 + _w = d_font.get("/W", ArrayObject()).get_object() + _w_length = len(_w) + for idx, w_entry in enumerate(_w): + if skip_count: + skip_count -= 1 + continue + w_entry = w_entry.get_object() + if not isinstance(w_entry, (int, float)): + # We should never get here due to skip_count above. But + # sometimes we do. + logger_warning( + "Expected numeric value for width, got %(w_entry)s. Ignoring it.", + source=__name__, + w_entry=w_entry, + ) + continue + # check for format (1): `int [int int int int ...]` + w_next_entry = _w[idx + 1].get_object() if idx + 1 < _w_length else None + if isinstance(w_next_entry, Sequence): + start_idx, width_list = int(w_entry), w_next_entry + stop_idx = start_idx + len(width_list) + Font.__check_range_length(start_idx, stop_idx) + entry_count += (stop_idx - start_idx) + Font.__check_entry_count(entry_count) + current_widths.update( + { + chr(_cidx): _width + for _cidx, _width in zip( + range(start_idx, stop_idx, 1), + width_list, + ) + } + ) + skip_count = 1 + # check for format (2): `int int int` + elif ( + isinstance(w_next_entry, (int, float)) + and idx + 2 < _w_length + and isinstance(_w[idx + 2].get_object(), (int, float)) + ): + start_idx, stop_idx, const_width = ( + int(w_entry), + int(w_next_entry), + _w[idx + 2].get_object(), + ) + Font.__check_range_length(start_idx, stop_idx + 1) + entry_count += (stop_idx - start_idx + 1) + Font.__check_entry_count(entry_count) + current_widths.update( + { + chr(_cidx): const_width + for _cidx in range( + start_idx, stop_idx + 1, 1 + ) + } + ) + skip_count = 2 + else: + # This handles the case of out of bounds (reaching the end of the width definitions + # while expecting more elements). + logger_warning( + "Invalid font width definition. Last element: %(w_entry)s.", + source=__name__, + w_entry=w_entry, + ) + + @staticmethod + def _get_space_char( + encoding: str | dict[int, str], + character_map: dict[Any, Any], + ) -> str: + space_char = " " + if isinstance(encoding, dict): + for char_code, char_str in encoding.items(): + if char_str == space_char: + return chr(char_code) + + for glyph_id, char_str in character_map.items(): + if char_str == space_char: + return str(glyph_id) + + return space_char + + @staticmethod + def _add_default_width(current_widths: dict[str, float], flags: int, space_char: str) -> None: + if not current_widths: + current_widths["default"] = 500 + return + + if space_char in current_widths and current_widths[space_char] != 0: + # Setting default to once or twice the space width, depending on fixed pitch + if (flags & FontFlags.FIXED_PITCH) == FontFlags.FIXED_PITCH: + current_widths["default"] = current_widths[space_char] + return + + current_widths["default"] = int(2 * current_widths[space_char]) + return + + # Use the average width of existing glyph widths + valid_widths = [w for w in current_widths.values() if w > 0] + current_widths["default"] = sum(valid_widths) // len(valid_widths) if valid_widths else 500 + + @staticmethod + def _add_space_width( + character_widths: dict[str, float], + flags: int, + space_char: str + ) -> float: + space_width = character_widths.get(space_char, 0) + if space_width != 0: + return space_width + + if (flags & FontFlags.FIXED_PITCH) == FontFlags.FIXED_PITCH: + return character_widths["default"] + + return character_widths["default"] // 2 + + @staticmethod + def _parse_bbox(raw_bbox: Any) -> tuple[float, float, float, float] | None: + """ + Convert a raw /FontBBox value into four floats. + + Args: + raw_bbox: The raw /FontBBox value read from the PDF. + + Returns: + The four bounding box values, or ``None`` when the value is not + a sequence of exactly four numbers, so that a malformed entry + falls back to the default bounding box rather than raising. + + """ + try: + bbox = [float(value) for value in raw_bbox] + except (TypeError, ValueError): + return None + if len(bbox) != 4: + return None + return bbox[0], bbox[1], bbox[2], bbox[3] + + @staticmethod + def _parse_font_descriptor(font_descriptor_obj: DictionaryObject) -> dict[str, Any]: + font_descriptor_kwargs: dict[Any, Any] = {} + for source_key, target_key in [ + ("/FontName", "name"), + ("/FontFamily", "family"), + ("/FontWeight", "weight"), + ("/Ascent", "ascent"), + ("/Descent", "descent"), + ("/CapHeight", "cap_height"), + ("/XHeight", "x_height"), + ("/ItalicAngle", "italic_angle"), + ("/Flags", "flags"), + ("/FontBBox", "bbox") + ]: + if source_key in font_descriptor_obj: + font_descriptor_kwargs[target_key] = font_descriptor_obj[source_key] + # Handle missing or malformed bbox gracefully - PDFs may have fonts without valid bounding boxes + if "bbox" in font_descriptor_kwargs: + bbox = Font._parse_bbox(font_descriptor_kwargs["bbox"]) + if bbox is None: + del font_descriptor_kwargs["bbox"] + else: + font_descriptor_kwargs["bbox"] = bbox + + # Find the binary stream for this font if there is one + for source_key in ["/FontFile", "/FontFile2", "/FontFile3"]: + if source_key in font_descriptor_obj: + if "font_file" in font_descriptor_kwargs: + raise PdfReadError(f"More than one /FontFile found in {font_descriptor_obj}") + + try: + font_file = font_descriptor_obj[source_key].get_object() + font_descriptor_kwargs["font_file"] = font_file + except PdfReadError as e: + logger_warning( + "Failed to get %(source_key)r in %(font_descriptor_obj)s: %(error)s", + source=__name__, + source_key=source_key, + font_descriptor_obj=font_descriptor_obj, + error=e, + ) + return font_descriptor_kwargs + + @classmethod + def from_font_resource( + cls, + pdf_font_dict: DictionaryObject, + ) -> Font: + from pypdf._codecs.core_font_metrics import CORE_FONT_METRICS # noqa: PLC0415 + + # Can collect base_font, name and encoding directly from font resource + name = pdf_font_dict.get("/BaseFont", "Unknown").removeprefix("/") + sub_type = pdf_font_dict.get("/Subtype", "Unknown").removeprefix("/") + encoding, character_map = get_encoding(pdf_font_dict) + font_descriptor = None + character_widths: dict[str, float] = {} + interpretable = True + + # Deal with fonts by type; Type1, TrueType and certain Type3 + if pdf_font_dict.get("/Subtype") in ("/Type1", "/MMType1", "/TrueType", "/Type3"): + # Type3 fonts that do not specify a "/ToUnicode" mapping cannot be + # reliably converted into character codes unless all named chars + # in /CharProcs map to a standard adobe glyph. See §9.10.2 of the + # PDF 1.7 standard. + if sub_type == "Type3" and "/ToUnicode" not in pdf_font_dict: + interpretable = all( + cname in adobe_glyphs + for cname in pdf_font_dict.get("/CharProcs") or [] + ) + if interpretable: # Save some overhead if font is not interpretable + if "/Widths" in pdf_font_dict: + cls._collect_tt_t1_character_widths( + pdf_font_dict, character_map, encoding, character_widths + ) + + elif name in CORE_FONT_METRICS: + font_descriptor = CORE_FONT_METRICS[name].font_descriptor + if isinstance(encoding, dict): + for code, character in encoding.items(): + # Look up the width using the glyph name from the encoding + if character in CORE_FONT_METRICS[name].character_widths: + character_widths[chr(code)] = CORE_FONT_METRICS[name].character_widths[character] + else: + for code in range(256): + character = chr(code) + if character in CORE_FONT_METRICS[name].character_widths: + character_widths[character] = CORE_FONT_METRICS[name].character_widths[character] + if "/FontDescriptor" in pdf_font_dict: + font_descriptor_obj = pdf_font_dict.get("/FontDescriptor", DictionaryObject()).get_object() + if "/MissingWidth" in font_descriptor_obj: + character_widths["default"] = cast(int, font_descriptor_obj["/MissingWidth"].get_object()) + font_descriptor = FontDescriptor(**cls._parse_font_descriptor(font_descriptor_obj)) + elif "/FontBBox" in pdf_font_dict: + # For Type3 without Font Descriptor but with FontBBox, see Table 110 in the PDF specification 2.0 + font_descriptor_kwargs: dict[str, Any] = {"name": name} + bbox = cls._parse_bbox(pdf_font_dict["/FontBBox"]) + if bbox is not None: + font_descriptor_kwargs["bbox"] = bbox + font_descriptor = FontDescriptor(**font_descriptor_kwargs) + + else: + # Composite font or CID font - CID fonts have a /W array mapping character codes + # to widths stashed in /DescendantFonts. No need to test for /DescendantFonts though, + # because all other fonts have already been dealt with. + d_font: DictionaryObject + for d_font_idx, d_font in enumerate( + cast(ArrayObject, pdf_font_dict["/DescendantFonts"]) + ): + d_font = cast(DictionaryObject, d_font.get_object()) + cast(ArrayObject, pdf_font_dict["/DescendantFonts"])[d_font_idx] = d_font + cls._collect_cid_character_widths(d_font=d_font, current_widths=character_widths) + if "/DW" in d_font: + character_widths["default"] = cast(int, d_font["/DW"].get_object()) + font_descriptor_obj = d_font.get("/FontDescriptor", DictionaryObject()).get_object() + font_descriptor = FontDescriptor(**cls._parse_font_descriptor(font_descriptor_obj)) + + if not font_descriptor: + font_descriptor = FontDescriptor(name=name) + + space_char = cls._get_space_char(encoding, character_map) + if character_widths.get("default", 0) == 0: + cls._add_default_width(character_widths, font_descriptor.flags, space_char) + space_width = cls._add_space_width(character_widths, font_descriptor.flags, space_char) + + return cls( + name=name, + sub_type=sub_type, + encoding=encoding, + font_descriptor=font_descriptor, + character_map=character_map, + character_widths=character_widths, + space_width=space_width, + space_char=space_char, + interpretable=interpretable + ) + + @staticmethod + def _font_flags_from_truetype_font_tables( + header: table__h_e_a_d, + postscript: table__p_o_s_t, + os2: table_O_S_2f_2 + ) -> int: + # Get the font flags + if os2: + panose = os2.panose + # sFamilyClass is a two-byte field. The high byte describes the family class, whereas the low + # byte only describes the subclass. We only need the high byte, hence the bit shift below: + family_class = os2.sFamilyClass >> 8 + flags: int = 0 + + # ITALIC + if header.macStyle & HEADER_MACSTYLE_ITALIC or (os2 and os2.fsSelection & OS2_FSSELECTION_ITALIC): + flags |= FontFlags.ITALIC + if postscript: + italic_angle = postscript.italicAngle + if italic_angle != 0.0: + flags |= FontFlags.ITALIC + + # FIXED_PITCH + if ( + (os2 and panose.bProportion == OS2_PANOSE_BPROPORTION_MONOSPACED) or + (postscript and postscript.isFixedPitch > 0) # Actually 1, but originally (older versions of the TTF + ): # specification) any non-zero value signified monospace. + flags |= FontFlags.FIXED_PITCH + + # SCRIPT + if os2 and ( + family_class == OS2_SFAMILYSCLASS_SCRIPTS or panose.bFamilyType == OS2_PANOSE_BFAMILYTYPE_SCRIPT + ): + flags |= FontFlags.SCRIPT + + # SERIF + if os2 and ( + 2 <= panose.bSerifStyle <= 10 + or 1 <= family_class <= 5 or family_class == 7 # 6 is reserved, all 8 and above are not serif + ): + flags |= FontFlags.SERIF + + # SYMBOLIC + if os2 and ( + family_class == OS2_SFAMILYSCLASS_SYMBOLIC or + panose.bFamilyType in {OS2_PANOSE_BFAMILYTYPE_DECORATIVE, OS2_PANOSE_BFAMILYTYPE_PICTORIAL} + ): + flags |= FontFlags.SYMBOLIC + else: + flags |= FontFlags.NONSYMBOLIC + + return flags + + @classmethod + def from_truetype_font_file(cls, font_file: BytesIO) -> Font: + if not HAS_FONTTOOLS: + raise ImportError("The 'fontTools' library is required to use 'from_truetype_font_file'") + with TTFont(font_file) as tt_font_object: + # See Chapter 6 of the TrueType reference manual for the definition of the head, OS/2 and post tables: + # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6head.html + # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6OS2.html + # https://developer.apple.com/fonts/TrueType-Reference-Manual/RM06/Chap6post.html + header = tt_font_object["head"] + horizontal_header = tt_font_object["hhea"] + metrics = tt_font_object["hmtx"].metrics + + # Collect additional font tables to derive font information + postscript = tt_font_object.get("post", None) + os2 = tt_font_object.get("OS/2", None) + + # Get the scaling factor to convert font file's units per em to PDF's 1000 units per em + units_per_em = header.unitsPerEm + if not units_per_em: + raise PdfReadError("Font file has an invalid unitsPerEm of 0") + scale_factor = 1000.0 / units_per_em + + # Get the font descriptor + font_descriptor_kwargs: dict[Any, Any] = {} + names = tt_font_object.get("name", None) + if names: + font_descriptor_kwargs["name"] = names.getBestFullName() + font_descriptor_kwargs["family"] = names.getBestFamilyName() + font_descriptor_kwargs["weight"] = names.getBestSubFamilyName() + font_descriptor_kwargs["ascent"] = int(round(horizontal_header.ascent * scale_factor, 0)) + font_descriptor_kwargs["descent"] = int(round(horizontal_header.descent * scale_factor, 0)) + if os2: + try: + font_descriptor_kwargs["cap_height"] = int(round(os2.sCapHeight * scale_factor, 0)) + font_descriptor_kwargs["x_height"] = int(round(os2.sxHeight * scale_factor, 0)) + except AttributeError: + pass + + font_descriptor_kwargs["flags"] = cls._font_flags_from_truetype_font_tables(header, postscript, os2) + + font_descriptor_kwargs["bbox"] = ( + round(header.xMin * scale_factor, 0), + round(header.yMin * scale_factor, 0), + round(header.xMax * scale_factor, 0), + round(header.yMax * scale_factor, 0) + ) + + font_file_data = StreamObject() + font_file_raw_bytes = font_file.getvalue() + font_file_data.set_data(font_file_raw_bytes) + font_file_data.update({NameObject("/Length1"): NumberObject(len(font_file_raw_bytes))}) + font_descriptor_kwargs["font_file"] = font_file_data + + font_descriptor = FontDescriptor(**font_descriptor_kwargs) + encoding = "utf_16_be" # Assume unicode + + character_widths: dict[str, float] = {} + character_map: dict[str, str] = {} + + glyph_order = tt_font_object.getGlyphOrder() + # Note that one glyph can be mapped to multiple unicode code points. However, buildReversedMin() + # creates a dictionary mapping glyphs to the minimum Unicode codepoint. + tt_font_cmap_table = tt_font_object.get("cmap") + if tt_font_cmap_table: + try: + reverse_cmap = tt_font_cmap_table.buildReversedMin() + except AttributeError: + # use buildReversed on fonttools < 4.57 and build a list of minimums from it + reverse_cmap = {k: min(r) for k, r in tt_font_cmap_table.buildReversed().items()} + for gid, glyph in enumerate(glyph_order): + char_code = reverse_cmap.get(glyph) + if char_code is None: + continue + char = chr(char_code) + gid = tt_font_object.getGlyphID(glyph) + # The following is to comply with how font_glyph_byte_map works in _appearance_stream.py + gid_bytes = gid.to_bytes(2, "big") + gid_key_string = gid_bytes.decode("utf-16-be", "surrogatepass") + character_map[gid_key_string] = char + character_widths[gid_key_string] = int(round(metrics[glyph][0] * scale_factor, 0)) + else: + raise PdfReadError("Font file does not have a cmap table") + + space_char = cls._get_space_char(encoding, character_map) + cls._add_default_width(character_widths, font_descriptor_kwargs["flags"], space_char) + space_width = cls._add_space_width( + character_widths, font_descriptor_kwargs["flags"], space_char + ) + + return cls( + name=font_descriptor.name, + sub_type="Type0", + encoding=encoding, + font_descriptor=font_descriptor, + character_map=character_map, + character_widths=character_widths, + space_width=space_width, + space_char=space_char, + interpretable=True + ) + + @classmethod + def from_core_font_name(cls, core_font_name: str = "/Helvetica") -> Font: + from pypdf._codecs.core_font_metrics import CORE_FONT_METRICS # noqa: PLC0415 + + font_name = core_font_name.removeprefix("/") + core_font_metrics = CORE_FONT_METRICS[font_name] + win_ansi_encoding = encoding_dict_from_named_encoding("cp1252") # WinAnsiEncoding + + font = cls( + name=font_name, + character_map={}, + encoding=win_ansi_encoding, + sub_type="Type1", + font_descriptor=core_font_metrics.font_descriptor, + character_widths={ + char: core_font_metrics.character_widths[character] + for code, character in win_ansi_encoding.items() + if (char := chr(code)) in core_font_metrics.character_widths + } + ) + font.character_widths["default"] = core_font_metrics.character_widths["default"] + + return font + + def _get_typographic_maps(self) -> tuple[dict[str, str], dict[str, bytes]]: + """ + Generates maps to translate input unicode text to bytes in two steps: + Unicode code point -> raw_character (reverse cmap) -> PDF bytes (encoding cmap). + """ + reverse_cmap = {} + encoding_cmap = {} + if ( + HAS_FONTTOOLS + and getattr(self.font_descriptor, "font_file", None) + and isinstance(self.encoding, str) + ): + try: + font_file_data = cast(StreamObject, self.font_descriptor.font_file).get_data() + with TTFont(BytesIO(font_file_data)) as tt_font_object: + tt_font_cmap_table = tt_font_object.get("cmap") + best_cmap = tt_font_cmap_table.getBestCmap() + for unicode_int, glyph_name in best_cmap.items(): + gid = tt_font_object.getGlyphID(glyph_name) + gid_bytes = gid.to_bytes(2, "big") + gid_key_string = gid_bytes.decode("utf-16-be", "surrogatepass") + unicode_char = chr(unicode_int) + reverse_cmap[unicode_char] = gid_key_string + encoding_cmap[gid_key_string] = gid_key_string.encode(self.encoding) + + return reverse_cmap, encoding_cmap + + except (AttributeError, TTLibError): # Cmap table is missing or the font is corrupt. + reverse_cmap.clear() + encoding_cmap.clear() + + if isinstance(self.encoding, str): + for glyph_id, unicode_char in self.character_map.items(): + glyph_id_str = str(glyph_id) + reverse_cmap[unicode_char] = glyph_id_str + encoding_cmap[glyph_id_str] = glyph_id_str.encode(self.encoding) + else: # Encoding is a dict, which means we are dealing with a simple font + for character_code, unicode_char in self.encoding.items(): + character_str = chr(character_code) + reverse_cmap[unicode_char] = character_str + encoding_cmap[character_str] = bytes((character_code,)) + + unicode_to_bytes = { + unicode_char: bytes((character_code,)) for character_code, unicode_char in self.encoding.items() + } + for character_code_str, unicode_char in self.character_map.items(): + reverse_cmap[unicode_char] = character_code_str + encoding_cmap[character_code_str] = unicode_to_bytes.get( + unicode_char, + bytes((ord(character_code_str),)) + ) + + return reverse_cmap, encoding_cmap + + def _create_widths_list_and_unicode_stream(self) -> tuple[list[PdfObject], StreamObject]: + from pypdf._codecs.core_font_metrics import CORE_FONT_METRICS # noqa: PLC0415 + + widths_list = [] + unicode_map = [] + bfchar_map: list[str] = [] + + # Composite/CID fonts use 4-hex digits, simple fonts use 2-hex digits + src_hex_format = "{cid:04X}" if self.sub_type == "Type0" else "{cid:02X}" + codespace_min = "0000" if self.sub_type == "Type0" else "00" + codespace_max = "FFFF" if self.sub_type == "Type0" else "FF" + cmap_name = "Adobe-Identity-UCS" if self.sub_type == "Type0" else "Custom-Simple-8Bit" + cid_system_info = ( + "/CIDSystemInfo <<\n/Registry (Adobe)\n/Ordering (UCS)\n/Supplement 0\n>> def\n" + if self.sub_type == "Type0" else "" + ) + + # If we have self.character_map then use that. Otherwise fall back to self.encoding. + mapping_source = (self.character_map or cast(dict[int, str], self.encoding)).items() + + # In the loop, src_id is the decoded GID string (the reverse unicode hack) or the character code + # and actual_char is the actual character. + for src_id, actual_char in mapping_source: + # Make sure that we do not include characters such as arabic presentation form characters. + # Note that, in some cases, unicodedata.normalize() might split a ligature, resulting + # in multiple characters. + normalized_chars = unicodedata.normalize("NFKC", actual_char) + uni_points = [ord(char) for char in normalized_chars] + # Only deal with Basic Multilingual Plane characters. + # TODO: Add all characters. + if all(uni_point <= 0xFFFF for uni_point in uni_points): + cid = ord(src_id) if isinstance(src_id, str) else src_id + cid_hex = src_hex_format.format(cid=cid) + uni_hex = "".join(f"{uni_point:04X}" for uni_point in uni_points) + bfchar_map.append(f"<{cid_hex}> <{uni_hex}>") + + # Width mapping, but not for the 14 Adobe code fonts, which are dealt with elsewhere. + if self.name not in CORE_FONT_METRICS: + # The widths (/W) array can have two formats: + # [first_cid [w1 w2 w3]] or [first last width] + # Here we choose the first format and simply provide one array with one width for every cid. + width = self.character_widths.get(cast(str, src_id), self.character_widths["default"]) + widths_list.extend([NumberObject(cid), ArrayObject([NumberObject(width)])]) + + while partial_list := bfchar_map[:CMAP_MAX_ENTRIES_PER_GROUP]: + del bfchar_map[:CMAP_MAX_ENTRIES_PER_GROUP] + unicode_map.append(f"{len(partial_list)} beginbfchar") + unicode_map.extend(partial_list) + unicode_map.append("endbfchar") + + # Create the /ToUnicode CMap Stream + to_unicode_stream = StreamObject() + to_unicode_stream.set_data( + ( + "/CIDInit /ProcSet findresource begin\n" + "12 dict begin\n" + "begincmap\n" + f"{cid_system_info}" + f"/CMapName /{cmap_name} def\n" + f"/CMapType 2 def\n" + f"1 begincodespacerange <{codespace_min}> <{codespace_max}> endcodespacerange\n" + + "\n".join(unicode_map) + "\n" + "endcmap\n" + "CMapName currentdict /CMap defineresource pop\n" + "end end" + ).encode("ascii") + ) + + return widths_list, to_unicode_stream + + def as_font_resource(self) -> DictionaryObject: + # If we have an embedded Truetype font, we assume that we need to produce a Type 2 CID font resource. + # We check that we are 16-bit encoded, that is, Type0. + if self.font_descriptor.font_file and self.sub_type == "Type0": + # Begin with creating the widths array (part of the descendant font) and the unicode cmap (part + # of the Type 0 font object). + widths_list, to_unicode_stream = self._create_widths_list_and_unicode_stream() + + # Create the descendant font object + cid_font = DictionaryObject({ + NameObject("/Type"): NameObject("/Font"), + NameObject("/Subtype"): NameObject("/CIDFontType2"), + NameObject("/BaseFont"): NameObject(f"/{self.name}"), + NameObject("/CIDSystemInfo"): DictionaryObject({ + NameObject("/Registry"): TextStringObject("Adobe"), + NameObject("/Ordering"): TextStringObject("Identity"), + NameObject("/Supplement"): NumberObject(0) + }), + NameObject("/FontDescriptor"): self.font_descriptor.as_font_descriptor_resource(), + NameObject("/W"): ArrayObject(widths_list), + NameObject("/DW"): NumberObject(self.character_widths["default"]), + NameObject("/CIDToGIDMap"): NameObject("/Identity") + }) + + # Create the Type 0 font object + return DictionaryObject({ + NameObject("/Type"): NameObject("/Font"), + NameObject("/Subtype"): NameObject("/Type0"), + NameObject("/BaseFont"): NameObject(f"/{self.name}"), + NameObject("/Encoding"): NameObject("/Identity-H"), + NameObject("/DescendantFonts"): ArrayObject([cid_font]), + NameObject("/ToUnicode"): to_unicode_stream, + }) + + # Fallback: Return a font resource for one of the 14 Adobe Core fonts. + win_ansi_encoding = encoding_dict_from_named_encoding("cp1252") + differences_list: list[NumberObject | NameObject] = [] + reverse_adobe_glyphs = {value: key for key, value in adobe_glyphs.items()} + own_encoding: dict[int, str] = cast(dict[int, str], self.encoding) + for idx, character_code in win_ansi_encoding.items(): + encoding_char = own_encoding.get(idx) + if encoding_char and encoding_char != character_code: + differences_list.extend([NumberObject(idx), NameObject(reverse_adobe_glyphs[encoding_char])]) + + if differences_list: + encoding: DictionaryObject | NameObject = DictionaryObject({ + NameObject("/BaseEncoding"): NameObject("/WinAnsiEncoding"), + NameObject("/Differences"): ArrayObject(differences_list), + }) + else: + encoding = NameObject("/WinAnsiEncoding") + + simple_font = DictionaryObject({ + NameObject("/Type"): NameObject("/Font"), + NameObject("/Subtype"): NameObject("/Type1"), + NameObject("/Name"): NameObject(f"/{self.name}"), + NameObject("/BaseFont"): NameObject(f"/{self.name}"), + NameObject("/Encoding"): encoding + }) + + if differences_list: + _, to_unicode_stream = self._create_widths_list_and_unicode_stream() + simple_font[NameObject("/ToUnicode")] = to_unicode_stream + + return simple_font + + def _add_to_writer( + self, + writer: PdfWriter, + target_resource_dict: DictionaryObject, + font_resource_name: NameObject + ) -> IndirectObject: + """ + Some objects in a font resource need to be indirect objects. This method + ensures that ToUnicode, FontDescriptor, FontFile, and, ultimately, the font + resource itself, are registered with the PdfWriter instance as indirect objects. + """ + font_resource = self.as_font_resource() + if "/ToUnicode" in font_resource: + font_resource[NameObject("/ToUnicode")] = writer._add_object(font_resource["/ToUnicode"]) + + if "/DescendantFonts" in font_resource: + descendant_fonts = cast(ArrayObject, font_resource["/DescendantFonts"]) + font_resource_dict = cast(DictionaryObject, descendant_fonts[0]) + else: + font_resource_dict = font_resource + + if "/FontDescriptor" in font_resource_dict: + font_descriptor_obj = cast(DictionaryObject, font_resource_dict["/FontDescriptor"]) + for key in ["/FontFile", "/FontFile2", "/FontFile3"]: + if key in font_descriptor_obj: + font_descriptor_obj[NameObject(key)] = writer._add_object(font_descriptor_obj[key]) + font_resource_dict[NameObject("/FontDescriptor")] = writer._add_object( + font_resource_dict["/FontDescriptor"] + ) + font_resource_ref = writer._add_object(font_resource) + target_resource_dict[font_resource_name] = font_resource_ref + return font_resource_ref + + def get_text_width(self, text: str = "") -> float: + """Sum of character widths specified in PDF font for the supplied text.""" + return sum( + [self.character_widths.get(char, self.character_widths["default"]) for char in text], 0.0 + ) + + def can_encode(self, text: str) -> bool: + """Check whether the font is able to encode a text string.""" + if self.character_map: + supported_chars = set(self.character_map.values()) + return all(char in supported_chars for char in text) + + if isinstance(self.encoding, dict): + supported_chars = set(self.encoding.values()) + return all(char in supported_chars for char in text) + + # Not a simple font (encoding is not a dict), and missing ToUnicode cmap (no character_map). + # Assume we cannot use this font for text encoding. + return False diff --git a/app/vendor/pypdf/_page.py b/app/vendor/pypdf/_page.py new file mode 100644 index 0000000..7d3863f --- /dev/null +++ b/app/vendor/pypdf/_page.py @@ -0,0 +1,2652 @@ +# Copyright (c) 2006, Mathieu Fenniak +# Copyright (c) 2007, Ashish Kulkarni +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import math +from collections.abc import Iterable, Iterator, Sequence +from copy import deepcopy +from dataclasses import asdict, dataclass +from decimal import Decimal +from io import BytesIO +from pathlib import Path +from typing import ( + Any, + Callable, + Literal, + Optional, + Union, + cast, + overload, +) + +from ._configuration import get_configuration +from ._font import Font +from ._protocols import PdfCommonDocProtocol +from ._text_extraction import ( + _layout_mode, +) +from ._text_extraction._text_extractor import TextExtraction +from ._utils import ( + CompressedTransformationMatrix, + TransformationMatrixType, + _human_readable_bytes, + _TraversalState, + deprecate, + deprecate_no_replacement, + deprecate_with_replacement, + logger_warning, + matrix_multiply, +) +from .actions import Action, PageTrigger +from .constants import ( + _INLINE_IMAGE_KEY_MAPPING, + _INLINE_IMAGE_VALUE_MAPPING, + AnnotationDictionaryAttributes, + ImageAttributes, +) +from .constants import PageAttributes as PG +from .constants import Resources as RES +from .errors import PageSizeNotDefinedError, PdfReadError +from .generic import ( + ArrayObject, + ContentStream, + DictionaryObject, + EncodedStreamObject, + FloatObject, + IndirectObject, + NameObject, + NullObject, + NumberObject, + PdfObject, + RectangleObject, + StreamObject, + is_null_or_none, +) + +try: + from PIL.Image import Image + + pil_not_imported = False +except ImportError: + Image = object # type: ignore[assignment,misc,unused-ignore] # TODO: Remove unused-ignore on Python 3.10 + pil_not_imported = True # error will be raised only when using images + +MERGE_CROP_BOX = "cropbox" # DEPRECATED: Use pypdf.Configuration. + + +def _get_page_resources(obj: Any) -> DictionaryObject: + """Return the inherited /Resources, or an empty dictionary if malformed.""" + resources = obj.get_inherited(key=PG.RESOURCES, default=DictionaryObject()) + if is_null_or_none(resources): + return DictionaryObject() + if not isinstance(resources, DictionaryObject): + logger_warning( + "Page resources are not a dictionary: %(resources)s", + source=__name__, + resources=resources, + ) + return DictionaryObject() + return resources + + +def _get_rectangle(self: Any, name: str, defaults: Iterable[str]) -> RectangleObject: + retval: Union[RectangleObject, ArrayObject, IndirectObject, None] = self.get(name) + if isinstance(retval, RectangleObject): + return retval + if is_null_or_none(retval): + for d in defaults: + retval = self.get(d) + if retval is not None: + break + if isinstance(retval, IndirectObject): + retval = self.pdf.get_object(retval) + if not isinstance(retval, ArrayObject): + raise ValueError(f"Expected an array of four values for {name}, got {retval}") + if (length := len(retval)) != 4: + if length > 4: + # Keep backwards-compatibility with files previously written in a + # broken way by pypdf, which carried more than four values. + logger_warning( + "Expected four values, got %(length)d: %(retval)s", + source=__name__, + length=length, + retval=retval, + ) + retval = RectangleObject(tuple(retval[:4])) + else: + raise ValueError( + f"Expected four values for {name}, got {length}: {retval}" + ) + else: + retval = RectangleObject(retval) + _set_rectangle(self, name, retval) + return retval + + +def _set_rectangle(self: Any, name: str, value: Union[RectangleObject, float]) -> None: + if isinstance(value, (list, tuple)) and len(value) < 4: + # The getter tolerates more than four values for backwards compatibility + # but cannot do anything with fewer, so writing them would produce a page + # whose box cannot be read back. + raise ValueError( + f"Expected four values for {name}, got {len(value)}: {value}" + ) + self[NameObject(name)] = value + + +def _delete_rectangle(self: Any, name: str) -> None: + del self[name] + + +def _create_rectangle_accessor(name: str, fallback: Iterable[str]) -> property: + return property( + lambda self: _get_rectangle(self, name, fallback), + lambda self, value: _set_rectangle(self, name, value), + lambda self: _delete_rectangle(self, name), + ) + + +class Transformation: + """ + Represent a 2D transformation. + + The transformation between two coordinate systems is represented by a 3-by-3 + transformation matrix with the following form:: + + a b 0 + c d 0 + e f 1 + + Because a transformation matrix has only six elements that can be changed, + it is usually specified in PDF as the six-element array [ a b c d e f ]. + + Coordinate transformations are expressed as matrix multiplications:: + + a b 0 + [ x′ y′ 1 ] = [ x y 1 ] × c d 0 + e f 1 + + + Example: + >>> from pypdf import PdfWriter, Transformation + >>> page = PdfWriter().add_blank_page(800, 600) + >>> op = Transformation().scale(sx=2, sy=3).translate(tx=10, ty=20) + >>> page.add_transformation(op) + + """ + + def __init__(self, ctm: CompressedTransformationMatrix = (1, 0, 0, 1, 0, 0)) -> None: + self.ctm = ctm + + @property + def matrix(self) -> TransformationMatrixType: + """ + Return the transformation matrix as a tuple of tuples in the form: + + ((a, b, 0), (c, d, 0), (e, f, 1)) + """ + return ( + (self.ctm[0], self.ctm[1], 0), + (self.ctm[2], self.ctm[3], 0), + (self.ctm[4], self.ctm[5], 1), + ) + + @staticmethod + def compress(matrix: TransformationMatrixType) -> CompressedTransformationMatrix: + """ + Compresses the transformation matrix into a tuple of (a, b, c, d, e, f). + + Args: + matrix: The transformation matrix as a tuple of tuples. + + Returns: + A tuple representing the transformation matrix as (a, b, c, d, e, f) + + """ + return ( + matrix[0][0], + matrix[0][1], + matrix[1][0], + matrix[1][1], + matrix[2][0], + matrix[2][1], + ) + + def _to_cm(self) -> str: + # Returns the cm operation string for the given transformation matrix + return ( + f"{self.ctm[0]:.4f} {self.ctm[1]:.4f} {self.ctm[2]:.4f} " + f"{self.ctm[3]:.4f} {self.ctm[4]:.4f} {self.ctm[5]:.4f} cm" + ) + + def transform(self, m: "Transformation") -> "Transformation": + """ + Apply one transformation to another. + + Args: + m: a Transformation to apply. + + Returns: + A new ``Transformation`` instance + + Example: + >>> from pypdf import PdfWriter, Transformation + >>> height, width = 40, 50 + >>> page = PdfWriter().add_blank_page(800, 600) + >>> op = Transformation((1, 0, 0, -1, 0, height)) # vertical mirror + >>> op = Transformation().transform(Transformation((-1, 0, 0, 1, width, 0))) # horizontal mirror + >>> page.add_transformation(op) + + """ + ctm = Transformation.compress(matrix_multiply(self.matrix, m.matrix)) + return Transformation(ctm) + + def translate(self, tx: float = 0, ty: float = 0) -> "Transformation": + """ + Translate the contents of a page. + + Args: + tx: The translation along the x-axis. + ty: The translation along the y-axis. + + Returns: + A new ``Transformation`` instance + + """ + m = self.ctm + return Transformation(ctm=(m[0], m[1], m[2], m[3], m[4] + tx, m[5] + ty)) + + def scale( + self, sx: Optional[float] = None, sy: Optional[float] = None + ) -> "Transformation": + """ + Scale the contents of a page towards the origin of the coordinate system. + + Typically, that is the lower-left corner of the page. That can be + changed by translating the contents / the page boxes. + + Args: + sx: The scale factor along the x-axis. + sy: The scale factor along the y-axis. + + Returns: + A new Transformation instance with the scaled matrix. + + """ + if sx is None and sy is None: + raise ValueError("Either sx or sy must be specified") + if sx is None: + sx = sy + if sy is None: + sy = sx + assert sx is not None + assert sy is not None + op: TransformationMatrixType = ((sx, 0, 0), (0, sy, 0), (0, 0, 1)) + ctm = Transformation.compress(matrix_multiply(self.matrix, op)) + return Transformation(ctm) + + def rotate(self, rotation: float) -> "Transformation": + """ + Rotate the contents of a page. + + Args: + rotation: The angle of rotation in degrees. + + Returns: + A new ``Transformation`` instance with the rotated matrix. + + """ + rotation = math.radians(rotation) + op: TransformationMatrixType = ( + (math.cos(rotation), math.sin(rotation), 0), + (-math.sin(rotation), math.cos(rotation), 0), + (0, 0, 1), + ) + ctm = Transformation.compress(matrix_multiply(self.matrix, op)) + return Transformation(ctm) + + def __repr__(self) -> str: + return f"Transformation(ctm={self.ctm})" + + @overload + def apply_on(self, pt: list[float], as_object: bool = False) -> list[float]: + ... + + @overload + def apply_on( + self, pt: tuple[float, float], as_object: bool = False + ) -> tuple[float, float]: + ... + + def apply_on( + self, + pt: Union[tuple[float, float], list[float]], + as_object: bool = False, + ) -> Union[tuple[float, float], list[float]]: + """ + Apply the transformation matrix on the given point. + + Args: + pt: A tuple or list representing the point in the form (x, y). + as_object: If True, return items as FloatObject, otherwise as plain floats. + + Returns: + A tuple or list representing the transformed point in the form (x', y') + + """ + typ = FloatObject if as_object else float + pt1 = ( + typ(float(pt[0]) * self.ctm[0] + float(pt[1]) * self.ctm[2] + self.ctm[4]), + typ(float(pt[0]) * self.ctm[1] + float(pt[1]) * self.ctm[3] + self.ctm[5]), + ) + return list(pt1) if isinstance(pt, list) else pt1 + + +@dataclass +class ImageFile: + """ + Image within the PDF file. *This object is not designed to be built.* + + This object should not be modified except using :func:`ImageFile.replace` to replace the image with a new one. + """ + + name: str = "" + """ + Filename as identified within the PDF file. + + .. warning:: + + This value can contain arbitrary characters. Please make sure to sanitize it before + using it to write the file content to the disk for example. + """ + + data: bytes = b"" + """ + Data as bytes. + """ + + image: Optional[Image] = None + """ + Data as PIL image. + """ + + indirect_reference: Optional[IndirectObject] = None + """ + Reference to the object storing the stream. + """ + + is_inline: bool = False + """ + True if this is an inline image (~0~, ~1~, etc.). + """ + + is_displayed: bool = False + """ + True if this image is displayed in the page content stream. + """ + + def replace(self, new_image: Image, **kwargs: Any) -> None: + """ + Replace the image with a new PIL image. + + Args: + new_image (PIL.Image.Image): The new PIL image to replace the existing image. + **kwargs: Additional keyword arguments to pass to `Image.save()`. + + Raises: + TypeError: If the image is inline or in a PdfReader. + TypeError: If the image does not belong to a PdfWriter. + TypeError: If `new_image` is not a PIL Image. + + Note: + This method replaces the existing image with a new image. + It is not allowed for inline images or images within a PdfReader. + The `kwargs` parameter allows passing additional parameters + to `Image.save()`, such as quality. + + """ + if pil_not_imported: + raise ImportError( + "pillow is required to do image extraction. " + "It can be installed via 'pip install pypdf[image]'" + ) + + from ._reader import PdfReader # noqa: PLC0415 + from .generic import DictionaryObject, PdfObject # noqa: PLC0415 + from .generic._image_xobject import _xobj_to_image # noqa: PLC0415 + + if self.indirect_reference is None: + raise TypeError("Cannot update an inline image.") + if not hasattr(self.indirect_reference.pdf, "_id_translated"): + raise TypeError("Cannot update an image not belonging to a PdfWriter.") + if not isinstance(new_image, Image): + raise TypeError("new_image shall be a PIL Image") + b = BytesIO() + new_image.save(b, "PDF", **kwargs) + reader = PdfReader(b) + page_image = reader.pages[0].images[0] + assert page_image.indirect_reference is not None + self.indirect_reference.pdf._objects[self.indirect_reference.idnum - 1] = ( + page_image.indirect_reference.get_object() + ) + cast( + PdfObject, self.indirect_reference.get_object() + ).indirect_reference = self.indirect_reference + # change the object attributes + extension, byte_stream, img = _xobj_to_image( + cast(DictionaryObject, self.indirect_reference.get_object()), + pillow_parameters=kwargs, + ) + assert extension is not None + self.name = self.name[: self.name.rfind(".")] + extension + self.data = byte_stream + self.image = img + + def __str__(self) -> str: + return f"{self.__class__.__name__}(name={self.name}, data: {_human_readable_bytes(len(self.data))})" + + def __repr__(self) -> str: + return self.__str__()[:-1] + f", hash: {hash(self.data)})" + + +class VirtualListImages(Sequence[ImageFile]): + """ + Provides access to images referenced within a page. + Only one copy will be returned if the usage is used on the same page multiple times. + See :func:`PageObject.images` for more details. + """ + + def __init__( + self, + ids_function: Callable[[], list[Union[str, list[str]]]], + get_function: Callable[[Union[str, list[str], tuple[str]]], ImageFile], + ) -> None: + self.ids_function = ids_function + self.get_function = get_function + self.current = -1 + + def __len__(self) -> int: + return len(self.ids_function()) + + def keys(self) -> list[Union[str, list[str]]]: + return self.ids_function() + + def items(self) -> list[tuple[Union[str, list[str]], ImageFile]]: + return [(x, self[x]) for x in self.ids_function()] + + @overload + def __getitem__(self, index: Union[int, str, list[str]]) -> ImageFile: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[ImageFile]: + ... + + def __getitem__( + self, index: Union[int, slice, str, list[str], tuple[str]] + ) -> Union[ImageFile, Sequence[ImageFile]]: + lst = self.ids_function() + if isinstance(index, slice): + indices = range(*index.indices(len(self))) + lst = [lst[x] for x in indices] + cls = type(self) + return cls((lambda: lst), self.get_function) + if isinstance(index, (str, list, tuple)): + return self.get_function(index) + if not isinstance(index, int): + raise TypeError("Invalid sequence indices type") + len_self = len(lst) + if index < 0: + # support negative indexes + index += len_self + if not (0 <= index < len_self): + raise IndexError("Sequence index out of range") + return self.get_function(lst[index]) + + def __iter__(self) -> Iterator[ImageFile]: + for i in range(len(self)): + yield self[i] + + def __str__(self) -> str: + p = [f"Image_{i}={n}" for i, n in enumerate(self.ids_function())] + return f"[{', '.join(p)}]" + + +class PageObject(DictionaryObject): + """ + PageObject represents a single page within a PDF file. + + Typically these objects will be created by accessing the + :attr:`pages` property of the + :class:`PdfReader` class, but it is + also possible to create an empty page with the + :meth:`create_blank_page()` static method. + + Args: + pdf: PDF file the page belongs to. + indirect_reference: Stores the original indirect reference to + this object in its source PDF + + """ + + original_page: "PageObject" # very local use in writer when appending + + def __init__( + self, + pdf: Optional[PdfCommonDocProtocol] = None, + indirect_reference: Optional[IndirectObject] = None, + ) -> None: + DictionaryObject.__init__(self) + self.pdf = pdf + self._content_stream_images: Optional[dict[str, Optional[ImageFile]]] = None + self.indirect_reference = indirect_reference + if not is_null_or_none(indirect_reference): + assert indirect_reference is not None, "mypy" + self.update(cast(DictionaryObject, indirect_reference.get_object())) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Note: this function is overloaded to return the same results + as a DictionaryObject. + + Returns: + Hash considering type and value. + + """ + return hash( + (DictionaryObject, tuple(((k, v.hash_bin()) for k, v in self.items()))) + ) + + def hash_value_data(self) -> bytes: + data = super().hash_value_data() + data += f"{id(self)}".encode() + return data + + @property + def user_unit(self) -> float: + """ + A read-only positive number giving the size of user space units. + + It is in multiples of 1/72 inch. Hence a value of 1 means a user + space unit is 1/72 inch, and a value of 3 means that a user + space unit is 3/72 inch. + """ + return cast(float, self.get(PG.USER_UNIT, 1)) + + @staticmethod + def create_blank_page( + pdf: Optional[PdfCommonDocProtocol] = None, + width: Union[float, Decimal, None] = None, + height: Union[float, Decimal, None] = None, + ) -> "PageObject": + """ + Return a new blank page. + + If ``width`` or ``height`` is ``None``, try to get the page size + from the last page of *pdf*. + + Args: + pdf: PDF file the page is within. + width: The width of the new page expressed in default user + space units. + height: The height of the new page expressed in default user + space units. + + Returns: + The new blank page + + Raises: + PageSizeNotDefinedError: if ``pdf`` is ``None`` or contains + no page + + """ + page = PageObject(pdf) + + # Creates a new page (cf PDF Reference §7.7.3.3) + page.__setitem__(NameObject(PG.TYPE), NameObject("/Page")) + page.__setitem__(NameObject(PG.PARENT), NullObject()) + page.__setitem__(NameObject(PG.RESOURCES), DictionaryObject()) + if width is None or height is None: + if pdf is not None and len(pdf.pages) > 0: + lastpage = pdf.pages[len(pdf.pages) - 1] + width = lastpage.mediabox.width + height = lastpage.mediabox.height + else: + raise PageSizeNotDefinedError + page.__setitem__( + NameObject(PG.MEDIABOX), RectangleObject((0, 0, width, height)) + ) + + return page + + def _get_ids_image( + self, + obj: Optional[DictionaryObject] = None, + ancest: Optional[list[str]] = None, + call_stack: Optional[list[Any]] = None, + ) -> list[Union[str, list[str]]]: + if call_stack is None: + call_stack = [] + _i = getattr(obj, "indirect_reference", None) + if _i in call_stack: + return [] + call_stack.append(_i) + if self._content_stream_images is None: + self._content_stream_images = self._parse_images_from_content_stream() + if obj is None: + obj = self + if ancest is None: + ancest = [] + lst: list[Union[str, list[str]]] = [] + if ( + PG.RESOURCES not in obj or + is_null_or_none(resources := cast(DictionaryObject, obj[PG.RESOURCES])) or + RES.XOBJECT not in resources + ): + # Forms without XObject resources have no images inside them + if len(ancest) > 0: + return [] + # for inline images, cache dict entries are not None + return [image_name for image_name, image_value in self._content_stream_images.items() if image_value] + + x_object = resources[RES.XOBJECT].get_object() + if not isinstance(x_object, DictionaryObject): + logger_warning( + "XObject resources are not a dictionary: %(x_object)s", + source=__name__, + x_object=x_object, + ) + return [] + + # Iterate through all XObject resources + for o in x_object: + entry = x_object[o] + # Skip non-stream objects (only process StreamObject) + if not isinstance(entry, StreamObject): + continue + if entry.get(ImageAttributes.SUBTYPE, "") == "/Image": + # If it's an image, add it to lst for further processing + lst.append(o if len(ancest) == 0 else [*ancest, o]) + else: + # If it's a form, recursively search for images inside it + # Forms may contain images that are Do-referenced in their content stream + lst.extend(self._get_ids_image(entry, [*ancest, o], call_stack)) + + # Removes duplicates and preserves order + deduplicated = lst.copy() + + # Add inline images from _content_stream_images + for object_name, object_value in self._content_stream_images.items(): + # inline images have cache populated + if object_name not in deduplicated and object_value: + deduplicated.append(object_name) + + return deduplicated + + def _get_image( + self, + id: Union[str, list[str], tuple[str]], + obj: Optional[DictionaryObject] = None, + ) -> ImageFile: + if obj is None: + obj = cast(DictionaryObject, self) + if isinstance(id, tuple): + id = list(id) + if isinstance(id, list) and len(id) == 1: + id = id[0] + xobjs: Optional[DictionaryObject] = None + try: + xobjs = cast( + DictionaryObject, cast(DictionaryObject, obj[PG.RESOURCES])[RES.XOBJECT] + ) + except KeyError as exc: + if not (id[0] == "~" and id[-1] == "~"): + raise KeyError( + f"Cannot access image object {id} without XObject resources" + ) from exc + if isinstance(id, str): + if id[0] == "~" and id[-1] == "~": + if self._content_stream_images is None: + self._content_stream_images = self._parse_images_from_content_stream() + if id not in self._content_stream_images: + raise KeyError(f"Image {id} not found") + image_file = self._content_stream_images[id] + assert image_file is not None + return image_file + + # Do-referenced image name (non-inline string keys like /Im0) + assert xobjs is not None + if id not in xobjs: + raise KeyError(f"Image {id} not found") + xobj = cast(DictionaryObject, xobjs[id]) + if xobj.get(ImageAttributes.SUBTYPE, "") != "/Image": + raise KeyError(f"XObject {id} is not an image") + + # Check if displayed (in content stream) + is_displayed = self._content_stream_images is not None and id in self._content_stream_images + + from .generic._image_xobject import _xobj_to_image # noqa: PLC0415 + extension, byte_stream, img = _xobj_to_image(xobj) + return ImageFile( + name=f"{id[1:]}{extension}", + data=byte_stream, + image=img, + indirect_reference=xobj.indirect_reference, + is_inline=False, + is_displayed=is_displayed, + ) + # in a subobject + assert xobjs is not None + ids = id[1:] + return self._get_image(ids, cast(DictionaryObject, xobjs[id[0]])) + + @property + def images(self) -> VirtualListImages: + """ + Read-only property emulating a list of images on a page. + + Get a list of all images on the page. The key can be: + - A string (for the top object) + - A tuple (for images within XObject forms) + - An integer + + Examples: + * `reader.pages[0].images[0]` # return first image + * `reader.pages[0].images['/I0']` # return image '/I0' + * `reader.pages[0].images['/TP1','/Image1']` # return image '/Image1' within '/TP1' XObject form + * `for img in reader.pages[0].images:` # loops through all objects + + Example usage: + + >>> from pypdf import PdfWriter + >>> writer = PdfWriter() + >>> page = writer.add_blank_page(800, 600) + >>> images = page.images + + """ + return VirtualListImages(self._get_ids_image, self._get_image) + + @property + def inline_images(self) -> Optional[dict[str, ImageFile]]: + """ + Return only inline images from the page. + + .. deprecated:: + Use :attr:`images` and filter by :attr:`ImageFile.is_inline` instead. + This property will be removed in pypdf 7.0. + + Examples: + >>> from pypdf import PdfWriter + >>> writer = PdfWriter() + >>> page = writer.add_blank_page(800,600) + >>> for image_name, image_file in page.images.items(): + ... if image_file.is_inline: + ... print(f"{image_name} is inline") + """ + deprecate_with_replacement( + "PageObject.inline_images", + "PageObject.images", + "7.0.0", + ) + if self._content_stream_images is None: + return None + return { + image_name: image_file + for image_name, image_file in self._content_stream_images.items() + if image_file and image_file.is_inline # for inline images, image_file is populated + } + + @inline_images.setter + def inline_images(self, value: Optional[dict[str, ImageFile]]) -> None: + deprecate_no_replacement( + "PageObject.inline_images", + "7.0.0", + ) + if value is None: + self._content_stream_images = None + else: + assert self._content_stream_images is not None, "Can't edit inline_images before accessing images" + self._content_stream_images.update(value) + + def _translate_value_inline_image(self, k: str, v: PdfObject) -> PdfObject: + """Translate values used in inline image""" + try: + v = NameObject(_INLINE_IMAGE_VALUE_MAPPING[cast(str, v)]) + except (TypeError, KeyError): + if isinstance(v, NameObject): + # It is a custom name, thus we have to look in resources. + # The only applicable case is for ColorSpace. + try: + res = cast(DictionaryObject, self["/Resources"])["/ColorSpace"] + v = cast(DictionaryObject, res)[v] + except KeyError: # for res and v + raise PdfReadError(f"Cannot find resource entry {v} for {k}") + return v + + def _parse_images_from_content_stream(self) -> dict[str, Optional[ImageFile]]: + """Load images from content stream. Includes both inline images and Do-referenced images. + + This method scans the page content stream and extracts: + + 1. **Inline images** (~0~, ~1~...): Embedded directly in content stream via BI/EI operators + - is_inline=True, is_displayed=True, indirect_reference=None + + 2. **Do-referenced objects** (/Im0, /Im1..., /Form1...): Referenced via "Do" operator + - is_inline=False, is_displayed=True, indirect_reference= + + 3. **Pure XObject images** (/I0, /Image1...): Defined in Resources only (not in content stream) + - is_inline=False, is_displayed=False, indirect_reference= + + Returns: + Dictionary mapping names to ImageFile instances (inline) or None (Do-referenced). + """ + content = self.get_contents() + if is_null_or_none(content): + return {} + imgs_data = [] + do_image_names: list[bytes] = [] + assert content is not None, "mypy" + for param, ope in content.operations: + if ope == b"INLINE IMAGE": + imgs_data.append( + {"settings": param["settings"], "__streamdata__": param["data"]} + ) + elif ope == b"Do" and param: + do_image_names.append(param[0]) # First operand is the XObject name + elif ope in (b"BI", b"EI", b"ID"): # pragma: no cover + raise PdfReadError( + f"{ope!r} operator met whereas not expected, " + "please share use case with pypdf dev team" + ) + + files: dict[str, Optional[ImageFile]] = {} + # Process Do-referenced objects first (images + forms, no subtype check) + files = dict.fromkeys(list(map(str, do_image_names)), None) + + # Then process inline images + for num, ii in enumerate(imgs_data): + init = { + "__streamdata__": ii["__streamdata__"], + "/Length": len(ii["__streamdata__"]), + } + for k, v in ii["settings"].items(): + if k in {"/Length", "/L"}: # no length is expected + continue + if isinstance(v, list): + v = ArrayObject( + [self._translate_value_inline_image(k, x) for x in v] + ) + else: + v = self._translate_value_inline_image(k, v) + if k in _INLINE_IMAGE_KEY_MAPPING: + k = NameObject(_INLINE_IMAGE_KEY_MAPPING[k]) + else: + logger_warning( + "Unknown inline image key %(key)s, keeping it as-is.", + source=__name__, + key=k, + ) + k = NameObject(k) + if k not in init: + init[k] = v + ii["object"] = EncodedStreamObject.initialize_from_dictionary(init) + from .generic._image_xobject import _xobj_to_image # noqa: PLC0415 + extension, byte_stream, img = _xobj_to_image(ii["object"]) + files[f"~{num}~"] = ImageFile( + name=f"~{num}~{extension}", + data=byte_stream, + image=img, + indirect_reference=None, + is_inline=True, + is_displayed=True, + ) + + return files + + @property + def rotation(self) -> int: + """ + The visual rotation of the page. + + This number has to be a multiple of 90 degrees: 0, 90, 180, or 270 are + valid values. This property does not affect ``/Contents``. + """ + rotate_obj = self.get(PG.ROTATE, 0) + return rotate_obj if isinstance(rotate_obj, int) else rotate_obj.get_object() + + @rotation.setter + def rotation(self, r: float) -> None: + self[NameObject(PG.ROTATE)] = NumberObject((((int(r) + 45) // 90) * 90) % 360) + + def transfer_rotation_to_content(self) -> None: + """ + Apply the rotation of the page to the content and the media/crop/... + boxes. + + It is recommended to apply this function before page merging. + """ + r = -self.rotation # rotation to apply is in the otherway + self.rotation = 0 + mb = RectangleObject(self.mediabox) + trsf = ( + Transformation() + .translate( + -float(mb.left + mb.width / 2), -float(mb.bottom + mb.height / 2) + ) + .rotate(r) + ) + pt1 = trsf.apply_on(mb.lower_left) + pt2 = trsf.apply_on(mb.upper_right) + trsf = trsf.translate(-min(pt1[0], pt2[0]), -min(pt1[1], pt2[1])) + self.add_transformation(trsf, False) + for b in ["/MediaBox", "/CropBox", "/BleedBox", "/TrimBox", "/ArtBox"]: + if b in self: + rr = RectangleObject(self[b]) # type: ignore[arg-type] + pt1 = trsf.apply_on(rr.lower_left) + pt2 = trsf.apply_on(rr.upper_right) + self[NameObject(b)] = RectangleObject( + ( + min(pt1[0], pt2[0]), + min(pt1[1], pt2[1]), + max(pt1[0], pt2[0]), + max(pt1[1], pt2[1]), + ) + ) + + def rotate(self, angle: int) -> "PageObject": + """ + Rotate a page clockwise by increments of 90 degrees. + + Args: + angle: Angle to rotate the page. Must be an increment of 90 deg. + + Returns: + The rotated PageObject + + """ + if angle % 90 != 0: + raise ValueError("Rotation angle must be a multiple of 90") + self[NameObject(PG.ROTATE)] = NumberObject(self.rotation + angle) + return self + + def _merge_resources( + self, + res1: DictionaryObject, + res2: DictionaryObject, + resource: Any, + new_res1: bool = True, + ) -> tuple[dict[str, Any], dict[str, Any]]: + try: + assert isinstance(self.indirect_reference, IndirectObject) + pdf = self.indirect_reference.pdf + is_pdf_writer = hasattr( + pdf, "_add_object" + ) # expect isinstance(pdf, PdfWriter) + except (AssertionError, AttributeError): + pdf = None + is_pdf_writer = False + + def compute_unique_key(base_key: str) -> tuple[str, bool]: + """ + Find a key that either doesn't already exist or has the same value + (indicated by the bool) + + Args: + base_key: An index is added to this to get the computed key + + Returns: + A tuple (computed key, bool) where the boolean indicates + if there is a resource of the given computed_key with the same + value. + + """ + value = page2res.raw_get(base_key) + # TODO: a possible improvement for writer, the indirect_reference + # cannot be found because translated + + # try the current key first (e.g. "foo"), but otherwise iterate + # through "foo-0", "foo-1", etc. new_res can contain only finitely + # many keys, thus this'll eventually end, even if it's been crafted + # to be maximally annoying. + computed_key = base_key + idx = 0 + while computed_key in new_res: + if new_res.raw_get(computed_key) == value: + # there's already a resource of this name, with the exact + # same value + return computed_key, True + computed_key = f"{base_key}-{idx}" + idx += 1 + return computed_key, False + + if new_res1: + new_res = DictionaryObject() + new_res.update(res1.get(resource, DictionaryObject()).get_object()) + else: + new_res = cast(DictionaryObject, res1[resource]) + page2res = cast( + DictionaryObject, res2.get(resource, DictionaryObject()).get_object() + ) + rename_res = {} + for key in page2res: + unique_key, same_value = compute_unique_key(key) + newname = NameObject(unique_key) + if key != unique_key: + # we have to use a different name for this + rename_res[key] = newname + + if not same_value: + if is_pdf_writer: + new_res[newname] = page2res.raw_get(key).clone(pdf) + try: + new_res[newname] = new_res[newname].indirect_reference + except AttributeError: + pass + else: + new_res[newname] = page2res.raw_get(key) + lst = sorted(new_res.items()) + new_res.clear() + for el in lst: + new_res[el[0]] = el[1] + return new_res, rename_res + + @staticmethod + def _content_stream_rename( + stream: ContentStream, + rename: dict[Any, Any], + pdf: Optional[PdfCommonDocProtocol], + ) -> ContentStream: + if not rename: + return stream + stream = ContentStream(stream, pdf) + for operands, _operator in stream.operations: + if isinstance(operands, list): + for i, op in enumerate(operands): + if isinstance(op, NameObject): + operands[i] = rename.get(op, op) + elif isinstance(operands, dict): + for i, op in operands.items(): + if isinstance(op, NameObject): + operands[i] = rename.get(op, op) + else: + raise KeyError(f"Type of operands is {type(operands)}") + return stream + + @staticmethod + def _add_transformation_matrix( + contents: Any, + pdf: Optional[PdfCommonDocProtocol], + ctm: CompressedTransformationMatrix, + ) -> ContentStream: + """Add transformation matrix at the beginning of the given contents stream.""" + content_stream = ContentStream(contents, pdf) + content_stream.operations.insert( + 0, + ( + [FloatObject(x) for x in ctm], + b"cm", + ), + ) + return content_stream + + def _get_contents_as_bytes(self) -> Optional[bytes]: + """ + Return the page contents as bytes. + + Returns: + The ``/Contents`` object as bytes, or ``None`` if it doesn't exist. + + """ + if PG.CONTENTS in self: + obj = self[PG.CONTENTS].get_object() + if isinstance(obj, list): + return b"".join(x.get_object().get_data() for x in obj) + return cast(EncodedStreamObject, obj).get_data() + return None + + def get_contents(self) -> Optional[ContentStream]: + """ + Access the page contents. + + Returns: + The ``/Contents`` object, or ``None`` if it does not exist. + ``/Contents`` is optional, as described in §7.7.3.3 of the PDF Reference. + + """ + if PG.CONTENTS in self: + try: + pdf = cast(IndirectObject, self.indirect_reference).pdf + except AttributeError: + pdf = None + obj = self[PG.CONTENTS] + if is_null_or_none(obj): + return None + resolved_object = obj.get_object() + return ContentStream(resolved_object, pdf) + return None + + def replace_contents( + self, content: Union[ContentStream, EncodedStreamObject, ArrayObject, None] + ) -> None: + """ + Replace the page contents with the new content and nullify old objects + Args: + content: new content; if None delete the content field. + """ + if not hasattr(self, "indirect_reference") or self.indirect_reference is None: + # the page is not attached : the content is directly attached. + self[NameObject(PG.CONTENTS)] = content + return + + from pypdf._writer import PdfWriter # noqa: PLC0415 + if not isinstance(self.indirect_reference.pdf, PdfWriter): + deprecate( + "Calling `PageObject.replace_contents()` for pages not assigned to a writer is deprecated " + "and will be removed in pypdf 7.0.0. Attach the page to the writer first or use " + "`PdfWriter(clone_from=...)` directly. The existing approach has proved being unreliable." + ) + + writer = self.indirect_reference.pdf + if isinstance(self.get(PG.CONTENTS, None), ArrayObject): + content_array = cast(ArrayObject, self[PG.CONTENTS]) + for reference in content_array: + try: + writer._replace_object(indirect_reference=reference.indirect_reference, obj=NullObject()) + except ValueError: + # Occurs when called on PdfReader. + pass + + if isinstance(content, ArrayObject): + content = ArrayObject(writer._add_object(obj) for obj in content) + + if is_null_or_none(content): + if PG.CONTENTS not in self: + return + assert self[PG.CONTENTS].indirect_reference is not None + writer._replace_object(indirect_reference=self[PG.CONTENTS].indirect_reference, obj=NullObject()) + del self[PG.CONTENTS] + elif not hasattr(self.get(PG.CONTENTS, None), "indirect_reference"): + try: + self[NameObject(PG.CONTENTS)] = writer._add_object(content) + except AttributeError: + # applies at least for page not in writer + # as a backup solution, we put content as an object although not in accordance with pdf ref + # this will be fixed with the _add_object + self[NameObject(PG.CONTENTS)] = content + else: + assert content is not None, "mypy" + content.indirect_reference = self[ + PG.CONTENTS + ].indirect_reference # TODO: in the future may require generation management + try: + writer._replace_object(indirect_reference=content.indirect_reference, obj=content) + except AttributeError: + # applies at least for page not in writer + # as a backup solution, we put content as an object although not in accordance with pdf ref + # this will be fixed with the _add_object + self[NameObject(PG.CONTENTS)] = content + # forces recalculation of images + self._content_stream_images = None + + def merge_page( + self, page2: "PageObject", expand: bool = False, over: bool = True + ) -> None: + """ + Merge the content streams of two pages into one. + + Resource references (e.g. fonts) are maintained from both pages. + The mediabox, cropbox, etc of this page are not altered. + The parameter page's content stream will + be added to the end of this page's content stream, + meaning that it will be drawn after, or "on top" of this page. + + Args: + page2: The page to be merged into this one. Should be + an instance of :class:`PageObject`. + over: set the page2 content over page1 if True (default) else under + expand: If True, the current page dimensions will be + expanded to accommodate the dimensions of the page to be merged. + + """ + self._merge_page(page2, over=over, expand=expand) + + def _merge_page( + self, + page2: "PageObject", + page2_transformation: Optional[Callable[[Any], ContentStream]] = None, + ctm: Optional[CompressedTransformationMatrix] = None, + over: bool = True, + expand: bool = False, + ) -> None: + # First we work on merging the resource dictionaries. This allows us + # to find out what symbols in the content streams we might need to + # rename. + try: + assert isinstance(self.indirect_reference, IndirectObject) + if hasattr(self.indirect_reference.pdf, "_add_object"): # to detect PdfWriter + return self._merge_page_writer( + page2, page2_transformation, ctm, over, expand + ) + except (AssertionError, AttributeError): + pass + + new_resources = DictionaryObject() + rename: dict[str, Any] = {} + original_resources = cast(DictionaryObject, self.get(PG.RESOURCES, DictionaryObject()).get_object()) + page2_resources = cast(DictionaryObject, page2.get(PG.RESOURCES, DictionaryObject()).get_object()) + new_annots = ArrayObject() + + for page in (self, page2): + if PG.ANNOTS in page: + annots = page[PG.ANNOTS] + if isinstance(annots, ArrayObject): + new_annots.extend(annots) + self[NameObject(PG.ANNOTS)] = new_annots + + for res in ( + RES.EXT_G_STATE, + RES.COLOR_SPACE, + RES.PATTERN, + RES.SHADING, + RES.XOBJECT, + RES.FONT, + RES.PROPERTIES, + ): + new, new_resource_name = self._merge_resources( + original_resources, page2_resources, res + ) + if new: + new_resources[NameObject(res)] = new + rename.update(new_resource_name) + + # Combine /ProcSet sets, making sure there is a consistent order + new_resources[NameObject(RES.PROC_SET)] = ArrayObject( + sorted( + set( + original_resources.get(RES.PROC_SET, ArrayObject()).get_object() + ).union( + set(page2_resources.get(RES.PROC_SET, ArrayObject()).get_object()) + ) + ) + ) + + new_content_array = ArrayObject() + original_content = self.get_contents() + if original_content is not None: + original_content.isolate_graphics_state() + new_content_array.append(original_content) + + page2_content = page2.get_contents() + if page2_content is not None: + configuration = get_configuration() + rect = getattr(page2, configuration.page_merge_box) + page2_content.operations.insert( + 0, + ( + map( + FloatObject, + [ + rect.left, + rect.bottom, + rect.width, + rect.height, + ], + ), + b"re", + ), + ) + page2_content.operations.insert(1, ([], b"W")) + page2_content.operations.insert(2, ([], b"n")) + if page2_transformation is not None: + page2_content = page2_transformation(page2_content) + page2_content = PageObject._content_stream_rename( + page2_content, rename, self.pdf + ) + page2_content.isolate_graphics_state() + if over: + new_content_array.append(page2_content) + else: + new_content_array.insert(0, page2_content) + + # if expanding the page to fit a new page, calculate the new media box size + if expand: + self._expand_mediabox(page2, ctm) + + self.replace_contents(ContentStream(new_content_array, self.pdf)) + self[NameObject(PG.RESOURCES)] = new_resources + + return None + + def _merge_page_writer( + self, + page2: "PageObject", + page2transformation: Optional[Callable[[Any], ContentStream]] = None, + ctm: Optional[CompressedTransformationMatrix] = None, + over: bool = True, + expand: bool = False, + ) -> None: + # First we work on merging the resource dictionaries. This allows us + # to find which symbols in the content streams we might need to + # rename. + assert isinstance(self.indirect_reference, IndirectObject) + pdf = self.indirect_reference.pdf + + if PG.RESOURCES not in self: + self[NameObject(PG.RESOURCES)] = DictionaryObject() + original_resources = cast(DictionaryObject, self[PG.RESOURCES].get_object()) + if PG.RESOURCES not in page2: + page2resources = DictionaryObject() + else: + page2resources = cast(DictionaryObject, page2[PG.RESOURCES].get_object()) + + rename = {} + for res in ( + RES.EXT_G_STATE, + RES.COLOR_SPACE, + RES.PATTERN, + RES.SHADING, + RES.XOBJECT, + RES.FONT, + RES.PROPERTIES, + ): + if res in page2resources: + if res not in original_resources: + original_resources[NameObject(res)] = DictionaryObject() + _, newrename = self._merge_resources( + original_resources, page2resources, res, False + ) + rename.update(newrename) + # Combine /ProcSet sets + if RES.PROC_SET in page2resources: + if RES.PROC_SET not in original_resources: + original_resources[NameObject(RES.PROC_SET)] = ArrayObject() + arr = cast(ArrayObject, original_resources[RES.PROC_SET]) + for x in cast(ArrayObject, page2resources[RES.PROC_SET]): + if x not in arr: + arr.append(x) + arr.sort() + + if not is_null_or_none(page2.get(PG.ANNOTS, None)): + if PG.ANNOTS not in self: + self[NameObject(PG.ANNOTS)] = ArrayObject() + annots = cast(ArrayObject, self[PG.ANNOTS].get_object()) + if ctm is None: + trsf = Transformation() + else: + trsf = Transformation(ctm) + # Ensure we are working on a copy of the list. Otherwise, if both pages + # are the same object, we might run into an infinite loop. + for a in cast(ArrayObject, deepcopy(page2[PG.ANNOTS])): + a = a.get_object() + aa = a.clone( + pdf, + ignore_fields=("/P", "/StructParent", "/Parent"), + force_duplicate=True, + ) + r = cast(ArrayObject, a["/Rect"]) + pt1 = trsf.apply_on((r[0], r[1]), True) + pt2 = trsf.apply_on((r[2], r[3]), True) + aa[NameObject("/Rect")] = ArrayObject( + ( + min(pt1[0], pt2[0]), + min(pt1[1], pt2[1]), + max(pt1[0], pt2[0]), + max(pt1[1], pt2[1]), + ) + ) + if "/QuadPoints" in a: + q = cast(ArrayObject, a["/QuadPoints"]) + aa[NameObject("/QuadPoints")] = ArrayObject( + trsf.apply_on((q[0], q[1]), True) + + trsf.apply_on((q[2], q[3]), True) + + trsf.apply_on((q[4], q[5]), True) + + trsf.apply_on((q[6], q[7]), True) + ) + # The /Rect update above only repositions and resizes the + # annotation's bounding box; it does not touch the + # appearance stream's own coordinate system. See + # transform_annotation_appearance for why that matters. + from pypdf.generic._appearance_stream import ( # noqa: PLC0415 + transform_annotation_appearance, + ) + transform_annotation_appearance(aa, trsf) + try: + aa["/Popup"][NameObject("/Parent")] = aa.indirect_reference + except KeyError: + pass + try: + aa[NameObject("/P")] = self.indirect_reference + annots.append(aa.indirect_reference) + except AttributeError: + pass + + new_content_array = ArrayObject() + original_content = self.get_contents() + if original_content is not None: + original_content.isolate_graphics_state() + new_content_array.append(original_content) + + page2content = page2.get_contents() + if page2content is not None: + configuration = get_configuration() + rect = getattr(page2, configuration.page_merge_box) + page2content.operations.insert( + 0, + ( + map( + FloatObject, + [ + rect.left, + rect.bottom, + rect.width, + rect.height, + ], + ), + b"re", + ), + ) + page2content.operations.insert(1, ([], b"W")) + page2content.operations.insert(2, ([], b"n")) + if page2transformation is not None: + page2content = page2transformation(page2content) + page2content = PageObject._content_stream_rename( + page2content, rename, self.pdf + ) + page2content.isolate_graphics_state() + if over: + new_content_array.append(page2content) + else: + new_content_array.insert(0, page2content) + + # if expanding the page to fit a new page, calculate the new media box size + if expand: + self._expand_mediabox(page2, ctm) + + self.replace_contents(new_content_array) + + def _expand_mediabox( + self, page2: "PageObject", ctm: Optional[CompressedTransformationMatrix] + ) -> None: + corners1 = ( + self.mediabox.left.as_numeric(), + self.mediabox.bottom.as_numeric(), + self.mediabox.right.as_numeric(), + self.mediabox.top.as_numeric(), + ) + corners2 = ( + page2.mediabox.left.as_numeric(), + page2.mediabox.bottom.as_numeric(), + page2.mediabox.left.as_numeric(), + page2.mediabox.top.as_numeric(), + page2.mediabox.right.as_numeric(), + page2.mediabox.top.as_numeric(), + page2.mediabox.right.as_numeric(), + page2.mediabox.bottom.as_numeric(), + ) + if ctm is not None: + ctm = tuple(float(x) for x in ctm) # type: ignore[assignment] + new_x = tuple( + ctm[0] * corners2[i] + ctm[2] * corners2[i + 1] + ctm[4] + for i in range(0, 8, 2) + ) + new_y = tuple( + ctm[1] * corners2[i] + ctm[3] * corners2[i + 1] + ctm[5] + for i in range(0, 8, 2) + ) + else: + new_x = corners2[0:8:2] + new_y = corners2[1:8:2] + lowerleft = (min(new_x), min(new_y)) + upperright = (max(new_x), max(new_y)) + lowerleft = (min(corners1[0], lowerleft[0]), min(corners1[1], lowerleft[1])) + upperright = ( + max(corners1[2], upperright[0]), + max(corners1[3], upperright[1]), + ) + + self.mediabox.lower_left = lowerleft + self.mediabox.upper_right = upperright + + def merge_transformed_page( + self, + page2: "PageObject", + ctm: Union[CompressedTransformationMatrix, Transformation], + over: bool = True, + expand: bool = False, + ) -> None: + """ + Similar to :meth:`~pypdf._page.PageObject.merge_page`, but a transformation + matrix is applied to the merged stream. + + Args: + page2: The page to be merged into this one. + ctm: a 6-element tuple containing the operands of the + transformation matrix + over: set the page2 content over page1 if True (default) else under + expand: Whether the page should be expanded to fit the dimensions + of the page to be merged. + + """ + if isinstance(ctm, Transformation): + ctm = ctm.ctm + self._merge_page( + page2, + lambda page2_content: PageObject._add_transformation_matrix( + page2_content, page2.pdf, ctm + ), + ctm, + over, + expand, + ) + + def merge_scaled_page( + self, page2: "PageObject", scale: float, over: bool = True, expand: bool = False + ) -> None: + """ + Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be merged + is scaled by applying a transformation matrix. + + Args: + page2: The page to be merged into this one. + scale: The scaling factor + over: set the page2 content over page1 if True (default) else under + expand: Whether the page should be expanded to fit the + dimensions of the page to be merged. + + """ + op = Transformation().scale(scale, scale) + self.merge_transformed_page(page2, op, over, expand) + + def merge_rotated_page( + self, + page2: "PageObject", + rotation: float, + over: bool = True, + expand: bool = False, + ) -> None: + """ + Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be merged + is rotated by applying a transformation matrix. + + Args: + page2: The page to be merged into this one. + rotation: The angle of the rotation, in degrees + over: set the page2 content over page1 if True (default) else under + expand: Whether the page should be expanded to fit the + dimensions of the page to be merged. + + """ + op = Transformation().rotate(rotation) + self.merge_transformed_page(page2, op, over, expand) + + def merge_translated_page( + self, + page2: "PageObject", + tx: float, + ty: float, + over: bool = True, + expand: bool = False, + ) -> None: + """ + Similar to :meth:`~pypdf._page.PageObject.merge_page`, but the stream to be + merged is translated by applying a transformation matrix. + + Args: + page2: the page to be merged into this one. + tx: The translation on X axis + ty: The translation on Y axis + over: set the page2 content over page1 if True (default) else under + expand: Whether the page should be expanded to fit the + dimensions of the page to be merged. + + """ + op = Transformation().translate(tx, ty) + self.merge_transformed_page(page2, op, over, expand) + + def add_transformation( + self, + ctm: Union[Transformation, CompressedTransformationMatrix], + expand: bool = False, + ) -> None: + """ + Apply a transformation matrix to the page. + + Args: + ctm: A 6-element tuple containing the operands of the + transformation matrix. Alternatively, a + :py:class:`Transformation` + object can be passed. + + See :doc:`/user/cropping-and-transforming`. + + """ + if isinstance(ctm, Transformation): + ctm = ctm.ctm + content = self.get_contents() + if content is not None: + content = PageObject._add_transformation_matrix(content, self.pdf, ctm) + content.isolate_graphics_state() + self.replace_contents(content) + # if expanding the page to fit a new page, calculate the new media box size + if expand: + corners = [ + self.mediabox.left.as_numeric(), + self.mediabox.bottom.as_numeric(), + self.mediabox.left.as_numeric(), + self.mediabox.top.as_numeric(), + self.mediabox.right.as_numeric(), + self.mediabox.top.as_numeric(), + self.mediabox.right.as_numeric(), + self.mediabox.bottom.as_numeric(), + ] + + ctm = tuple(float(x) for x in ctm) # type: ignore[assignment] + new_x = [ + ctm[0] * corners[i] + ctm[2] * corners[i + 1] + ctm[4] + for i in range(0, 8, 2) + ] + new_y = [ + ctm[1] * corners[i] + ctm[3] * corners[i + 1] + ctm[5] + for i in range(0, 8, 2) + ] + + self.mediabox.lower_left = (min(new_x), min(new_y)) + self.mediabox.upper_right = (max(new_x), max(new_y)) + + def scale(self, sx: float, sy: float) -> None: + """ + Scale a page by the given factors by applying a transformation matrix + to its content and updating the page size. + + This updates the various page boundaries (bleedbox, trimbox, etc.) + and the contents of the page. + + Args: + sx: The scaling factor on horizontal axis. + sy: The scaling factor on vertical axis. + + """ + self.add_transformation((sx, 0, 0, sy, 0, 0)) + self.bleedbox = self.bleedbox.scale(sx, sy) + self.trimbox = self.trimbox.scale(sx, sy) + self.artbox = self.artbox.scale(sx, sy) + self.cropbox = self.cropbox.scale(sx, sy) + self.mediabox = self.mediabox.scale(sx, sy) + + if PG.ANNOTS in self: + annotations = self[PG.ANNOTS] + if isinstance(annotations, ArrayObject): + for annotation in annotations: + annotation_obj = annotation.get_object() + if AnnotationDictionaryAttributes.Rect in annotation_obj: + rectangle = annotation_obj[AnnotationDictionaryAttributes.Rect] + if isinstance(rectangle, ArrayObject): + rectangle[0] = FloatObject(float(rectangle[0]) * sx) + rectangle[1] = FloatObject(float(rectangle[1]) * sy) + rectangle[2] = FloatObject(float(rectangle[2]) * sx) + rectangle[3] = FloatObject(float(rectangle[3]) * sy) + + if PG.VP in self: + viewport = self[PG.VP] + if isinstance(viewport, ArrayObject): + bbox = viewport[0]["/BBox"] + else: + bbox = viewport["/BBox"] # type: ignore[index] + scaled_bbox = RectangleObject( + ( + float(bbox[0]) * sx, + float(bbox[1]) * sy, + float(bbox[2]) * sx, + float(bbox[3]) * sy, + ) + ) + if isinstance(viewport, ArrayObject): + self[NameObject(PG.VP)][NumberObject(0)][ # type: ignore[index] + NameObject("/BBox") + ] = scaled_bbox + else: + self[NameObject(PG.VP)][NameObject("/BBox")] = scaled_bbox # type: ignore[index] + + def scale_by(self, factor: float) -> None: + """ + Scale a page by the given factor by applying a transformation matrix to + its content and updating the page size. + + Args: + factor: The scaling factor (for both X and Y axis). + + """ + self.scale(factor, factor) + + def scale_to(self, width: float, height: float) -> None: + """ + Scale a page to the specified dimensions by applying a transformation + matrix to its content and updating the page size. + + Args: + width: The new width. + height: The new height. + + """ + sx = width / float(self.mediabox.width) + sy = height / float(self.mediabox.height) + self.scale(sx, sy) + + def compress_content_streams(self, level: int = -1) -> None: + """ + Compress the size of this page by joining all content streams and + applying a FlateDecode filter. + + However, it is possible that this function will perform no action if + content stream compression becomes "automatic". + """ + content = self.get_contents() + if content is not None: + content_obj = content.flate_encode(level) + try: + content.indirect_reference.pdf._objects[ # type: ignore[union-attr] + content.indirect_reference.idnum - 1 # type: ignore[union-attr] + ] = content_obj + except AttributeError: + if self.indirect_reference is not None and hasattr( + self.indirect_reference.pdf, "_add_object" + ): + self.replace_contents(content_obj) + else: + raise ValueError("Page must be part of a PdfWriter") + + @property + def page_number(self) -> Optional[int]: + """ + Read-only property which returns the page number within the PDF file. + + Returns: + Page number; None if the page is not attached to a PDF. + + """ + if self.indirect_reference is None: + return None + try: + lst = self.indirect_reference.pdf.pages + return int(lst.index(self)) + except ValueError: + return None + + def _debug_for_extract(self) -> str: # pragma: no cover + out = "" + for ope, op in ContentStream( + self["/Contents"].get_object(), self.pdf, "bytes" + ).operations: + if op == b"TJ": + s = [x for x in ope[0] if isinstance(x, str)] + else: + s = [] + out += op.decode("utf-8") + " " + "".join(s) + ope.__repr__() + "\n" + out += "\n=============================\n" + try: + for fo in self[PG.RESOURCES]["/Font"]: # type:ignore + out += fo + "\n" + out += self[PG.RESOURCES]["/Font"][fo].__repr__() + "\n" # type:ignore + try: + enc_repr = self[PG.RESOURCES]["/Font"][fo][ # type:ignore + "/Encoding" + ].__repr__() + out += enc_repr + "\n" + except Exception: + pass + try: + out += ( + self[PG.RESOURCES]["/Font"][fo][ # type:ignore + "/ToUnicode" + ] + .get_data() + .decode() + + "\n" + ) + except Exception: + pass + + except KeyError: + out += "No Font\n" + return out + + def _extract_text( + self, + obj: DictionaryObject, + pdf: Any, + orientations: tuple[int, ...] = (0, 90, 180, 270), + space_width: float = 200.0, + content_key: Optional[str] = PG.CONTENTS, + visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None, + visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None, + visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, + *, + known_ids: Optional[set[int]] = None, + traversal_state: Optional[_TraversalState] = None + ) -> str: + """ + See extract_text for most arguments. + + Args: + content_key: indicate the default key where to extract data + None = the object; this allows reusing the function on an XObject + default = "/Content" + + """ + if known_ids is None: + known_ids = set() + if traversal_state is None: + traversal_state = _TraversalState() + + extractor = TextExtraction() + font_resources: dict[str, DictionaryObject] = {} + fonts: dict[str, Font] = {} + + resources_dict = _get_page_resources(obj) + if not resources_dict: + # No resources means no text is possible (no font); we consider the + # file as not damaged, no need to check for TJ or Tj + return "" + + if ( + "/Font" in resources_dict + and (font_resources_dict := cast(DictionaryObject, resources_dict["/Font"])) + ): + for font_resource in font_resources_dict: + try: + font_resource_object = cast(DictionaryObject, font_resources_dict[font_resource].get_object()) + font_resources[font_resource] = font_resource_object + fonts[font_resource] = Font.from_font_resource(font_resource_object) + # Override space width, if applicable + if fonts[font_resource].character_widths.get(fonts[font_resource].space_char, 0) == 0: + fonts[font_resource].space_width = space_width + except (AttributeError, TypeError): + pass + + try: + content = ( + obj[content_key].get_object() if isinstance(content_key, str) else obj + ) + if not isinstance(content, ContentStream): + content = ContentStream(content, pdf, "bytes") + except (AttributeError, KeyError): # no content can be extracted (certainly empty page) + return "" + # We check all strings are TextStringObjects. ByteStringObjects + # are strings where the byte->string encoding was unknown, so adding + # them to the text here would be gibberish. + + # Initialize the extractor with the necessary parameters + extractor.initialize_extraction(orientations, visitor_text, font_resources, fonts) + + for operands, operator in content.operations: + if visitor_operand_before is not None: + visitor_operand_before(operator, operands, extractor.cm_matrix, extractor.tm_matrix) + # Multiple operators are handled here + if operator == b"'": + extractor.process_operation(b"T*", []) + extractor.process_operation(b"Tj", operands) + elif operator == b'"' and len(operands) >= 3: + extractor.process_operation(b"Tw", [operands[0]]) + extractor.process_operation(b"Tc", [operands[1]]) + extractor.process_operation(b"T*", []) + extractor.process_operation(b"Tj", operands[2:]) + elif operator == b"TJ": + # The space width may be smaller than the font width, so the width should be 95%. + _confirm_space_width = extractor._space_width * 0.95 + if operands: + for op in operands[0]: + if isinstance(op, (str, bytes)): + extractor.process_operation(b"Tj", [op]) + if isinstance(op, (int, float, NumberObject, FloatObject)) and ( + abs(float(op)) >= _confirm_space_width + and extractor.text + and extractor.text[-1] != " " + ): + extractor.process_operation(b"Tj", [" "]) + elif operator == b"TD" and len(operands) >= 2: + extractor.process_operation(b"TL", [-operands[1]]) + extractor.process_operation(b"Td", operands) + elif operator == b"Do": + extractor.output += extractor.text + if visitor_text is not None: + visitor_text( + extractor.text, + extractor.memo_cm, + extractor.memo_tm, + extractor.font_resource, + extractor.font_size, + ) + try: + if extractor.output[-1] != "\n": + extractor.output += "\n" + if visitor_text is not None: + visitor_text( + "\n", + extractor.memo_cm, + extractor.memo_tm, + extractor.font_resource, + extractor.font_size, + ) + except IndexError: + pass + try: + xform_text = self._extract_text__xform( + resources_dict=resources_dict, + operands=operands, + orientations=orientations, + space_width=space_width, + visitor_operand_before=visitor_operand_before, + visitor_operand_after=visitor_operand_after, + visitor_text=visitor_text, + known_ids=known_ids, + traversal_state=traversal_state + ) + if xform_text is not None: + text = xform_text + extractor.output += text + if visitor_text is not None: + visitor_text( + text, + extractor.memo_cm, + extractor.memo_tm, + extractor.font_resource, + extractor.font_size, + ) + except Exception as exception: + logger_warning( + "Impossible to decode XFormObject %(operand)s: %(exception)s", + source=__name__, + operand=operands[0], + exception=exception, + ) + finally: + extractor.text = "" + extractor.memo_cm = extractor.cm_matrix.copy() + extractor.memo_tm = extractor.tm_matrix.copy() + else: + extractor.process_operation(operator, operands) + if visitor_operand_after is not None: + visitor_operand_after(operator, operands, extractor.cm_matrix, extractor.tm_matrix) + extractor.output += extractor.text # just in case + if extractor.text != "" and visitor_text is not None: + visitor_text( + extractor.text, + extractor.memo_cm, + extractor.memo_tm, + extractor.font_resource, + extractor.font_size, + ) + return extractor.output + + def _extract_text__xform( + self, + *, + resources_dict: DictionaryObject, + operands: Any, + orientations: tuple[int, ...] = (0, 90, 180, 270), + space_width: float = 200.0, + visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None, + visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None, + visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, + known_ids: set[int], + traversal_state: _TraversalState + ) -> Optional[str]: + xobj = cast(DictionaryObject, resources_dict["/XObject"]) + xform = cast(EncodedStreamObject, xobj[operands[0]]) + if xform["/Subtype"] == NameObject("/Image"): + return None + + xform_id = id(xform) + if xform_id in known_ids: + logger_warning( + "Detected cyclic form XObject reference, skipping %(operand)s.", + source=__name__, + operand=operands[0] + ) + return "" + + configuration = get_configuration() + if traversal_state.entry_count >= configuration.xform_maximum_invocations_per_extraction: + if not traversal_state.has_logged: + traversal_state.has_logged = True + logger_warning( + ( + "Exceeded %(limit)d form XObject invocations while extracting text; " + "further form content is skipped." + ), + source=__name__, + limit=configuration.xform_maximum_invocations_per_extraction + ) + return "" + + traversal_state.entry_count += 1 + known_ids.add(xform_id) + try: + text = self.extract_xform_text( + xform, + orientations, + space_width, + visitor_operand_before, + visitor_operand_after, + visitor_text, + known_ids=known_ids, + traversal_state=traversal_state, + ) + finally: + known_ids.discard(xform_id) + return text + + def _layout_mode_fonts(self) -> dict[str, Font]: + """ + Get fonts formatted for "layout" mode text extraction. + + Returns: + Dict[str, Font]: dictionary of Font instances keyed by font name + + """ + # Font retrieval logic adapted from pypdf.PageObject._extract_text() + obj: Any = self + fonts: dict[str, Font] = {} + visited: set[int] = set() + while True: + obj_id = id(obj) + if obj_id in visited: + logger_warning("Detected cycle in /Parent hierarchy when retrieving fonts.", source=__name__) + break + visited.add(obj_id) + + resources_dict: Any = obj.get(PG.RESOURCES, {}) + if "/Font" in resources_dict and self.pdf is not None: + for font_name in resources_dict["/Font"]: + fonts[font_name] = Font.from_font_resource( + resources_dict["/Font"][font_name].get_object() + ) + + if "/Parent" not in obj: + break + obj = obj["/Parent"].get_object() + + return fonts + + def _layout_mode_text( + self, + space_vertically: bool = True, + scale_weight: float = 1.25, + strip_rotated: bool = True, + debug_path: Optional[Path] = None, + font_height_weight: float = 1, + ) -> str: + """ + Get text preserving fidelity to source PDF text layout. + + Args: + space_vertically: include blank lines inferred from y distance + font + height. Defaults to True. + scale_weight: multiplier for string length when calculating weighted + average character width. Defaults to 1.25. + strip_rotated: Removes text that is rotated w.r.t. to the page from + layout mode output. Defaults to True. + debug_path (Path | None): if supplied, must target a directory. + creates the following files with debug information for layout mode + functions if supplied: + - fonts.json: output of self._layout_mode_fonts + - tjs.json: individual text render ops with corresponding transform matrices + - bts.json: text render ops left justified and grouped by BT/ET operators + - bt_groups.json: BT/ET operations grouped by rendered y-coord (aka lines) + Defaults to None. + font_height_weight: multiplier for font height when calculating + blank lines. Defaults to 1. + + Returns: + str: multiline string containing page text in a fixed width format that + closely adheres to the rendered layout in the source pdf. + + """ + fonts = self._layout_mode_fonts() + if debug_path: # pragma: no cover + import json # noqa: PLC0415 + + debug_path.joinpath("fonts.json").write_text( + json.dumps(fonts, indent=2, default=asdict), + "utf-8" + ) + + ops = iter( + ContentStream(self["/Contents"].get_object(), self.pdf, "bytes").operations + ) + bt_groups = _layout_mode.text_show_operations( + ops, fonts, strip_rotated, debug_path + ) + + if not bt_groups: + return "" + + ty_groups = _layout_mode.y_coordinate_groups(bt_groups, debug_path) + + char_width = _layout_mode.fixed_char_width(bt_groups, scale_weight) + + return _layout_mode.fixed_width_page(ty_groups, char_width, space_vertically, font_height_weight) + + def extract_text( + self, + *args: Any, + orientations: Union[int, tuple[int, ...]] = (0, 90, 180, 270), + space_width: float = 200.0, + visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None, + visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None, + visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, + extraction_mode: Literal["plain", "layout"] = "plain", + **kwargs: Any, + ) -> str: + """ + Locate all text drawing commands, in the order they are provided in the + content stream, and extract the text. + + This works well for some PDF files, but poorly for others, depending on + the generator used. This will be refined in the future. + + Do not rely on the order of text coming out of this function, as it + will change if this function is made more sophisticated. + + Arabic and Hebrew are extracted in the correct order. + If required a custom RTL range of characters can be defined; + see function set_custom_rtl. + + Additionally you can provide visitor methods to get informed on all + operations and all text objects. + For example in some PDF files this can be useful to parse tables. + + Args: + orientations: list of orientations extract_text will look for + default = (0, 90, 180, 270) + note: currently only 0 (up),90 (turned left), 180 (upside down), + 270 (turned right) + Silently ignored in "layout" mode. + space_width: force default space width + if not extracted from font (default: 200) + Silently ignored in "layout" mode. + visitor_operand_before: function to be called before processing an operation. + It has four arguments: operator, operand-arguments, + current transformation matrix and text matrix. + Ignored with a warning in "layout" mode. + visitor_operand_after: function to be called after processing an operation. + It has four arguments: operator, operand-arguments, + current transformation matrix and text matrix. + Ignored with a warning in "layout" mode. + visitor_text: function to be called when extracting some text at some position. + It has five arguments: text, current transformation matrix, + text matrix, font-dictionary and font-size. + The font-dictionary may be None in case of unknown fonts. + If not None it may e.g. contain key "/BaseFont" with value "/Arial,Bold". + Ignored with a warning in "layout" mode. + extraction_mode (Literal["plain", "layout"]): "plain" for legacy functionality, + "layout" for experimental layout mode functionality. + NOTE: orientations, space_width, and visitor_* parameters are NOT respected + in "layout" mode. + + kwargs: + layout_mode_space_vertically (bool): include blank lines inferred from + y distance + font height. Defaults to True. + layout_mode_scale_weight (float): multiplier for string length when calculating + weighted average character width. Defaults to 1.25. + layout_mode_strip_rotated (bool): layout mode does not support rotated text. + Set to False to include rotated text anyway. If rotated text is discovered, + layout will be degraded and a warning will result. Defaults to True. + layout_mode_debug_path (Path | None): if supplied, must target a directory. + creates the following files with debug information for layout mode + functions if supplied: + + - fonts.json: output of self._layout_mode_fonts + - tjs.json: individual text render ops with corresponding transform matrices + - bts.json: text render ops left justified and grouped by BT/ET operators + - bt_groups.json: BT/ET operations grouped by rendered y-coord (aka lines) + layout_mode_font_height_weight (float): multiplier for font height when calculating + blank lines. Defaults to 1. + + Returns: + The extracted text + + """ + if extraction_mode not in ["plain", "layout"]: + raise ValueError(f"Invalid text extraction mode '{extraction_mode}'") + if extraction_mode == "layout": + for visitor in ( + "visitor_operand_before", + "visitor_operand_after", + "visitor_text", + ): + if locals()[visitor]: + logger_warning( + "Argument %(visitor)s is ignored in layout mode", + source=__name__, + visitor=visitor, + ) + return self._layout_mode_text( + space_vertically=kwargs.get("layout_mode_space_vertically", True), + scale_weight=kwargs.get("layout_mode_scale_weight", 1.25), + strip_rotated=kwargs.get("layout_mode_strip_rotated", True), + debug_path=kwargs.get("layout_mode_debug_path"), + font_height_weight=kwargs.get("layout_mode_font_height_weight", 1) + ) + if len(args) >= 1: + if isinstance(args[0], str): + if len(args) >= 3: + if isinstance(args[2], (tuple, int)): + orientations = args[2] + else: + raise TypeError(f"Invalid positional parameter {args[2]}") + if len(args) >= 4: + if isinstance(args[3], (float, int)): + space_width = args[3] + else: + raise TypeError(f"Invalid positional parameter {args[3]}") + elif isinstance(args[0], (tuple, int)): + orientations = args[0] + if len(args) >= 2: + if isinstance(args[1], (float, int)): + space_width = args[1] + else: + raise TypeError(f"Invalid positional parameter {args[1]}") + else: + raise TypeError(f"Invalid positional parameter {args[0]}") + + if isinstance(orientations, int): + orientations = (orientations,) + + return self._extract_text( + self, + self.pdf, + orientations, + space_width, + PG.CONTENTS, + visitor_operand_before, + visitor_operand_after, + visitor_text, + ) + + def extract_xform_text( + self, + xform: EncodedStreamObject, + orientations: tuple[int, ...] = (0, 90, 270, 360), + space_width: float = 200.0, + visitor_operand_before: Optional[Callable[[Any, Any, Any, Any], None]] = None, + visitor_operand_after: Optional[Callable[[Any, Any, Any, Any], None]] = None, + visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, + *, + known_ids: Optional[set[int]] = None, + traversal_state: Optional[Any] = None + ) -> str: + """ + Extract text from an XObject. + + Args: + xform: + orientations: + space_width: force default space width (if not extracted from font (default 200) + visitor_operand_before: + visitor_operand_after: + visitor_text: + known_ids: + traversal_state: + + Returns: + The extracted text + + """ + # The type hint would have to use an internal type otherwise, which is not desired. + assert traversal_state is None or isinstance(traversal_state, _TraversalState) + return self._extract_text( + xform, + self.pdf, + orientations, + space_width, + None, + visitor_operand_before, + visitor_operand_after, + visitor_text, + known_ids=known_ids, + traversal_state=traversal_state, + ) + + def _get_fonts(self) -> tuple[set[str], set[str]]: + """ + Get the names of embedded fonts and unembedded fonts. + + Returns: + A tuple (set of embedded fonts, set of unembedded fonts) + + """ + obj = self.get_object() + assert isinstance(obj, DictionaryObject) + fonts: set[str] = set() + embedded: set[str] = set() + fonts, embedded = _get_fonts_walk(obj, fonts, embedded) + unembedded = fonts - embedded + return embedded, unembedded + + mediabox = _create_rectangle_accessor(PG.MEDIABOX, ()) + """A :class:`RectangleObject`, expressed in + default user space units, defining the boundaries of the physical medium on + which the page is intended to be displayed or printed.""" + + cropbox = _create_rectangle_accessor("/CropBox", (PG.MEDIABOX,)) + """ + A :class:`RectangleObject`, expressed in + default user space units, defining the visible region of default user + space. + + When the page is displayed or printed, its contents are to be clipped + (cropped) to this rectangle and then imposed on the output medium in some + implementation-defined manner. Default value: same as + :attr:`mediabox`. + """ + + bleedbox = _create_rectangle_accessor("/BleedBox", ("/CropBox", PG.MEDIABOX)) + """A :class:`RectangleObject`, expressed in + default user space units, defining the region to which the contents of the + page should be clipped when output in a production environment.""" + + trimbox = _create_rectangle_accessor("/TrimBox", ("/CropBox", PG.MEDIABOX)) + """A :class:`RectangleObject`, expressed in + default user space units, defining the intended dimensions of the finished + page after trimming.""" + + artbox = _create_rectangle_accessor("/ArtBox", ("/CropBox", PG.MEDIABOX)) + """A :class:`RectangleObject`, expressed in + default user space units, defining the extent of the page's meaningful + content as intended by the page's creator.""" + + @property + def annotations(self) -> Optional[ArrayObject]: + if "/Annots" not in self: + return None + annotations = self["/Annots"].get_object() + if not isinstance(annotations, ArrayObject): + logger_warning( + "Annotations are not an array: %(annotations)s", + source=__name__, + annotations=annotations, + ) + return None + return annotations + + @annotations.setter + def annotations(self, value: Optional[ArrayObject]) -> None: + """ + Set the annotations array of the page. + + Typically you do not want to set this value, but append to it. + If you append to it, remember to add the object first to the writer + and only add the indirect object. + """ + if value is None: + if "/Annots" not in self: + return + del self[NameObject("/Annots")] + else: + self[NameObject("/Annots")] = value + + def add_action(self, trigger: PageTrigger, action: Action) -> None: + """ + Add an action which will launch on the given trigger event of this page. + + Args: + trigger: The action trigger to use. + action: The action to be done. + + Example: + >>> from pypdf import PdfWriter + >>> from pypdf.actions import JavaScript, PageTrigger + >>> writer = PdfWriter() + >>> page = writer.add_blank_page(595, 842) + >>> # Display the page number when the page is opened + >>> page.add_action(PageTrigger("open"), JavaScript("app.alert('This is page ' + this.pageNum);")) + >>> # Display the page number when the page is closed + >>> page.add_action(PageTrigger("close"), JavaScript("app.alert('This is page ' + this.pageNum);")) + """ + return Action._create_new(self, trigger, action) + + def delete_action(self, trigger: PageTrigger) -> None: + """ + Delete all actions associated with an open or close trigger event of this page. + + Args: + trigger: An open or close trigger. + + Example: + >>> from pypdf import PdfWriter + >>> from pypdf.actions import JavaScript, PageTrigger + >>> writer = PdfWriter() + >>> page = writer.add_blank_page(595, 842) + >>> page.add_action(PageTrigger("open"), JavaScript("app.alert('This is page ' + this.pageNum);")) + >>> page.add_action(PageTrigger("close"), JavaScript("app.alert('This is page ' + this.pageNum);")) + >>> # Delete all actions triggered by a page open + >>> page.delete_action(PageTrigger("open")) + >>> # Delete all actions triggered by a page close + >>> page.delete_action(PageTrigger("close")) + """ + return Action._delete(self, trigger) + + +class _VirtualList(Sequence[PageObject]): + def __init__( + self, + length_function: Callable[[], int], + get_function: Callable[[int], PageObject], + ) -> None: + self.length_function = length_function + self.get_function = get_function + self.current = -1 + + def __len__(self) -> int: + return self.length_function() + + @overload + def __getitem__(self, index: int) -> PageObject: + ... + + @overload + def __getitem__(self, index: slice) -> Sequence[PageObject]: + ... + + def __getitem__( + self, index: Union[int, slice] + ) -> Union[PageObject, Sequence[PageObject]]: + if isinstance(index, slice): + indices = range(*index.indices(len(self))) + cls = type(self) + return cls(indices.__len__, lambda idx: self[indices[idx]]) + if not isinstance(index, int): + raise TypeError("Sequence indices must be integers") + len_self = len(self) + if index < 0: + # support negative indexes + index += len_self + if not (0 <= index < len_self): + raise IndexError("Sequence index out of range") + return self.get_function(index) + + def __delitem__(self, index: Union[int, slice]) -> None: + if isinstance(index, slice): + r = list(range(*index.indices(len(self)))) + # pages have to be deleted from last to first + r.sort() + r.reverse() + for p in r: + del self[p] # recursive call + return + if not isinstance(index, int): + raise TypeError("Index must be integers") + len_self = len(self) + if index < 0: + # support negative indexes + index += len_self + if not (0 <= index < len_self): + raise IndexError("Index out of range") + ind = self[index].indirect_reference + assert ind is not None + parent: Optional[PdfObject] = cast(DictionaryObject, ind.get_object()).get( + "/Parent", None + ) + first = True + while parent is not None: + parent = cast(DictionaryObject, parent.get_object()) + try: + i = cast(ArrayObject, parent["/Kids"]).index(ind) + del cast(ArrayObject, parent["/Kids"])[i] + first = False + try: + assert ind is not None + del ind.pdf.flattened_pages[index] # case of page in a Reader + except Exception: # pragma: no cover + pass + if "/Count" in parent: + parent[NameObject("/Count")] = NumberObject( + cast(int, parent["/Count"]) - 1 + ) + if len(cast(ArrayObject, parent["/Kids"])) == 0: + # No more objects in this part of this subtree + ind = parent.indirect_reference + parent = parent.get("/Parent", None) + except ValueError: # from index + if first: + raise PdfReadError(f"Page not found in page tree: {ind}") + break + + def __iter__(self) -> Iterator[PageObject]: + for i in range(len(self)): + yield self[i] + + def __str__(self) -> str: + p = [f"PageObject({i})" for i in range(self.length_function())] + return f"[{', '.join(p)}]" + + +def _get_fonts_walk( + obj: DictionaryObject, + fnt: set[str], + emb: set[str], +) -> tuple[set[str], set[str]]: + """ + Get the set of all fonts and all embedded fonts. + + Args: + obj: Page resources dictionary + fnt: font + emb: embedded fonts + + Returns: + A tuple (fnt, emb) + + If there is a key called 'BaseFont', that is a font that is used in the document. + If there is a key called 'FontName' and another key in the same dictionary object + that is called 'FontFilex' (where x is null, 2, or 3), then that fontname is + embedded. + + We create and add to two sets, fnt = fonts used and emb = fonts embedded. + + """ + fontkeys = ("/FontFile", "/FontFile2", "/FontFile3") + + def process_font(f: PdfObject) -> None: + nonlocal fnt, emb + f = cast(DictionaryObject, f.get_object()) # to be sure + if "/BaseFont" in f: + fnt.add(cast(str, f["/BaseFont"])) + + if ( + ("/CharProcs" in f) + or ( + "/FontDescriptor" in f + and any( + x in cast(DictionaryObject, f["/FontDescriptor"]) for x in fontkeys + ) + ) + or ( + "/DescendantFonts" in f + and "/FontDescriptor" + in cast( + DictionaryObject, + cast(ArrayObject, f["/DescendantFonts"])[0].get_object(), + ) + and any( + x + in cast( + DictionaryObject, + cast( + DictionaryObject, + cast(ArrayObject, f["/DescendantFonts"])[0].get_object(), + )["/FontDescriptor"], + ) + for x in fontkeys + ) + ) + ): + # the list comprehension ensures there is FontFile + try: + emb.add(cast(str, f["/BaseFont"])) + except KeyError: + emb.add("(" + cast(str, f["/Subtype"]) + ")") + + if "/DR" in obj and "/Font" in cast(DictionaryObject, obj["/DR"]): + for f in cast( + DictionaryObject, cast(DictionaryObject, obj["/DR"])["/Font"] + ).values(): + process_font(f) + if "/Resources" in obj: + if "/Font" in cast(DictionaryObject, obj["/Resources"]): + for f in cast( + DictionaryObject, cast(DictionaryObject, obj["/Resources"])["/Font"] + ).values(): + process_font(f) + if "/XObject" in cast(DictionaryObject, obj["/Resources"]): + for x in cast( + DictionaryObject, cast(DictionaryObject, obj["/Resources"])["/XObject"] + ).values(): + _get_fonts_walk(cast(DictionaryObject, x.get_object()), fnt, emb) + if "/Annots" in obj: + for a in cast(ArrayObject, obj["/Annots"]): + _get_fonts_walk(cast(DictionaryObject, a.get_object()), fnt, emb) + if "/AP" in obj: + if ( + cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]).get( + "/Type" + ) + == "/XObject" + ): + _get_fonts_walk( + cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]), + fnt, + emb, + ) + else: + for a in cast(DictionaryObject, cast(DictionaryObject, obj["/AP"])["/N"]): + _get_fonts_walk(cast(DictionaryObject, a), fnt, emb) + return fnt, emb # return the sets for each page diff --git a/app/vendor/pypdf/_page_labels.py b/app/vendor/pypdf/_page_labels.py new file mode 100644 index 0000000..474d158 --- /dev/null +++ b/app/vendor/pypdf/_page_labels.py @@ -0,0 +1,338 @@ +""" +Page labels are shown by PDF viewers as "the page number". + +A page has a numeric index, starting at 0. Additionally, the page +has a label. In the most simple case: + + label = index + 1 + +However, the title page and the table of contents might have Roman numerals as +page labels. This makes things more complicated. + +Example 1 +--------- + +>>> reader.root_object["/PageLabels"]["/Nums"] +[0, IndirectObject(18, 0, 139929798197504), + 8, IndirectObject(19, 0, 139929798197504)] +>>> reader.get_object(reader.root_object["/PageLabels"]["/Nums"][1]) +{'/S': '/r'} +>>> reader.get_object(reader.root_object["/PageLabels"]["/Nums"][3]) +{'/S': '/D'} + +Example 2 +--------- +The following is a document with pages labeled +i, ii, iii, iv, 1, 2, 3, A-8, A-9, ... + +1 0 obj + << /Type /Catalog + /PageLabels << /Nums [ + 0 << /S /r >> + 4 << /S /D >> + 7 << /S /D + /P ( A- ) + /St 8 + >> + % A number tree containing + % three page label dictionaries + ] + >> + ... + >> +endobj + + +§12.4.2 PDF Specification 1.7 and 2.0 +===================================== + +Entries in a page label dictionary +---------------------------------- +The /S key: +D Decimal Arabic numerals +R Uppercase Roman numerals +r Lowercase Roman numerals +A Uppercase letters (A to Z for the first 26 pages, + AA to ZZ for the next 26, and so on) +a Lowercase letters (a to z for the first 26 pages, + aa to zz for the next 26, and so on) +""" +import string +from typing import TYPE_CHECKING, Optional, cast + +from ._protocols import PdfCommonDocProtocol +from ._utils import logger_warning +from .generic import ( + ArrayObject, + DictionaryObject, + NullObject, + NumberObject, + is_null_or_none, +) + +if TYPE_CHECKING: + from collections.abc import Callable + +# The largest number one can represent using the usual symbols +# and subtractive notation without additional conventions. +# Aligns with https://github.com/AA-Turner/roman-numerals/blob/master/python/roman_numerals/__init__.py +MAXIMUM_ROMAN_NUMERAL = 3_999 + +# Allow values of up to 512 * 26 = 13_312 for the `/A` and `/a`. +# Please note that this limit is somehow arbitrary and some trade-off +# between rather restrictive <= 256 output bytes per label and the possibly +# unrealistic 1000 output bytes per label. +MAXIMUM_PAGE_LABEL_LENGTH = 512 + + +def number2uppercase_roman_numeral(num: int) -> str: + if num <= 0: + raise ValueError("Expecting a positive number.") + if num > MAXIMUM_ROMAN_NUMERAL: + raise ValueError("Number is out of range.") + roman = [ + (1000, "M"), + (900, "CM"), + (500, "D"), + (400, "CD"), + (100, "C"), + (90, "XC"), + (50, "L"), + (40, "XL"), + (10, "X"), + (9, "IX"), + (5, "V"), + (4, "IV"), + (1, "I"), + ] + + result = [] + for value, symbol in roman: + count, num = divmod(num, value) + result.append(symbol * count) + + return "".join(result) + + +def number2lowercase_roman_numeral(number: int) -> str: + return number2uppercase_roman_numeral(number).lower() + + +def number2uppercase_letter(number: int) -> str: + if number <= 0: + raise ValueError("Expecting a positive number.") + # A to Z, then the same letter repeated: AA to ZZ, AAA to ZZZ, and so on. + repetitions, position = divmod(number - 1, 26) + length = repetitions + 1 + if length > MAXIMUM_PAGE_LABEL_LENGTH: + raise ValueError("Number is too large.") + return string.ascii_uppercase[position] * (repetitions + 1) + + +def number2lowercase_letter(number: int) -> str: + return number2uppercase_letter(number).lower() + + +def get_label_from_nums(dictionary_object: DictionaryObject, index: int) -> str: + # [Nums] shall be an array of the form + # [ key_1 value_1 key_2 value_2 ... key_n value_n ] + # where each key_i is an integer and the corresponding + # value_i shall be the object associated with that key. + # The keys shall be sorted in numerical order, + # analogously to the arrangement of keys in a name tree + # as described in 7.9.6, "Name Trees." + nums = cast(ArrayObject, dictionary_object["/Nums"]) + nums_length = len(nums) + i = 0 + value = None + start_index = 0 + while i < nums_length: + if i + 1 >= nums_length: + logger_warning( + "Ignoring last /Nums key without a value.", source=__name__ + ) + break + start_index = nums[i] + value = nums[i + 1].get_object() + if i + 2 == nums_length: + break + if nums[i + 2] > index: + break + i += 2 + m: dict[Optional[str], Callable[[int], str]] = { + None: lambda _: "", + "/D": str, + "/R": number2uppercase_roman_numeral, + "/r": number2lowercase_roman_numeral, + "/A": number2uppercase_letter, + "/a": number2lowercase_letter, + } + # if /Nums array is not following the specification or if /Nums is empty + if not isinstance(value, dict): + return str(index + 1) # Fallback + start = value.get("/St", 1) + prefix = cast(str, value.get("/P", "")) + mapping_function = m.get(value.get("/S")) + if mapping_function is None: + # Unknown /S numbering style; fall back to the page position. + logger_warning( + "Ignoring unknown page label numbering style %(style)r in /Nums.", + source=__name__, + style=value.get("/S"), + ) + return str(index + 1) # Fallback + try: + return prefix + mapping_function(index - start_index + start) + except (TypeError, ValueError) as exception: + # Malformed /St or /P value; fall back to the page position. + logger_warning( + "Ignoring malformed page label entry in /Nums (/St=%(start)r, /P=%(prefix)r): %(exception)s", + source=__name__, + start=start, + prefix=prefix, + exception=exception, + ) + return str(index + 1) # Fallback + + +def index2label(reader: PdfCommonDocProtocol, index: int) -> str: + """ + See 7.9.7 "Number Trees". + + Args: + reader: The PdfReader + index: The index of the page + + Returns: + The label of the page, e.g. "iv" or "4". + + """ + root = cast(DictionaryObject, reader.root_object) + if "/PageLabels" not in root: + return str(index + 1) # Fallback + number_tree = root["/PageLabels"].get_object() + if not isinstance(number_tree, DictionaryObject): + logger_warning( + "Page labels are not a dictionary: %(number_tree)s", + source=__name__, + number_tree=number_tree, + ) + return str(index + 1) # Fallback + if "/Nums" in number_tree: + return get_label_from_nums(number_tree, index) + if "/Kids" in number_tree and not isinstance(number_tree["/Kids"], NullObject): + # number_tree = {'/Kids': [IndirectObject(7333, 0, 140132998195856), ...]} + # Limit maximum depth. + level = 0 + while level < 100: + kids = cast(list[DictionaryObject], number_tree["/Kids"]) + for kid in kids: + # kid = {'/Limits': [0, 63], '/Nums': [0, {'/P': 'C1'}, ...]} + limits = kid.get("/Limits", NullObject()).get_object() + if not isinstance(limits, list) or len(limits) < 2: + # Skip kids whose /Limits range is missing or malformed. + logger_warning( + "Ignoring kid with missing or malformed /Limits in /PageLabels.", + source=__name__, + ) + continue + if limits[0] <= index <= limits[1]: + if not is_null_or_none(kid.get("/Kids", None)): + # Recursive definition. + level += 1 + if level == 100: # pragma: no cover + raise NotImplementedError( + "Too deep nesting is not supported." + ) + number_tree = kid + # Exit the inner `for` loop and continue at the next level with the + # next iteration of the `while` loop. + break + return get_label_from_nums(kid, index) + else: + # When there are no kids, make sure to exit the `while` loop directly + # and continue with the fallback. + break + + logger_warning("Could not reliably determine page label for %(index)d.", source=__name__, index=index) + return str(index + 1) # Fallback if neither /Nums nor /Kids is in the number_tree + + +def nums_insert( + key: NumberObject, + value: DictionaryObject, + nums: ArrayObject, +) -> None: + """ + Insert a key, value pair in a Nums array. + + See 7.9.7 "Number Trees". + + Args: + key: number key of the entry + value: value of the entry + nums: Nums array to modify + + """ + if len(nums) % 2 != 0: + raise ValueError("A nums like array must have an even number of elements") + + i = len(nums) + while i != 0 and key <= nums[i - 2]: + i = i - 2 + + if i < len(nums) and key == nums[i]: + nums[i + 1] = value + else: + nums.insert(i, key) + nums.insert(i + 1, value) + + +def nums_clear_range( + key: NumberObject, + page_index_to: int, + nums: ArrayObject, +) -> None: + """ + Remove all entries in a number tree in a range after an entry. + + See 7.9.7 "Number Trees". + + Args: + key: number key of the entry before the range + page_index_to: The page index of the upper limit of the range + nums: Nums array to modify + + """ + if len(nums) % 2 != 0: + raise ValueError("A nums like array must have an even number of elements") + if page_index_to < key: + raise ValueError("page_index_to must be greater or equal than key") + + i = nums.index(key) + 2 + while i < len(nums) and nums[i] <= page_index_to: + nums.pop(i) + nums.pop(i) + + +def nums_next( + key: NumberObject, + nums: ArrayObject, +) -> tuple[Optional[NumberObject], Optional[DictionaryObject]]: + """ + Return the (key, value) pair of the entry after the given one. + + See 7.9.7 "Number Trees". + + Args: + key: number key of the entry + nums: Nums array + + """ + if len(nums) % 2 != 0: + raise ValueError("A nums like array must have an even number of elements") + + i = nums.index(key) + 2 + if i < len(nums): + return nums[i], nums[i + 1] + return None, None diff --git a/app/vendor/pypdf/_protocols.py b/app/vendor/pypdf/_protocols.py new file mode 100644 index 0000000..3d02fee --- /dev/null +++ b/app/vendor/pypdf/_protocols.py @@ -0,0 +1,90 @@ +"""Helpers for working with PDF types.""" + +from abc import abstractmethod +from collections.abc import Sequence +from pathlib import Path +from typing import IO, Any, Literal, Optional, Protocol, Union + +from ._utils import StrByteType, StreamType + + +class PdfObjectProtocol(Protocol): + indirect_reference: Any + + def clone( + self, + pdf_dest: Any, + force_duplicate: bool = False, + ignore_fields: Union[tuple[str, ...], list[str], None] = (), + ) -> Any: + ... # pragma: no cover + + def _reference_clone(self, clone: Any, pdf_dest: Any) -> Any: + ... # pragma: no cover + + def get_object(self) -> Optional["PdfObjectProtocol"]: + ... # pragma: no cover + + def hash_value(self) -> bytes: + ... # pragma: no cover + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + ... # pragma: no cover + + +class XmpInformationProtocol(PdfObjectProtocol): + pass + + +class PdfCommonDocProtocol(Protocol): + @property + def pdf_header(self) -> str: + ... # pragma: no cover + + @property + def pages(self) -> Sequence[Any]: + ... # pragma: no cover + + @property + def root_object(self) -> PdfObjectProtocol: + ... # pragma: no cover + + def get_object(self, indirect_reference: Any) -> Optional[PdfObjectProtocol]: + ... # pragma: no cover + + @property + def strict(self) -> bool: + ... # pragma: no cover + + +class PdfReaderProtocol(PdfCommonDocProtocol, Protocol): + @property + @abstractmethod + def xref(self) -> dict[int, dict[int, Any]]: + ... # pragma: no cover + + @property + @abstractmethod + def trailer(self) -> dict[str, Any]: + ... # pragma: no cover + + +class PdfWriterProtocol(PdfCommonDocProtocol, Protocol): + _objects: list[Any] + # The "PreventGC" entry holds the source document, keeping it alive while + # the translation table is in use. + _id_translated: dict[int, dict[Union[int, Literal["PreventGC"]], Any]] + + incremental: bool + # Only populated in incremental mode, None otherwise. + _reader: Optional[Any] # PdfReader + + @abstractmethod + def write(self, stream: Union[Path, StrByteType]) -> tuple[bool, IO[Any]]: + ... # pragma: no cover + + @abstractmethod + def _add_object(self, obj: Any) -> Any: + ... # pragma: no cover diff --git a/app/vendor/pypdf/_reader.py b/app/vendor/pypdf/_reader.py new file mode 100644 index 0000000..2938651 --- /dev/null +++ b/app/vendor/pypdf/_reader.py @@ -0,0 +1,1649 @@ +# Copyright (c) 2006, Mathieu Fenniak +# Copyright (c) 2007, Ashish Kulkarni +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import os +import re +import sys +from collections.abc import Iterable +from io import BytesIO, UnsupportedOperation +from operator import itemgetter +from pathlib import Path +from types import TracebackType +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Optional, + Union, + cast, +) + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +from ._configuration import apply_legacy_configuration +from ._doc_common import PdfDocCommon, convert_to_int +from ._encryption import Encryption, PasswordType +from ._utils import ( + WHITESPACES_AS_BYTES, + StrByteType, + StreamType, + logger_warning, + read_non_whitespace, + read_previous_line, + read_until_whitespace, + skip_over_comment, + skip_over_whitespace, +) +from .constants import TrailerKeys as TK +from .errors import ( + EmptyFileError, + FileNotDecryptedError, + LimitReachedError, + PdfReadError, + PdfStreamError, + WrongPasswordError, +) +from .generic import ( + ArrayObject, + Destination, + DictionaryObject, + EncodedStreamObject, + IndirectObject, + NameObject, + NullObject, + NumberObject, + PdfObject, + StreamObject, + TextStringObject, + is_null_or_none, + read_object, +) +from .xmp import XmpInformation + +if TYPE_CHECKING: + from ._page import PageObject + + +class PdfReader(PdfDocCommon): + """ + Initialize a PdfReader object. + + This operation can take some time, as the PDF stream's cross-reference + tables are read into memory. + + Args: + stream: A File object or an object that supports the standard read + and seek methods similar to a File object. Could also be a + string representing a path to a PDF file. + strict: Determines whether user should be warned of all + problems and also causes some correctable problems to be fatal. + Defaults to ``False``. + password: Decrypt PDF file at initialization. If the + password is None, the file will not be decrypted. + Defaults to ``None``. + root_object_recovery_limit: The maximum number of objects to query + for recovering the Root object in non-strict mode. To disable + this security measure, pass ``None``. + + """ + + def __init__( + self, + stream: Union[StrByteType, Path], + strict: bool = False, + password: Union[str, bytes, None] = None, + *, + root_object_recovery_limit: Optional[int] = 10_000, + ) -> None: + self.strict = strict + self.flattened_pages: Optional[list[PageObject]] = None + + #: Storage of parsed PDF objects. + self.resolved_objects: dict[tuple[Any, Any], Optional[PdfObject]] = {} + + self._startxref: int = 0 + self.xref_index = 0 + self.xref: dict[int, dict[Any, Any]] = {} + self.xref_free_entry: dict[int, dict[Any, Any]] = {} + self.xref_objStm: dict[int, tuple[Any, Any]] = {} + self.trailer = DictionaryObject() + + # Security parameters. + self._root_object_recovery_limit = ( + root_object_recovery_limit if isinstance(root_object_recovery_limit, int) else sys.maxsize + ) + apply_legacy_configuration() + + # Map page indirect_reference number to page number + self._page_id2num: Optional[dict[Any, Any]] = None + + self._validated_root: Optional[DictionaryObject] = None + + self._initialize_stream(stream) + self._known_objects: set[tuple[int, int]] = set() + + self._override_encryption = False + self._encryption: Optional[Encryption] = None + if self.is_encrypted: + self._handle_encryption(password) + elif password is not None: + raise PdfReadError("Not an encrypted file") + + self._named_destinations_cache: Optional[dict[str, Destination]] = None + + def _initialize_stream(self, stream: Union[StrByteType, Path]) -> None: + if hasattr(stream, "mode") and "b" not in stream.mode: + logger_warning( + "PdfReader stream/file object is not in binary mode. " + "It may not be read correctly.", + source=__name__, + ) + self._stream_opened = False + if isinstance(stream, (str, Path)): + with open(stream, "rb") as fh: + stream = BytesIO(fh.read()) + self._stream_opened = True + self.read(stream) + self.stream = stream + + def _handle_encryption(self, password: Optional[Union[str, bytes]]) -> None: + self._override_encryption = True + # Some documents may not have a /ID, use two empty + # byte strings instead. Solves + # https://github.com/py-pdf/pypdf/issues/608 + id_entry = self.trailer.get(TK.ID) + id1_entry = id_entry[0].get_object().original_bytes if id_entry else b"" + encrypt_entry = cast(DictionaryObject, self.trailer[TK.ENCRYPT].get_object()) + self._encryption = Encryption.read(encrypt_entry, id1_entry) + + # try empty password if no password provided + pwd = password if password is not None else b"" + if ( + self._encryption.verify(pwd, strict=self.strict) == PasswordType.NOT_DECRYPTED + and password is not None + ): + # raise if password provided + raise WrongPasswordError("Wrong password") + self._override_encryption = False + + def __enter__(self) -> Self: + return self + + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc_val: Optional[BaseException], + exc_tb: Optional[TracebackType], + ) -> None: + self.close() + + def close(self) -> None: + """Close the stream if opened in __init__ and clear memory.""" + if self._stream_opened: + self.stream.close() + self.flattened_pages = [] + self.resolved_objects = {} + self.trailer = DictionaryObject() + self.xref = {} + self.xref_free_entry = {} + self.xref_objStm = {} + + @property + def root_object(self) -> DictionaryObject: + """Provide access to "/Root". Standardized with PdfWriter.""" + if self._validated_root: + return self._validated_root + root = self.trailer.get(TK.ROOT) + if is_null_or_none(root): + logger_warning('Cannot find "/Root" key in trailer', source=__name__) + elif ( + cast(DictionaryObject, cast(PdfObject, root).get_object()).get("/Type") + == "/Catalog" + ): + self._validated_root = cast( + DictionaryObject, cast(PdfObject, root).get_object() + ) + else: + logger_warning("Invalid Root object in trailer", source=__name__) + if self._validated_root is None: + logger_warning('Searching object with "/Catalog" key', source=__name__) + number_of_objects = cast(int, self.trailer.get("/Size", 0)) + for i in range(number_of_objects): + if i >= self._root_object_recovery_limit: + raise LimitReachedError("Maximum Root object recovery limit reached.") + try: + obj = self.get_object(i + 1) + except Exception: # to be sure to capture all errors + obj = None + if isinstance(obj, DictionaryObject) and obj.get("/Type") == "/Catalog": + self._validated_root = obj + logger_warning( + "Root found at %(obj_reference)r", + source=__name__, + obj_reference=obj.indirect_reference, + ) + break + if self._validated_root is None: + if not is_null_or_none(root) and "/Pages" in cast(DictionaryObject, cast(PdfObject, root).get_object()): + logger_warning( + "Possible root found at %(root_ref)r, but missing /Catalog key", + source=__name__, + root_ref=cast(PdfObject, root).indirect_reference, + ) + self._validated_root = cast( + DictionaryObject, cast(PdfObject, root).get_object() + ) + else: + raise PdfReadError("Cannot find Root object in pdf") + return self._validated_root + + @property + def _info(self) -> Optional[DictionaryObject]: + """ + Provide access to "/Info". Standardized with PdfWriter. + + Returns: + /Info Dictionary; None if the entry does not exist + + """ + info = self.trailer.get(TK.INFO, None) + if is_null_or_none(info): + return None + assert info is not None, "mypy" + info = info.get_object() + if not isinstance(info, DictionaryObject): + raise PdfReadError( + "Trailer not found or does not point to a document information dictionary" + ) + return info + + @property + def _ID(self) -> Optional[ArrayObject]: + """ + Provide access to "/ID". Standardized with PdfWriter. + + Returns: + /ID array; None if the entry does not exist + + """ + id = self.trailer.get(TK.ID, None) + if is_null_or_none(id): + return None + assert id is not None, "mypy" + return cast(ArrayObject, id.get_object()) + + @property + def pdf_header(self) -> str: + """ + The first 8 bytes of the file. + + This is typically something like ``'%PDF-1.6'`` and can be used to + detect if the file is actually a PDF file and which version it is. + """ + # TODO: Make this return a bytes object for consistency + # but that needs a deprecation + loc = self.stream.tell() + self.stream.seek(0, 0) + pdf_file_version = self.stream.read(8).decode("utf-8", "backslashreplace") + self.stream.seek(loc, 0) # return to where it was + return pdf_file_version + + @property + def xmp_metadata(self) -> Optional[XmpInformation]: + """XMP (Extensible Metadata Platform) data.""" + try: + self._override_encryption = True + return cast(XmpInformation, self.root_object.xmp_metadata) + finally: + self._override_encryption = False + + def _get_page_number_by_indirect( + self, indirect_reference: Union[int, NullObject, IndirectObject, None] + ) -> Optional[int]: + """ + Retrieve the page number from an indirect reference. + + Args: + indirect_reference: The indirect reference to locate. + + Returns: + Page number or None. + + """ + if self._page_id2num is None: + self._page_id2num = { + x.indirect_reference.idnum: i for i, x in enumerate(self.pages) # type: ignore[union-attr] + } + + if is_null_or_none(indirect_reference): + return None + assert isinstance(indirect_reference, (int, IndirectObject)), "mypy" + if isinstance(indirect_reference, int): + idnum = indirect_reference + else: + idnum = indirect_reference.idnum + assert self._page_id2num is not None, "hint for mypy" + return self._page_id2num.get(idnum, None) + + def _get_object_from_stream( + self, indirect_reference: IndirectObject + ) -> PdfObject: + # indirect reference to object in object stream + # read the entire object stream into memory + stmnum, _idx = self.xref_objStm[indirect_reference.idnum] + obj_stm: EncodedStreamObject = IndirectObject(stmnum, 0, self).get_object() # type: ignore[assignment] + # This is an xref to a stream, so its type better be a stream + assert cast(str, obj_stm["/Type"]) == "/ObjStm" + # Parse ALL objects in this stream in one pass and cache them. + # This avoids O(N²) behavior when many objects from the same stream + # are resolved individually (each call would re-parse the header). + stream_data = BytesIO(obj_stm.get_data()) + n = int(obj_stm["/N"]) # type: ignore[call-overload] + first_offset = int(obj_stm["/First"]) # type: ignore[call-overload] + + # ObjStm header format: "objnum offset objnum offset ..." + # smallest possible entry: "0 0" = 3 bytes (1 digit + 1 space + 1 digit) + # using // 4 would reject a valid 3-byte single entry (3 // 4 = 0) + max_n = stream_data.getbuffer().nbytes // 3 + stream_data.seek(0) + if n > max_n: + if self.strict: + raise LimitReachedError(f"Value /N {n} for object {stmnum} exceeds maximum allowed value {max_n}.") + logger_warning( + "Value /N %(n)d for object %(stmnum)d exceeds maximum allowed value %(max_n)d. Limiting to %(max_n)d.", + source=__name__, + n=n, + stmnum=stmnum, + max_n=max_n, + ) + n = max_n + + # Phase 1: Read the index (objnum, offset) pairs from the header. + obj_index: list[tuple[int, int]] = [] + for _i in range(n): + read_non_whitespace(stream_data) + stream_data.seek(-1, 1) + objnum = NumberObject.read_from_stream(stream_data) + read_non_whitespace(stream_data) + stream_data.seek(-1, 1) + offset = NumberObject.read_from_stream(stream_data) + read_non_whitespace(stream_data) + stream_data.seek(-1, 1) + obj_index.append((int(objnum), int(offset))) + + # Phase 2: Parse each object and cache it. + target_obj: PdfObject = NullObject() + found = False + for i, (obj_num, obj_offset) in enumerate(obj_index): + # Skip objects already in the cache. + cached = self.cache_get_indirect_object(0, obj_num) + if cached is not None: + if obj_num == indirect_reference.idnum: + target_obj = cached + found = True + continue + + stream_data.seek(first_offset + obj_offset, 0) + + # To cope with case where the 'pointer' is on a white space + read_non_whitespace(stream_data) + stream_data.seek(-1, 1) + + try: + obj = read_object(stream_data, self) + except PdfStreamError as exc: + # Stream object cannot be read. Normally, a critical error, but + # Adobe Reader doesn't complain, so continue (in strict mode?) + logger_warning( + "Invalid stream (index %(index)d) within object %(obj_num)d 0: %(exc)s", + source=__name__, + index=i, + obj_num=obj_num, + exc=exc, + ) + if self.strict: # pragma: no cover + raise PdfReadError( + f"Cannot read object stream: {exc}" + ) # pragma: no cover + obj = NullObject() # pragma: no cover + + # Only cache if this stream is the authoritative source for the object. + # Incremental updates may override objects originally in the stream; + # caching those stale versions would shadow the newer xref entry. + authoritative_stm, _idx = self.xref_objStm.get(obj_num, (None, None)) + if authoritative_stm == stmnum: + self.cache_indirect_object(0, obj_num, obj) + + if obj_num == indirect_reference.idnum: + target_obj = obj + found = True + + if not found and self.strict: # pragma: no cover + raise PdfReadError( + "This is a fatal error in strict mode." + ) # pragma: no cover + return target_obj + + def get_object( + self, indirect_reference: Union[int, IndirectObject] + ) -> Optional[PdfObject]: + if isinstance(indirect_reference, int): + indirect_reference = IndirectObject(indirect_reference, 0, self) + retval = self.cache_get_indirect_object( + indirect_reference.generation, indirect_reference.idnum + ) + if retval is not None: + return retval + if ( + indirect_reference.generation == 0 + and indirect_reference.idnum in self.xref_objStm + ): + retval = self._get_object_from_stream(indirect_reference) + elif ( + indirect_reference.generation in self.xref + and indirect_reference.idnum in self.xref[indirect_reference.generation] + ): + if self.xref_free_entry.get(indirect_reference.generation, {}).get( + indirect_reference.idnum, False + ): + return NullObject() + start = self.xref[indirect_reference.generation][indirect_reference.idnum] + self.stream.seek(start, 0) + try: + idnum, generation = self.read_object_header(self.stream) + if ( + idnum != indirect_reference.idnum + or generation != indirect_reference.generation + ): + raise PdfReadError("Not matching, we parse the file for it") + except Exception: + if hasattr(self.stream, "getbuffer"): + buf = bytes(self.stream.getbuffer()) + else: + p = self.stream.tell() + self.stream.seek(0, 0) + buf = self.stream.read(-1) + self.stream.seek(p, 0) + m = re.search( + rf"\s{indirect_reference.idnum}\s+{indirect_reference.generation}\s+obj".encode(), + buf, + ) + if m is not None: + logger_warning( + "Object ID %(idnum)d,%(generation)d ref repaired", + source=__name__, + idnum=indirect_reference.idnum, + generation=indirect_reference.generation, + ) + self.xref[indirect_reference.generation][ + indirect_reference.idnum + ] = (m.start(0) + 1) + self.stream.seek(m.start(0) + 1) + idnum, generation = self.read_object_header(self.stream) + else: + idnum = -1 + generation = -1 # exception will be raised below + if idnum != indirect_reference.idnum and self.xref_index: + # xref table probably had bad indexes due to not being zero-indexed + if self.strict: + raise PdfReadError( + f"Expected object ID ({indirect_reference.idnum} {indirect_reference.generation}) " + f"does not match actual ({idnum} {generation}); " + "xref table not zero-indexed." + ) + # xref table is corrected in non-strict mode + elif idnum != indirect_reference.idnum and self.strict: + # some other problem + raise PdfReadError( + f"Expected object ID ({indirect_reference.idnum} {indirect_reference.generation}) " + f"does not match actual ({idnum} {generation})." + ) + if self.strict: + assert generation == indirect_reference.generation + + current_object = (indirect_reference.idnum, indirect_reference.generation) + if current_object in self._known_objects: + raise LimitReachedError(f"Detected loop with self reference for {indirect_reference!r}.") + self._known_objects.add(current_object) + retval = read_object(self.stream, self) + self._known_objects.remove(current_object) + + # override encryption is used for the /Encrypt dictionary + if not self._override_encryption and self._encryption is not None: + # if we don't have the encryption key: + if not self._encryption.is_decrypted(): + raise FileNotDecryptedError("File has not been decrypted") + # otherwise, decrypt here... + retval = self._encryption.decrypt_object( + retval, indirect_reference.idnum, indirect_reference.generation, + strict=self.strict, + ) + else: + if hasattr(self.stream, "getbuffer"): + buf = bytes(self.stream.getbuffer()) + else: + p = self.stream.tell() + self.stream.seek(0, 0) + buf = self.stream.read(-1) + self.stream.seek(p, 0) + m = re.search( + rf"\s{indirect_reference.idnum}\s+{indirect_reference.generation}\s+obj".encode(), + buf, + ) + if m is not None: + logger_warning( + "Object %(idnum)d %(generation)d found", + source=__name__, + idnum=indirect_reference.idnum, + generation=indirect_reference.generation, + ) + if indirect_reference.generation not in self.xref: + self.xref[indirect_reference.generation] = {} + self.xref[indirect_reference.generation][indirect_reference.idnum] = ( + m.start(0) + 1 + ) + self.stream.seek(m.end(0) + 1) + skip_over_whitespace(self.stream) + self.stream.seek(-1, 1) + retval = read_object(self.stream, self) + + # override encryption is used for the /Encrypt dictionary + if not self._override_encryption and self._encryption is not None: + # if we don't have the encryption key: + if not self._encryption.is_decrypted(): + raise FileNotDecryptedError("File has not been decrypted") + # otherwise, decrypt here... + retval = self._encryption.decrypt_object( + retval, indirect_reference.idnum, indirect_reference.generation, + strict=self.strict, + ) + else: + logger_warning( + "Object %(idnum)d %(generation)d not defined.", + source=__name__, + idnum=indirect_reference.idnum, + generation=indirect_reference.generation, + ) + if self.strict: + raise PdfReadError("Could not find object.") + # For ObjStm objects, _get_object_from_stream already cached + # the result during batch parsing; skip the redundant cache write + # to avoid "Overwriting cache" warnings. For non-ObjStm objects + # (including encrypted ones that need decrypted values cached), + # always write. + if not ( + indirect_reference.generation == 0 + and indirect_reference.idnum in self.xref_objStm + ): + self.cache_indirect_object( + indirect_reference.generation, indirect_reference.idnum, retval + ) + return retval + + def read_object_header(self, stream: StreamType) -> tuple[int, int]: + # Should never be necessary to read out whitespace, since the + # cross-reference table should put us in the right spot to read the + # object header. In reality some files have stupid cross-reference + # tables that are off by whitespace bytes. + skip_over_comment(stream) + extra = skip_over_whitespace(stream) + stream.seek(-1, 1) + idnum = read_until_whitespace(stream, max_bytes=IndirectObject._MAXIMUM_PART_LENGTH, strict=self.strict) + extra |= skip_over_whitespace(stream) + stream.seek(-1, 1) + generation = read_until_whitespace(stream, max_bytes=IndirectObject._MAXIMUM_PART_LENGTH, strict=self.strict) + extra |= skip_over_whitespace(stream) + stream.seek(-1, 1) + + # although it's not used, it might still be necessary to read + _obj = stream.read(3) + + read_non_whitespace(stream) + stream.seek(-1, 1) + if extra and self.strict: + logger_warning( + "Superfluous whitespace found in object header %(idnum)r %(generation)r", + source=__name__, + idnum=idnum, + generation=generation, + ) + + try: + return int(idnum), int(generation) + except (ValueError, OverflowError) as e: + # Only raise a ValueError here as other types would break future processing. + raise ValueError( + f"Invalid indirect object reference ({idnum!r} {generation!r} R): {e}" + ) from e + + def cache_get_indirect_object( + self, generation: int, idnum: int + ) -> Optional[PdfObject]: + try: + return self.resolved_objects.get((generation, idnum)) + except RecursionError: + raise PdfReadError("Maximum recursion depth reached.") + + def cache_indirect_object( + self, generation: int, idnum: int, obj: Optional[PdfObject] + ) -> Optional[PdfObject]: + if (generation, idnum) in self.resolved_objects: + msg = "Overwriting cache for %(generation)d %(idnum)d" + values = {"generation": generation, "idnum": idnum} + if self.strict: + raise PdfReadError(msg % values) + logger_warning(msg, source=__name__, **values) + self.resolved_objects[(generation, idnum)] = obj + if obj is not None: + obj.indirect_reference = IndirectObject(idnum, generation, self) + return obj + + def _replace_object(self, indirect_reference: IndirectObject, obj: PdfObject) -> PdfObject: + # function reserved for future development + if indirect_reference.pdf != self: + raise ValueError("Cannot update PdfReader with external object") + if (indirect_reference.generation, indirect_reference.idnum) not in self.resolved_objects: + raise ValueError("Cannot find referenced object") + self.resolved_objects[(indirect_reference.generation, indirect_reference.idnum)] = obj + obj.indirect_reference = indirect_reference + return obj + + def read(self, stream: StreamType) -> None: + """ + Read and process the PDF stream, extracting necessary data. + + Args: + stream: The PDF file stream. + + """ + self._basic_validation(stream) + self._find_eof_marker(stream) + startxref = self._find_startxref_pos(stream) + self._startxref = startxref + + # check and eventually correct the startxref only if not strict + xref_issue_nr = self._get_xref_issues(stream, startxref) + if xref_issue_nr != 0: + if self.strict and xref_issue_nr: + raise PdfReadError("Broken xref table") + logger_warning( + "incorrect startxref pointer(%(xref_issue_nr)d)", + source=__name__, + xref_issue_nr=xref_issue_nr, + ) + + # read all cross-reference tables and their trailers + self._read_xref_tables_and_trailers(stream, startxref, xref_issue_nr) + + # if not zero-indexed, verify that the table is correct; change it if necessary + if self.xref_index and not self.strict: + loc = stream.tell() + for gen, xref_entry in self.xref.items(): + if gen == 65535: + continue + xref_k = sorted( + xref_entry.keys() + ) # ensure ascending to prevent damage + for id in xref_k: + stream.seek(xref_entry[id], 0) + try: + pid, _pgen = self.read_object_header(stream) + except ValueError: + self._rebuild_xref_table(stream) + break + if pid == id - self.xref_index: + # fixing index item per item is required for revised PDF. + self.xref[gen][pid] = self.xref[gen][id] + del self.xref[gen][id] + # if not, then either it's just plain wrong, or the + # non-zero-index is actually correct + stream.seek(loc, 0) # return to where it was + + # remove wrong objects (not pointing to correct structures) - cf #2326 + if not self.strict: + loc = stream.tell() + for gen, xref_entry in self.xref.items(): + if gen == 65535: + continue + ids = list(xref_entry.keys()) + for id in ids: + stream.seek(xref_entry[id], 0) + try: + self.read_object_header(stream) + except ValueError: + logger_warning( + "Ignoring wrong pointing object %(id)d %(gen)d (offset %(offset)d)", + source=__name__, + id=id, + gen=gen, + offset=xref_entry[id], + ) + del xref_entry[id] # we can delete the id, we are parsing ids + stream.seek(loc, 0) # return to where it was + + def _basic_validation(self, stream: StreamType) -> None: + """Ensure the stream is valid and not empty.""" + stream.seek(0, os.SEEK_SET) + try: + header_byte = stream.read(5) + except UnicodeDecodeError: + raise UnsupportedOperation("cannot read header") + if header_byte == b"": + raise EmptyFileError("Cannot read an empty file") + if header_byte != b"%PDF-": + if self.strict: + raise PdfReadError( + f"PDF starts with '{header_byte.decode('utf8')}', " + "but '%PDF-' expected" + ) + logger_warning("invalid pdf header: %(header_byte)r", source=__name__, header_byte=header_byte) + stream.seek(0, os.SEEK_END) + + def _find_eof_marker(self, stream: StreamType) -> None: + """ + Jump to the %%EOF marker. + + According to the specs, the %%EOF marker should be at the very end of + the file. Hence for standard-compliant PDF documents this function will + read only the last part (DEFAULT_BUFFER_SIZE). + """ + header_size = 8 # to parse whole file, Header is e.g. '%PDF-1.6' + line = b"" + first = True + while not line.startswith(b"%%EOF"): + if line != b"" and first: + if any( + line.strip().endswith(tr) for tr in (b"%%EO", b"%%E", b"%%", b"%") + ): + # Consider the file as truncated while + # having enough confidence to carry on. + logger_warning("EOF marker seems truncated", source=__name__) + break + first = False + if b"startxref" in line: + logger_warning( + "CAUTION: startxref found while searching for %%EOF. " + "The file might be truncated and some data might not be read.", + source=__name__, + ) + if stream.tell() < header_size: + if self.strict: + raise PdfReadError("EOF marker not found") + logger_warning("EOF marker not found", source=__name__) + line = read_previous_line(stream) + + def _find_startxref_pos(self, stream: StreamType) -> int: + """ + Find startxref entry - the location of the xref table. + + Args: + stream: + + Returns: + The bytes offset + + """ + line = read_previous_line(stream) + # Some producers append further %%EOF markers below the one that + # closes the last revision (#4008). _find_eof_marker() stops at the + # very last of them, so the line above it is another marker rather + # than the offset. Skip that trailing run to reach the real offset; + # the revision being read is unchanged, only the marker padding is + # ignored. + duplicate_markers = 0 + while line.startswith(b"%%EOF") and stream.tell() > 0: + if duplicate_markers == self._MAX_STARTXREF_RECOVERY_LINES: + break + line = read_previous_line(stream) + duplicate_markers += 1 + if duplicate_markers: + logger_warning( + "Duplicate %%EOF marker(s) found, skipping them", source=__name__ + ) + try: + startxref = int(line) + except ValueError: + # 'startxref' may be on the same line as the location + if not line.startswith(b"startxref"): + raise PdfReadError("startxref not found") + startxref = int(line[9:].strip()) + logger_warning("startxref on same line as offset", source=__name__) + else: + line = read_previous_line(stream) + if not line.startswith(b"startxref"): + # The 'startxref' keyword expected just above the offset is + # missing or corrupt (for example a truncated 'tartxref'). + # Some producers append a broken trailing cross-reference + # pointer while an earlier, intact 'startxref' from a previous + # revision is still present. Recovering from this violates the + # standard, so only attempt it in non-strict mode (#3238). + if self.strict: + raise PdfReadError("startxref not found") + startxref = self._find_previous_startxref_pos(stream) + return startxref + + # Upper bound on the number of lines _find_previous_startxref_pos scans + # backwards while recovering a corrupt trailing startxref pointer. Kept + # fixed and non-configurable so a crafted file cannot trigger an unbounded + # backwards scan. + _MAX_STARTXREF_RECOVERY_LINES = 1000 + + @classmethod + def _find_previous_startxref_pos(cls, stream: StreamType) -> int: + """ + Recover the most recent intact ``startxref`` pointer by scanning + backwards from the current position. + + This is used as a fallback when the ``startxref`` keyword belonging to + the final ``%%EOF`` is corrupt (#3238). The offset always appears on + the line directly below the keyword, so the value read immediately + before encountering ``startxref`` (while moving backwards) is returned. + At most ``_MAX_STARTXREF_RECOVERY_LINES`` lines are inspected. + + Args: + stream: The PDF byte stream, positioned just above the corrupt + trailing pointer. + + Returns: + The bytes offset of the recovered ``startxref``. + + """ + offset: Optional[int] = None + for _ in range(cls._MAX_STARTXREF_RECOVERY_LINES): + if stream.tell() <= 0: + break + line = read_previous_line(stream) + if not line.startswith(b"startxref"): + try: + offset = int(line) + except ValueError: + offset = None + continue + if len(line) > 9: + # 'startxref' on the same line as the offset + return int(line[9:].strip()) + if offset is not None: + logger_warning( + "found startxref pointing to a previous revision after " + "a corrupt one", + source=__name__, + ) + return offset + break + raise PdfReadError("startxref not found") + + def _load_recovery_cache(self, data: bytes) -> dict[int, tuple[int, int]]: + cache = {} + for object_number, generation_number, object_start in self._find_pdf_objects(data): + if object_number in cache: + # Always use the first match. + continue + cache[object_number] = (object_start, generation_number) + return cache + + def _read_standard_xref_table(self, stream: StreamType) -> None: + # standard cross-reference table + ref = stream.read(3) + if ref != b"ref": + raise PdfReadError("xref table read error") + read_non_whitespace(stream) + stream.seek(-1, 1) + first_time = True # check if the first time looking at the xref table + recovery_cache: Optional[dict[int, tuple[int, int]]] = None + while True: + num = cast(int, read_object(stream, self)) + if first_time and num != 0: + self.xref_index = num + if self.strict: + logger_warning( + "Xref table not zero-indexed. ID numbers for objects will be corrected.", + source=__name__, + ) + # if table not zero indexed, could be due to error from when PDF was created + # which will lead to mismatched indices later on, only warned and corrected if self.strict==True + first_time = False + read_non_whitespace(stream) + stream.seek(-1, 1) + size = cast(int, read_object(stream, self)) + if not isinstance(size, int): + logger_warning( + "Invalid/Truncated xref table. Rebuilding it.", + source=__name__, + ) + self._rebuild_xref_table(stream) + stream.read() + return + read_non_whitespace(stream) + stream.seek(-1, 1) + cnt = 0 + while cnt < size: + line = stream.read(20) + if not line: + raise PdfReadError("Unexpected empty line in Xref table.") + + # It's very clear in section 3.4.3 of the PDF spec + # that all cross-reference table lines are a fixed + # 20 bytes (as of PDF 1.7). However, some files have + # 21-byte entries (or more) due to the use of \r\n + # (CRLF) EOL's. Detect that case, and adjust the line + # until it does not begin with a \r (CR) or \n (LF). + while line[0] in b"\x0D\x0A": + stream.seek(-20 + 1, 1) + line = stream.read(20) + + # On the other hand, some malformed PDF files + # use a single character EOL without a preceding + # space. Detect that case, and seek the stream + # back one character (0-9 means we've bled into + # the next xref entry, t means we've bled into the + # text "trailer"): + if line[-1] in b"0123456789t": + stream.seek(-1, 1) + + try: + offset_b, generation_b = line[:16].split(b" ") + entry_type_b = line[17:18] + + offset, generation = int(offset_b), int(generation_b) + except Exception: + if hasattr(stream, "getbuffer"): + buf = bytes(stream.getbuffer()) + else: + p = stream.tell() + stream.seek(0, 0) + buf = stream.read(-1) + stream.seek(p) + + if recovery_cache is None: + recovery_cache = self._load_recovery_cache(buf) + + if num not in recovery_cache: + logger_warning( + "entry %(num)d in Xref table invalid; object not found", + source=__name__, + num=num, + ) + generation = 65535 + offset = -1 + entry_type_b = b"f" + else: + logger_warning( + "entry %(num)d in Xref table invalid but object found", + source=__name__, + num=num, + ) + generation, offset = recovery_cache[num] + entry_type_b = b"n" + + if generation not in self.xref: + self.xref[generation] = {} + self.xref_free_entry[generation] = {} + if num in self.xref[generation]: + # It really seems like we should allow the last + # xref table in the file to override previous + # ones. Since we read the file backwards, assume + # any existing key is already set correctly. + pass + else: + if entry_type_b == b"n": + self.xref[generation][num] = offset + try: + self.xref_free_entry[generation][num] = entry_type_b == b"f" + except Exception: + pass + try: + self.xref_free_entry[65535][num] = entry_type_b == b"f" + except Exception: + pass + cnt += 1 + num += 1 + read_non_whitespace(stream) + stream.seek(-1, 1) + # Skip any PDF comments between xref entries and the trailer + # keyword. Some PDF producers (e.g. Vectorizer.AI) insert + # comments here which are legal per the PDF spec (§7.2.3). + while stream.read(1) == b"%": + stream.seek(-1, 1) + skip_over_comment(stream) + read_non_whitespace(stream) + stream.seek(-1, 1) + stream.seek(-1, 1) + trailer_tag = stream.read(7) + if trailer_tag != b"trailer": + # more xrefs! + stream.seek(-7, 1) + else: + break + + def _read_xref_tables_and_trailers( + self, stream: StreamType, startxref: Optional[int], xref_issue_nr: int + ) -> None: + """Read the cross-reference tables and trailers in the PDF stream.""" + self.xref = {} + self.xref_free_entry = {} + self.xref_objStm = {} + self.trailer = DictionaryObject() + visited_xref_offsets: set[int] = set() + while startxref is not None: + # Detect circular /Prev references in the xref chain + if startxref in visited_xref_offsets: + logger_warning( + "Circular xref chain detected at offset %(startxref)d, stopping", + source=__name__, + startxref=startxref, + ) + break + visited_xref_offsets.add(startxref) + # load the xref table + stream.seek(startxref, 0) + x = stream.read(1) + if x in b"\r\n": + x = stream.read(1) + if x == b"x": + startxref = self._read_xref(stream) + elif xref_issue_nr: + try: + self._rebuild_xref_table(stream) + break + except Exception: + xref_issue_nr = 0 + elif x.isdigit(): + try: + xrefstream = self._read_pdf15_xref_stream(stream) + except Exception as e: + if TK.ROOT in self.trailer: + logger_warning( + "Previous trailer cannot be read: %(args)s", + source=__name__, + args=e.args, + ) + break + raise PdfReadError(f"Trailer cannot be read: {e!s}") + self._process_xref_stream(xrefstream) + if "/Prev" in xrefstream: + startxref = cast(int, xrefstream["/Prev"]) + else: + break + else: + startxref = self._read_xref_other_error(stream, startxref) + + # The trailer keys a PDF 1.5+ cross-reference stream carries in place of a + # `trailer` keyword (PDF 2.0 specification, table 17). + _XREF_STREAM_TRAILER_KEYS = (TK.ROOT, TK.ENCRYPT, TK.INFO, TK.ID, TK.SIZE) + + def _process_xref_stream(self, xrefstream: DictionaryObject) -> None: + """Process and handle the xref stream.""" + for key in self._XREF_STREAM_TRAILER_KEYS: + if key in xrefstream and key not in self.trailer: + self.trailer[NameObject(key)] = xrefstream.raw_get(key) + if "/XRefStm" in xrefstream: + p = self.stream.tell() + self.stream.seek(cast(int, xrefstream["/XRefStm"]) + 1, 0) + self._read_pdf15_xref_stream(self.stream) + self.stream.seek(p, 0) + + def _read_xref(self, stream: StreamType) -> Optional[int]: + self._read_standard_xref_table(stream) + if stream.read(1) == b"": + return None + stream.seek(-1, 1) + read_non_whitespace(stream) + stream.seek(-1, 1) + new_trailer = cast(dict[str, Any], read_object(stream, self)) + for key, value in new_trailer.items(): + if key not in self.trailer: + self.trailer[key] = value + if "/XRefStm" in new_trailer: + p = stream.tell() + stream.seek(cast(int, new_trailer["/XRefStm"]) + 1, 0) + try: + self._read_pdf15_xref_stream(stream) + except Exception: + logger_warning( + "XRef object at %(xref_stm)d can not be read, some object may be missing", + source=__name__, + xref_stm=int(new_trailer["/XRefStm"]), + ) + stream.seek(p, 0) + if "/Prev" in new_trailer: + return cast(int, new_trailer["/Prev"]) + return None + + def _read_xref_other_error( + self, stream: StreamType, startxref: int + ) -> Optional[int]: + # some PDFs have /Prev=0 in the trailer, instead of no /Prev + if startxref == 0: + if self.strict: + raise PdfReadError( + "/Prev=0 in the trailer (try opening with strict=False)" + ) + logger_warning( + "/Prev=0 in the trailer - assuming there is no previous xref table", + source=__name__, + ) + return None + # bad xref character at startxref. Let's see if we can find + # the xref table nearby, as we've observed this error with an + # off-by-one before. + stream.seek(-11, 1) + tmp = stream.read(20) + xref_loc = tmp.find(b"xref") + if xref_loc != -1: + startxref -= 10 - xref_loc + return startxref + # No explicit xref table, try finding a cross-reference stream. + stream.seek(startxref, 0) + for look in range(25): # value extended to cope with more linearized files + if stream.read(1).isdigit(): + # This is not a standard PDF, consider adding a warning + startxref += look + return startxref + # no xref table found at specified location + if "/Root" in self.trailer and not self.strict: + # if Root has been already found, just raise warning + logger_warning("Invalid parent xref., rebuild xref", source=__name__) + try: + self._rebuild_xref_table(stream) + return None + except Exception: + raise PdfReadError("Cannot rebuild xref") + raise PdfReadError("Could not find xref table at specified location") + + def _sanitize_pdf15_xref_stream_index_pairs( + self, index_pairs: list[int], entry_sizes: list[int], xref_stream: StreamObject + ) -> list[int]: + # `entry_sizes` holds the byte widths for the entries. Summing determines the total number of bytes per entry. + # We expect up to 3 values. `min_entry_bytes` will be the smallest plausible size of one xref entry. + min_entry_bytes = sum(int(entry_sizes[i]) for i in range(min(len(entry_sizes), 3))) + if min_entry_bytes == 0: + message = "Cross-reference stream encodes no entry data." + if self.strict: + raise PdfStreamError(message) + logger_warning(message, source=__name__) + return [] + + # maximum number of entries that could physically fit + max_entries = len(xref_stream.get_data()) // min_entry_bytes + 1 + + result = [] + total = 0 + + for index, pair_value in enumerate(index_pairs): + pair_value_int = int(pair_value) + + # `index_pairs` has the format `[start0, count0, start1, count1, ...]` + # Only modify the counts here, but keep the start values. + if index % 2 == 1: + if total + pair_value_int > max_entries: + if self.strict: + raise LimitReachedError( + f"Total XRef entries {total + pair_value_int} exceed maximum allowed value {max_entries}." + ) + new_v = max(0, max_entries - total) + logger_warning( + "Clamping XRef count from %(old_count)d to %(new_count)d to fit stream size.", + source=__name__, + old_count=pair_value_int, + new_count=new_v, + ) + pair_value_int = new_v + + total += pair_value_int + + result.append(pair_value_int) + + return result + + def _read_pdf15_xref_stream(self, stream: StreamType) -> StreamObject: + """Read the cross-reference stream for PDF 1.5+.""" + stream.seek(-1, 1) + stream_idnum, stream_generation = self.read_object_header(stream) + xref_stream = cast(StreamObject, read_object(stream, self)) + if cast(str, xref_stream["/Type"]) != "/XRef": + raise PdfReadError(f"Unexpected type {xref_stream['/Type']!r}") + self.cache_indirect_object(stream_generation, stream_idnum, xref_stream) + + # Index pairs specify the subsections in the dictionary. + # If none, create one subsection that spans everything. + if "/Size" not in xref_stream: + # According to table 17 of the PDF 2.0 specification, this key is required. + raise PdfReadError(f"Size missing from XRef stream {xref_stream!r}!") + index_pairs = xref_stream.get("/Index", [0, xref_stream["/Size"]]) + + entry_sizes = cast(list[int], xref_stream.get("/W")) + assert len(entry_sizes) >= 3 + if self.strict and len(entry_sizes) > 3: + raise PdfReadError(f"Too many entry sizes: {entry_sizes}") + index_pairs = self._sanitize_pdf15_xref_stream_index_pairs( + index_pairs=index_pairs, entry_sizes=entry_sizes, xref_stream=xref_stream + ) + + stream_data = BytesIO(xref_stream.get_data()) + + def get_entry(i: int) -> Union[int, tuple[int, ...]]: + # Reads the correct number of bytes for each entry. See the + # discussion of the W parameter in PDF spec table 17. + if entry_sizes[i] > 0: + d = stream_data.read(entry_sizes[i]) + return convert_to_int(d, entry_sizes[i]) + + # PDF Spec Table 17: A value of zero for an element in the + # W array indicates...the default value shall be used + if i == 0: + return 1 # First value defaults to 1 + return 0 + + def used_before(num: int, generation: Union[int, tuple[int, ...]]) -> bool: + # We move backwards through the xrefs, don't replace any. + return num in self.xref.get(generation, []) or num in self.xref_objStm # type: ignore[arg-type] + + # Iterate through each subsection + self._read_xref_subsections(index_pairs, get_entry, used_before) + return xref_stream + + @staticmethod + def _get_xref_issues(stream: StreamType, startxref: int) -> int: + """ + Return an int which indicates an issue. 0 means there is no issue. + + Args: + stream: + startxref: + + Returns: + 0 means no issue, other values represent specific issues. + + """ + if startxref == 0: + return 4 + + stream.seek(startxref - 1, 0) # -1 to check character before + line = stream.read(1) + if line == b"j": + line = stream.read(1) + if line not in b"\r\n \t": + return 1 + line = stream.read(4) + if line != b"xref": + # not a xref so check if it is an XREF object + line = b"" + while line in b"0123456789 \t": + line = stream.read(1) + if line == b"": + return 2 + line += stream.read(2) # 1 char already read, +2 to check "obj" + if line.lower() != b"obj": + return 3 + return 0 + + @classmethod + def _find_pdf_objects(cls, data: bytes) -> Iterable[tuple[int, int, int]]: + index = 0 + ord_0 = ord("0") + ord_9 = ord("9") + while True: + index = data.find(b" obj", index) + if index == -1: + return + + index_before_space = index - 1 + + # Skip whitespace backwards + while index_before_space >= 0 and data[index_before_space] in WHITESPACES_AS_BYTES: + index_before_space -= 1 + + # Read generation number + generation_end = index_before_space + 1 + while index_before_space >= 0 and ord_0 <= data[index_before_space] <= ord_9: + index_before_space -= 1 + generation_start = index_before_space + 1 + + # Skip whitespace + while index_before_space >= 0 and data[index_before_space] in WHITESPACES_AS_BYTES: + index_before_space -= 1 + + # Read object number + object_end = index_before_space + 1 + while index_before_space >= 0 and ord_0 <= data[index_before_space] <= ord_9: + index_before_space -= 1 + object_start = index_before_space + 1 + + # Validate + if object_start < object_end and generation_start < generation_end: + object_number = int(data[object_start:object_end]) + generation_number = int(data[generation_start:generation_end]) + + yield object_number, generation_number, object_start + + index += 4 # len(b" obj") + + @classmethod + def _find_pdf_trailers(cls, data: bytes) -> Iterable[int]: + index = 0 + data_length = len(data) + while True: + index = data.find(b"trailer", index) + if index == -1: + return + + index_after_trailer = index + 7 # len(b"trailer") + + # Skip whitespace + while index_after_trailer < data_length and data[index_after_trailer] in WHITESPACES_AS_BYTES: + index_after_trailer += 1 + + # Must be dictionary start + if index_after_trailer + 1 < data_length and data[index_after_trailer:index_after_trailer+2] == b"<<": + yield index_after_trailer # offset of '<<' + + index += 7 # len(b"trailer") + + def _rebuild_xref_table(self, stream: StreamType) -> None: + self.xref = {} + stream.seek(0, 0) + stream_data = stream.read(-1) + + for object_number, generation_number, object_start in self._find_pdf_objects(stream_data): + if generation_number not in self.xref: + self.xref[generation_number] = {} + self.xref[generation_number][object_number] = object_start + + logger_warning("parsing for Object Streams", source=__name__) + # PDF 1.5+ files may carry the trailer keys inside a cross-reference + # stream instead of behind a `trailer` keyword. Collect them here, + # keyed by their offset, to merge them below in file order. + xref_stream_trailers: list[tuple[int, DictionaryObject]] = [] + for generation_number in self.xref: + for object_number in self.xref[generation_number]: + # get_object in manual + object_start = self.xref[generation_number][object_number] + stream.seek(object_start, 0) + try: + _ = self.read_object_header(stream) + obj = cast(StreamObject, read_object(stream, self)) + object_type = obj.get("/Type", "") + if object_type == "/XRef": + trailer = DictionaryObject() + for key in self._XREF_STREAM_TRAILER_KEYS: + if key in obj: + trailer[NameObject(key)] = obj.raw_get(key) + xref_stream_trailers.append((object_start, trailer)) + continue + if object_type != "/ObjStm": + continue + object_stream = BytesIO(obj.get_data()) + actual_count = 0 + while True: + current = read_until_whitespace( + object_stream, max_bytes=IndirectObject._MAXIMUM_PART_LENGTH, strict=self.strict + ) + if not current.isdigit(): + break + inner_object_number = int(current) + skip_over_whitespace(object_stream) + object_stream.seek(-1, 1) + current = read_until_whitespace( + object_stream, max_bytes=IndirectObject._MAXIMUM_PART_LENGTH, strict=self.strict + ) + if not current.isdigit(): # pragma: no cover + break # pragma: no cover + inner_offset = int(current) + self.xref_objStm[inner_object_number] = (object_number, inner_offset) + actual_count += 1 + expected_count = cast(int, obj["/N"]) + if actual_count != expected_count: # pragma: no cover + logger_warning( # pragma: no cover + ( + "found %(actual_count)d objects within " + "Object(%(object_number)d,%(generation_number)d) " + "whereas %(expected)d expected" + ), + source=__name__, + actual_count=actual_count, + object_number=object_number, + generation_number=generation_number, + expected=expected_count, + ) + except LimitReachedError: + # Do not let the broad recovery below bypass the token-length limit + # when strict parsing is enabled. + raise + except Exception: # could be multiple causes + pass + + stream.seek(0, 0) + trailers: list[tuple[int, dict[Any, Any]]] = list(xref_stream_trailers) + for position in self._find_pdf_trailers(stream_data): + stream.seek(position, 0) + trailers.append((position, cast(dict[Any, Any], read_object(stream, self)))) + # Here, we are parsing the file from start to end, the new data have to erase the existing. + for _, new_trailer in sorted(trailers, key=itemgetter(0)): + for key, value in new_trailer.items(): + self.trailer[key] = value + + def _read_xref_subsections( + self, + idx_pairs: list[int], + get_entry: Callable[[int], Union[int, tuple[int, ...]]], + used_before: Callable[[int, Union[int, tuple[int, ...]]], bool], + ) -> None: + """Read and process the subsections of the xref.""" + for start, size in self._pairs(idx_pairs): + # The subsections must increase + for num in range(start, start + size): + # The first entry is the type + xref_type = get_entry(0) + # The rest of the elements depend on the xref_type + if xref_type == 0: + # linked list of free objects + next_free_object = get_entry(1) # noqa: F841 + next_generation = get_entry(2) # noqa: F841 + elif xref_type == 1: + # objects that are in use but are not compressed + byte_offset = get_entry(1) + generation = get_entry(2) + if generation not in self.xref: + self.xref[generation] = {} # type: ignore[index] + if not used_before(num, generation): + self.xref[generation][num] = byte_offset # type: ignore[index] + elif xref_type == 2: + # compressed objects + objstr_num = get_entry(1) + obstr_idx = get_entry(2) + generation = 0 # PDF spec table 18, generation is 0 + if not used_before(num, generation): + self.xref_objStm[num] = (objstr_num, obstr_idx) + elif self.strict: + raise PdfReadError(f"Unknown xref type: {xref_type}") + + def _pairs(self, array: list[int]) -> Iterable[tuple[int, int]]: + """Iterate over pairs in the array.""" + i = 0 + while i + 1 < len(array): + yield array[i], array[i + 1] + i += 2 + + def decrypt(self, password: Union[str, bytes]) -> PasswordType: + """ + When using an encrypted / secured PDF file with the PDF Standard + encryption handler, this function will allow the file to be decrypted. + It checks the given password against the document's user password and + owner password, and then stores the resulting decryption key if either + password is correct. + + It does not matter which password was matched. Both passwords provide + the correct decryption key that will allow the document to be used with + this library. + + Args: + password: The password to match. + + Returns: + An indicator if the document was decrypted and whether it was the + owner password or the user password. + + """ + if not self._encryption: + raise PdfReadError("Not encrypted file") + # TODO: raise Exception for wrong password + return self._encryption.verify(password, strict=self.strict) + + @property + def is_encrypted(self) -> bool: + """ + Read-only boolean property showing whether this PDF file is encrypted. + + Note that this property, if true, will remain true even after the + :meth:`decrypt()` method is called. + """ + return TK.ENCRYPT in self.trailer + + def add_form_topname(self, name: str) -> Optional[DictionaryObject]: + """ + Add a top level form that groups all form fields below it. + + Args: + name: text string of the "/T" Attribute of the created object + + Returns: + The created object. ``None`` means no object was created. + + """ + catalog = self.root_object + + if "/AcroForm" not in catalog or not isinstance( + catalog["/AcroForm"], DictionaryObject + ): + return None + acroform = cast(DictionaryObject, catalog[NameObject("/AcroForm")]) + if "/Fields" not in acroform: + # TODO: No error but this may be extended for XFA Forms + return None + + interim = DictionaryObject() + interim[NameObject("/T")] = TextStringObject(name) + interim[NameObject("/Kids")] = acroform[NameObject("/Fields")] + self.cache_indirect_object( + 0, + max(i for (g, i) in self.resolved_objects if g == 0) + 1, + interim, + ) + arr = ArrayObject() + arr.append(interim.indirect_reference) + acroform[NameObject("/Fields")] = arr + for o in cast(ArrayObject, interim["/Kids"]): + obj = o.get_object() + if "/Parent" in obj: + logger_warning( + "Top Level Form Field %(obj_ref)s has a non-expected parent", + source=__name__, + obj_ref=obj.indirect_reference, + ) + obj[NameObject("/Parent")] = interim.indirect_reference + return interim + + def rename_form_topname(self, name: str) -> Optional[DictionaryObject]: + """ + Rename top level form field that all form fields below it. + + Args: + name: text string of the "/T" field of the created object + + Returns: + The modified object. ``None`` means no object was modified. + + """ + catalog = self.root_object + + if "/AcroForm" not in catalog or not isinstance( + catalog["/AcroForm"], DictionaryObject + ): + return None + acroform = cast(DictionaryObject, catalog[NameObject("/AcroForm")]) + if "/Fields" not in acroform: + return None + + interim = cast( + DictionaryObject, + cast(ArrayObject, acroform[NameObject("/Fields")])[0].get_object(), + ) + interim[NameObject("/T")] = TextStringObject(name) + return interim + + def _repr_mimebundle_( + self, + include: Union[Iterable[str], None] = None, + exclude: Union[Iterable[str], None] = None, + ) -> dict[str, Any]: + """ + Integration into Jupyter Notebooks. + + This method returns a dictionary that maps a mime-type to its + representation. + + .. seealso:: + + https://ipython.readthedocs.io/en/stable/config/integrating.html + """ + self.stream.seek(0) + pdf_data = self.stream.read() + data = { + "application/pdf": pdf_data, + } + + if include is not None: + # Filter representations based on include list + data = {k: v for k, v in data.items() if k in include} + + if exclude is not None: + # Remove representations based on exclude list + data = {k: v for k, v in data.items() if k not in exclude} + + return data + + def _get_named_destinations( + self, + *, + tree: Optional[DictionaryObject] = None, + retval: Optional[dict[str, Destination]] = None, + visited: Optional[set[int]] = None, + ) -> dict[str, Destination]: + """Override from PdfDocCommon. In the reader we can assume this is + static, but not in the writer. + """ + if tree or retval: + return super()._get_named_destinations(tree=tree, retval=retval, visited=visited) + + if self._named_destinations_cache is None: + self._named_destinations_cache = super()._get_named_destinations() + return self._named_destinations_cache diff --git a/app/vendor/pypdf/_text_extraction/__init__.py b/app/vendor/pypdf/_text_extraction/__init__.py new file mode 100644 index 0000000..0428d13 --- /dev/null +++ b/app/vendor/pypdf/_text_extraction/__init__.py @@ -0,0 +1,269 @@ +""" +Code related to text extraction. + +Some parts are still in _page.py. In doubt, they will stay there. +""" + +import math +from collections.abc import Mapping +from typing import Any, Callable, Literal, Optional, Union + +from .._font import Font +from .._utils import is_char_neutral, is_char_rtl +from ..generic import DictionaryObject, TextStringObject, encode_pdfdocencoding + +CUSTOM_RTL_MIN: str = "" +CUSTOM_RTL_MAX: str = "" +CUSTOM_RTL_SPECIAL_CHARS: str = "" +LAYOUT_NEW_BT_GROUP_SPACE_WIDTHS: int = 5 +UNICODE_LOWER_LIMIT = 0 +UNICODE_UPPER_LIMIT = 0x10FFFF + + +class OrientationNotFoundError(Exception): + pass + + +def set_custom_rtl( + _min: Union[str, int, None] = "", + _max: Union[str, int, None] = "", + specials: Union[str, list[int], None] = None, +) -> tuple[str, str, str]: + """ + Change the Right-To-Left and special characters custom parameters. + + Args: + _min: The new minimum value for the range of custom characters that + will be written right to left. + If set to ``None``, the value will not be changed. + If set to a valid integer, it will be converted to its corresponding character. + The default value is "", which sets no additional range to be converted. + _max: The new maximum value for the range of custom characters that will + be written right to left. + If set to ``None``, the value will not be changed. + If set to a valid integer, it will be converted to its corresponding character. + The default value is "", which sets no additional range to be converted. + specials: The new list of special characters to be inserted in the + current insertion order. + If set to ``None``, the current value will not be changed. + If set to a string, it will be converted to a list of ASCII codes. + The default value is an empty list. + + Returns: + A tuple containing the new values for ``CUSTOM_RTL_MIN``, + ``CUSTOM_RTL_MAX``, and ``CUSTOM_RTL_SPECIAL_CHARS``. + + """ + global CUSTOM_RTL_MIN, CUSTOM_RTL_MAX, CUSTOM_RTL_SPECIAL_CHARS + if isinstance(_min, int): + CUSTOM_RTL_MIN = chr(_min) if UNICODE_LOWER_LIMIT <= _min <= UNICODE_UPPER_LIMIT else "" + elif isinstance(_min, str): + CUSTOM_RTL_MIN = _min + if isinstance(_max, int): + CUSTOM_RTL_MAX = chr(_max) if UNICODE_LOWER_LIMIT <= _max <= UNICODE_UPPER_LIMIT else "" + elif isinstance(_max, str): + CUSTOM_RTL_MAX = _max + if isinstance(specials, str): + CUSTOM_RTL_SPECIAL_CHARS = specials + elif isinstance(specials, list): + CUSTOM_RTL_SPECIAL_CHARS = "".join( + chr(char) for char in specials if UNICODE_LOWER_LIMIT <= char <= UNICODE_UPPER_LIMIT + ) + return CUSTOM_RTL_MIN, CUSTOM_RTL_MAX, CUSTOM_RTL_SPECIAL_CHARS + + +def mult( + m: list[float], + n: Union[ + list[float], + Mapping[Union[int, Literal["is_text", "is_render"]], Union[float, bool]], + ], +) -> list[float]: + return [ + m[0] * n[0] + m[1] * n[2], + m[0] * n[1] + m[1] * n[3], + m[2] * n[0] + m[3] * n[2], + m[2] * n[1] + m[3] * n[3], + m[4] * n[0] + m[5] * n[2] + n[4], + m[4] * n[1] + m[5] * n[3] + n[5], + ] + + +def orient(m: list[float]) -> int: + if m[3] > 1e-6: + return 0 + if m[3] < -1e-6: + return 180 + if m[1] > 0: + return 90 + return 270 + + +def crlf_space_check( + text: str, + cmtm_prev: tuple[list[float], list[float]], + cmtm_matrix: tuple[list[float], list[float]], + memo_cmtm: tuple[list[float], list[float]], + font_resource: Optional[DictionaryObject], + orientations: tuple[int, ...], + output: str, + font_size: float, + visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]], + str_widths: float, + spacewidth: float, + str_height: float, +) -> tuple[str, str, list[float], list[float]]: + cm_prev = cmtm_prev[0] + tm_prev = cmtm_prev[1] + cm_matrix = cmtm_matrix[0] + tm_matrix = cmtm_matrix[1] + memo_cm = memo_cmtm[0] + memo_tm = memo_cmtm[1] + + m_prev = mult(tm_prev, cm_prev) + m = mult(tm_matrix, cm_matrix) + orientation = orient(m) + delta_x = m[4] - m_prev[4] + delta_y = m[5] - m_prev[5] + # Table 108 of the 1.7 reference ("Text positioning operators") + # delta_x/delta_y are expressed in the coordinate system produced by + # text matrix x current transformation matrix, so the scaling factors + # they get compared against have to be taken from the same combined + # matrices instead of the text matrices alone. + scale_prev_x = math.sqrt(m_prev[0]**2 + m_prev[1]**2) + scale_prev_y = math.sqrt(m_prev[2]**2 + m_prev[3]**2) + scale_y = math.sqrt(m[2]**2 + m[3]**2) + cm_prev = m + + if orientation not in orientations: + raise OrientationNotFoundError + if orientation in (0, 180): + moved_height: float = delta_y + moved_width: float = delta_x + elif orientation in (90, 270): + moved_height = delta_x + moved_width = delta_y + try: + if abs(moved_height) > 0.8 * min(str_height * scale_prev_y, font_size * scale_y): + if (output + text)[-1] != "\n": + output += text + "\n" + if visitor_text is not None: + visitor_text( + text + "\n", + memo_cm, + memo_tm, + font_resource, + font_size, + ) + text = "" + elif ( + (moved_width >= (spacewidth + str_widths) * scale_prev_x) + and (output + text)[-1] != " " + ): + text += " " + except Exception: + pass + tm_prev = tm_matrix.copy() + cm_prev = cm_matrix.copy() + return text, output, cm_prev, tm_prev + + +def get_text_operands( + operands: list[Union[str, TextStringObject]], + cm_matrix: list[float], + tm_matrix: list[float], + font: Font, + orientations: tuple[int, ...] +) -> tuple[str, bool]: + t: str = "" + is_str_operands = False + m = mult(tm_matrix, cm_matrix) + orientation = orient(m) + if orientation in orientations and len(operands) > 0: + if isinstance(operands[0], str): + t = operands[0] + is_str_operands = True + else: + t = "" + tt: bytes = ( + encode_pdfdocencoding(operands[0]) + if isinstance(operands[0], str) + else operands[0] + ) + if isinstance(font.encoding, str): # Apply named encoding + try: + t = tt.decode(font.encoding, "surrogatepass") + except Exception: + # The data does not match the expectation, + # we use "charmap" encoding as an alternative; + # text extraction may not be good. + t = tt.decode("charmap", "surrogatepass") + else: # Apply dict encoding + t = "".join( + [font.encoding[x] if x in font.encoding else bytes((x,)).decode() for x in tt] + ) + return (t, is_str_operands) + + +def get_display_str( + text: str, + cm_matrix: list[float], + tm_matrix: list[float], + font_resource: Optional[DictionaryObject], + font: Font, + text_operands: str, + font_size: float, + rtl_dir: bool, + visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] +) -> tuple[str, bool, float]: + # "\u0590 - \u08FF \uFB50 - \uFDFF" + widths: float = 0.0 + width_cache: dict[str, float] = {} + neutral_cache: dict[str, bool] = {} + rtl_cache: dict[str, bool] = {} + + def clear_character_caches() -> None: + width_cache.clear() + neutral_cache.clear() + rtl_cache.clear() + + for raw_character in text_operands: + if raw_character == font.space_char: + widths += font.space_width + else: + if raw_character not in width_cache: + width_cache[raw_character] = font.get_text_width(raw_character) + widths += width_cache[raw_character] + x = font.character_map.get(raw_character, raw_character) + # Test whether x is a sequence of bytes; ex: habibi.pdf + if len(x) == 1: + if x not in neutral_cache: + neutral_cache[x] = is_char_neutral(x, CUSTOM_RTL_SPECIAL_CHARS) + if neutral_cache[x]: + # Cases where the current inserting order is kept + text = x + text if rtl_dir else text + x + else: + if x not in rtl_cache: + rtl_cache[x] = is_char_rtl(x, CUSTOM_RTL_MIN, CUSTOM_RTL_MAX) + if rtl_cache[x]: + # Right-to-left characters + if not rtl_dir: + rtl_dir = True + if visitor_text is not None: + visitor_text(text, cm_matrix, tm_matrix, font_resource, font_size) + clear_character_caches() + text = "" + text = x + text + else: + # Left-to-right characters + if rtl_dir: + rtl_dir = False + if visitor_text is not None: + visitor_text(text, cm_matrix, tm_matrix, font_resource, font_size) + clear_character_caches() + text = "" + text = text + x + else: + # Treat a sequence of bytes as a neutral character. + text = x + text if rtl_dir else text + x + return text, rtl_dir, widths diff --git a/app/vendor/pypdf/_text_extraction/_layout_mode/__init__.py b/app/vendor/pypdf/_text_extraction/_layout_mode/__init__.py new file mode 100644 index 0000000..d315160 --- /dev/null +++ b/app/vendor/pypdf/_text_extraction/_layout_mode/__init__.py @@ -0,0 +1,16 @@ +"""Layout mode text extraction extension for pypdf""" +from ..._font import Font +from ._fixed_width_page import ( + fixed_char_width, + fixed_width_page, + text_show_operations, + y_coordinate_groups, +) + +__all__ = [ + "Font", + "fixed_char_width", + "fixed_width_page", + "text_show_operations", + "y_coordinate_groups", +] diff --git a/app/vendor/pypdf/_text_extraction/_layout_mode/_fixed_width_page.py b/app/vendor/pypdf/_text_extraction/_layout_mode/_fixed_width_page.py new file mode 100644 index 0000000..0954db5 --- /dev/null +++ b/app/vendor/pypdf/_text_extraction/_layout_mode/_fixed_width_page.py @@ -0,0 +1,447 @@ +"""Extract PDF text preserving the layout of the source PDF""" + +from collections.abc import Iterator +from itertools import groupby +from math import ceil +from pathlib import Path +from typing import Any, Literal, Optional, TypedDict + +from ..._font import Font +from ..._utils import logger_warning +from .. import LAYOUT_NEW_BT_GROUP_SPACE_WIDTHS +from ._text_state_manager import TextStateManager +from ._text_state_params import TextStateParams + +WHITESPACE_LIMIT = 10_000 +NEWLINE_LIMIT = 1_000 + + +class BTGroup(TypedDict): + """ + Dict describing a line of text rendered within a BT/ET operator pair. + If multiple text show operations render text on the same line, the text + will be combined into a single BTGroup dict. + + Keys: + tx: x coordinate of first character in BTGroup + ty: y coordinate of first character in BTGroup + font_size: nominal font size + font_height: effective font height + text: rendered text + displaced_tx: x coordinate of last character in BTGroup + flip_sort: -1 if page is upside down, else 1 + """ + + tx: float + ty: float + font_size: float + font_height: float + text: str + displaced_tx: float + flip_sort: Literal[-1, 1] + + +def resolve_font(fonts: dict[str, Font], name: str) -> Font: + """ + Resolve a Tf font name to a layout mode Font. + + A content stream may select a font name that is not declared in the page + resources. Fall back to an uninterpretable font so extraction degrades to + the existing incomplete-output path instead of raising KeyError. + + Args: + fonts: font dictionary as returned by PageObject._layout_mode_fonts() + name: font name supplied by a Tf operator + + Returns: + Font: the matching font, or an uninterpretable placeholder font. + + """ + if name in fonts: + return fonts[name] + logger_warning( + "Font %(name)s is not in the page resources.", + name=name, source=__name__ + ) + return Font("Unknown", encoding={}, interpretable=False) + + +def bt_group(tj_op: TextStateParams, rendered_text: str, displaced_tx: float) -> BTGroup: + """ + BTGroup constructed from a TextStateParams instance, rendered text, and + displaced tx value. + + Args: + tj_op (TextStateParams): TextStateParams instance + rendered_text (str): rendered text + displaced_tx (float): x coordinate of last character in BTGroup + + """ + return BTGroup( + tx=tj_op.tx, + ty=tj_op.ty, + font_size=tj_op.font_size, + font_height=tj_op.font_height, + text=rendered_text, + displaced_tx=displaced_tx, + flip_sort=-1 if tj_op.flip_vertical else 1, + ) + + +def recurse_to_target_op( + ops: Iterator[tuple[list[Any], bytes]], + text_state_mgr: TextStateManager, + end_target: Literal[b"Q", b"ET"], + fonts: dict[str, Font], + strip_rotated: bool = True, +) -> tuple[list[BTGroup], list[TextStateParams]]: + """ + Recurse operators between BT/ET and/or q/Q operators managing the transform + stack and capturing text positioning and rendering data. + + Args: + ops: iterator of operators in content stream + text_state_mgr: a TextStateManager instance + end_target: Either b"Q" (ends b"q" op) or b"ET" (ends b"BT" op) + fonts: font dictionary as returned by PageObject._layout_mode_fonts() + + Returns: + tuple: list of BTGroup dicts + list of TextStateParams dataclass instances. + + """ + # 1 entry per line of text rendered within each BT/ET operation. + bt_groups: list[BTGroup] = [] + + # 1 entry per text show operator (Tj/TJ/'/") + tj_ops: list[TextStateParams] = [] + + if end_target == b"Q": + # add new q level. cm's added at this level will be popped at next b'Q' + text_state_mgr.add_q() + + for operands, op in ops: + # The loop is broken by the end target, or exits normally when there are no more ops. + if op == end_target: + if op == b"Q": + text_state_mgr.remove_q() + if op == b"ET": + if not tj_ops: + return bt_groups, tj_ops + _text = "" + bt_idx = 0 # idx of first tj in this bt group + last_displaced_tx = tj_ops[bt_idx].displaced_tx + last_ty = tj_ops[bt_idx].ty + for _idx, _tj in enumerate( + tj_ops + ): # ... build text from new Tj operators + if strip_rotated and _tj.rotated: + continue + if not _tj.font.interpretable: # generates warning + continue + # if the y position of the text is greater than the font height, assume + # the text is on a new line and start a new group + if abs(_tj.ty - last_ty) > _tj.font_height: + if _text.strip(): + bt_groups.append( + bt_group(tj_ops[bt_idx], _text, last_displaced_tx) + ) + bt_idx = _idx + _text = "" + + # if the x position of the text is less than the last x position by + # more than 5 spaces widths, assume the text order should be flipped + # and start a new group + if ( + last_displaced_tx - _tj.tx + > _tj.space_tx * LAYOUT_NEW_BT_GROUP_SPACE_WIDTHS + ): + if _text.strip(): + bt_groups.append( + bt_group(tj_ops[bt_idx], _text, last_displaced_tx) + ) + bt_idx = _idx + last_displaced_tx = _tj.displaced_tx + _text = "" + + # calculate excess x translation based on ending tx of previous Tj. + # multiply by bool (_idx != bt_idx) to ensure spaces aren't double + # applied to the first tj of a BTGroup in fixed_width_page(). + excess_tx = round(_tj.tx - last_displaced_tx, 3) * (_idx != bt_idx) + # space_tx could be 0 if either Tz or font_size was 0 for this _tj. + spaces = round(excess_tx / _tj.space_tx) if excess_tx > 0 else 0 + if spaces > WHITESPACE_LIMIT: + logger_warning( + "Limiting excessive whitespace from %(actual)d to %(limit)d characters.", + actual=spaces, limit=WHITESPACE_LIMIT, source=__name__ + ) + spaces = WHITESPACE_LIMIT + new_text = f'{" " * spaces}{_tj.text}' + + last_ty = _tj.ty + _text = f"{_text}{new_text}" + last_displaced_tx = _tj.displaced_tx + if _text: + bt_groups.append(bt_group(tj_ops[bt_idx], _text, last_displaced_tx)) + text_state_mgr.reset_tm() + break + if op == b"q": + bts, tjs = recurse_to_target_op( + ops, text_state_mgr, b"Q", fonts, strip_rotated + ) + bt_groups.extend(bts) + tj_ops.extend(tjs) + elif op == b"cm": + text_state_mgr.add_cm(*operands) + elif op == b"BT": + bts, tjs = recurse_to_target_op( + ops, text_state_mgr, b"ET", fonts, strip_rotated + ) + bt_groups.extend(bts) + tj_ops.extend(tjs) + elif op == b"Tj": + tj_ops.append(text_state_mgr.text_state_params(operands[0])) + elif op == b"TJ": + _tj = text_state_mgr.text_state_params() + for tj_op in operands[0]: + if isinstance(tj_op, bytes): + _tj = text_state_mgr.text_state_params(tj_op) + tj_ops.append(_tj) + else: + text_state_mgr.add_trm(_tj.displacement_matrix(td_offset=tj_op)) + elif op == b"'": + text_state_mgr.reset_trm() + text_state_mgr.add_tm([0, -text_state_mgr.TL]) + tj_ops.append(text_state_mgr.text_state_params(operands[0])) + elif op == b'"': + text_state_mgr.reset_trm() + text_state_mgr.set_state_param(b"Tw", operands[0]) + text_state_mgr.set_state_param(b"Tc", operands[1]) + text_state_mgr.add_tm([0, -text_state_mgr.TL]) + tj_ops.append(text_state_mgr.text_state_params(operands[2])) + elif op in (b"Td", b"Tm", b"TD", b"T*"): + text_state_mgr.reset_trm() + if op == b"Tm": + text_state_mgr.reset_tm() + elif op == b"TD": + text_state_mgr.set_state_param(b"TL", -operands[1]) + elif op == b"T*": + operands = [0, -text_state_mgr.TL] + text_state_mgr.add_tm(operands) + elif op == b"Tf": + text_state_mgr.set_font(resolve_font(fonts, operands[0]), operands[1]) + else: # handle Tc, Tw, Tz, TL, and Ts operators + text_state_mgr.set_state_param(op, operands) + else: + logger_warning( + "Unbalanced target operations, expected %(end_target)r.", + source=__name__, + end_target=end_target, + ) + return bt_groups, tj_ops + + +def y_coordinate_groups( + bt_groups: list[BTGroup], debug_path: Optional[Path] = None +) -> dict[int, list[BTGroup]]: + """ + Group text operations by rendered y coordinate, i.e. the line number. + + Args: + bt_groups: list of dicts as returned by text_show_operations() + debug_path (Path, optional): Path to a directory for saving debug output. + + Returns: + Dict[int, List[BTGroup]]: dict of lists of text rendered by each BT operator + keyed by y coordinate + + """ + ty_groups = { + ty: sorted(grp, key=lambda x: x["tx"]) + for ty, grp in groupby( + bt_groups, key=lambda bt_grp: int(bt_grp["ty"] * bt_grp["flip_sort"]) + ) + } + # combine groups whose y coordinates differ by less than the effective font height + # (accounts for mixed fonts and other minor oddities) + last_ty = next(iter(ty_groups)) + last_txs = {int(_t["tx"]) for _t in ty_groups[last_ty] if _t["text"].strip()} + for ty in list(ty_groups)[1:]: + fsz = min(ty_groups[_y][0]["font_height"] for _y in (ty, last_ty)) + txs = {int(_t["tx"]) for _t in ty_groups[ty] if _t["text"].strip()} + # prevent merge if both groups are rendering in the same x position. + no_text_overlap = not (txs & last_txs) + offset_less_than_font_height = abs(ty - last_ty) < fsz + if no_text_overlap and offset_less_than_font_height: + ty_groups[last_ty] = sorted( + ty_groups.pop(ty) + ty_groups[last_ty], key=lambda x: x["tx"] + ) + last_txs |= txs + else: + last_ty = ty + last_txs = txs + if debug_path: # pragma: no cover + import json # noqa: PLC0415 + + debug_path.joinpath("bt_groups.json").write_text( + json.dumps(ty_groups, indent=2, default=str), "utf-8" + ) + return ty_groups + + +def text_show_operations( + ops: Iterator[tuple[list[Any], bytes]], + fonts: dict[str, Font], + strip_rotated: bool = True, + debug_path: Optional[Path] = None, +) -> list[BTGroup]: + """ + Extract text from BT/ET operator pairs. + + Args: + ops (Iterator[Tuple[List, bytes]]): iterator of operators in content stream + fonts (Dict[str, Font]): font dictionary + strip_rotated: Removes text if rotated w.r.t. to the page. Defaults to True. + debug_path (Path, optional): Path to a directory for saving debug output. + + Returns: + List[BTGroup]: list of dicts of text rendered by each BT operator + + """ + state_mgr = TextStateManager() # transformation stack manager + bt_groups: list[BTGroup] = [] # BT operator dict + tj_ops: list[TextStateParams] = [] # Tj/TJ operator data + for operands, op in ops: + if op in (b"BT", b"q"): + bts, tjs = recurse_to_target_op( + ops, state_mgr, b"ET" if op == b"BT" else b"Q", fonts, strip_rotated + ) + bt_groups.extend(bts) + tj_ops.extend(tjs) + elif op == b"Tf": + state_mgr.set_font(resolve_font(fonts, operands[0]), operands[1]) + else: # set Tc, Tw, Tz, TL, and Ts if required. ignores all other ops + state_mgr.set_state_param(op, operands) + + if any(tj.rotated for tj in tj_ops): + if strip_rotated: + logger_warning( + "Rotated text discovered. Output will be incomplete.", source=__name__ + ) + else: + logger_warning( + "Rotated text discovered. Layout will be degraded.", source=__name__ + ) + if not all(tj.font.interpretable for tj in tj_ops): + logger_warning( + "PDF contains an uninterpretable font. Output will be incomplete.", source=__name__ + ) + + # left align the data, i.e. decrement all tx values by min(tx) + min_x = min((x["tx"] for x in bt_groups), default=0.0) + bt_groups = [ + dict(ogrp, tx=ogrp["tx"] - min_x, displaced_tx=ogrp["displaced_tx"] - min_x) # type: ignore[misc] + for ogrp in sorted( + bt_groups, key=lambda x: (x["ty"] * x["flip_sort"], -x["tx"]), reverse=True + ) + ] + + if debug_path: # pragma: no cover + import json # noqa: PLC0415 + + debug_path.joinpath("bts.json").write_text( + json.dumps(bt_groups, indent=2, default=str), "utf-8" + ) + debug_path.joinpath("tjs.json").write_text( + json.dumps( + tj_ops, indent=2, default=lambda x: getattr(x, "to_dict", str)(x) + ), + "utf-8", + ) + return bt_groups + + +def fixed_char_width(bt_groups: list[BTGroup], scale_weight: float = 1.25) -> float: + """ + Calculate average character width weighted by the length of the rendered + text in each sample for conversion to fixed-width layout. + + Args: + bt_groups (List[BTGroup]): List of dicts of text rendered by each + BT operator + + Returns: + float: fixed character width + + """ + char_widths = [] + for _bt in bt_groups: + _len = len(_bt["text"]) * scale_weight + char_widths.append(((_bt["displaced_tx"] - _bt["tx"]) / _len, _len)) + return sum(_w * _l for _w, _l in char_widths) / sum(_l for _, _l in char_widths) + + +def fixed_width_page( + ty_groups: dict[int, list[BTGroup]], char_width: float, space_vertically: bool, font_height_weight: float +) -> str: + """ + Generate page text from text operations grouped by rendered y coordinate. + + Args: + ty_groups: dict of text show ops as returned by y_coordinate_groups() + char_width: fixed character width + space_vertically: include blank lines inferred from y distance + font height. + font_height_weight: multiplier for font height when calculating blank lines. + + Returns: + str: page text in a fixed width format that closely adheres to the rendered + layout in the source pdf. + + """ + lines: list[str] = [] + last_y_coord = 0 + table = str.maketrans(dict.fromkeys(range(14, 32), " ")) + for y_coord, line_data in ty_groups.items(): + if space_vertically and lines: + fh = line_data[0]["font_height"] + blank_lines = 0 if fh == 0 else ( + int(abs(y_coord - last_y_coord) / (fh * font_height_weight)) - 1 + ) + if blank_lines > NEWLINE_LIMIT: + logger_warning( + "Limiting excessive newlines from %(actual)d to %(limit)d.", + actual=blank_lines, limit=NEWLINE_LIMIT, source=__name__ + ) + blank_lines = NEWLINE_LIMIT + lines.extend([""] * blank_lines) + + line_parts = [] # It uses a list to construct the line, avoiding string concatenation. + current_len = 0 # Track the size with int instead of len(str) overhead. + last_disp = 0.0 + for bt_op in line_data: + tx = bt_op["tx"] + offset = int(tx // char_width) + needed_spaces = offset - current_len + if needed_spaces > 0 and ceil(last_disp) < int(tx): + if needed_spaces > WHITESPACE_LIMIT: + logger_warning( + "Limiting excessive whitespace from %(actual)d to %(limit)d characters.", + actual=needed_spaces, limit=WHITESPACE_LIMIT, source=__name__ + ) + needed_spaces = WHITESPACE_LIMIT + padding = " " * needed_spaces + line_parts.append(padding) + current_len += needed_spaces + + raw_text = bt_op["text"] + text = raw_text.translate(table) + line_parts.append(text) + current_len += len(text) + last_disp = bt_op["displaced_tx"] + + full_line = "".join(line_parts).rstrip() + if full_line.strip() or (space_vertically and lines): + lines.append(full_line) + + last_y_coord = y_coord + + return "\n".join(lines) diff --git a/app/vendor/pypdf/_text_extraction/_layout_mode/_text_state_manager.py b/app/vendor/pypdf/_text_extraction/_layout_mode/_text_state_manager.py new file mode 100644 index 0000000..970eccd --- /dev/null +++ b/app/vendor/pypdf/_text_extraction/_layout_mode/_text_state_manager.py @@ -0,0 +1,204 @@ +"""manage the PDF transform stack during "layout" mode text extraction""" + +from collections import ChainMap, Counter +from collections import ChainMap as ChainMapType +from collections import Counter as CounterType +from collections.abc import MutableMapping +from typing import Any, Literal, Union + +from ..._font import Font +from ...errors import PdfReadError +from .. import mult +from ._text_state_params import TextStateParams + +TextStateManagerKeyType = Union[int, Literal["is_text", "is_render"]] +TextStateManagerChainMapType = ChainMapType[TextStateManagerKeyType, Union[float, bool]] +TextStateManagerDictType = MutableMapping[TextStateManagerKeyType, Union[float, bool]] + + +class TextStateManager: + """ + Tracks the current text state including cm/tm/trm transformation matrices. + + Attributes: + transform_stack (ChainMap): ChainMap of cm/tm transformation matrices + q_queue (Counter[int]): Counter of q operators + q_depth (List[int]): list of q operator nesting levels + Tc (float): character spacing + Tw (float): word spacing + Tz (int): horizontal scaling + TL (float): leading + Ts (float): text rise + font (Font): font object + font_size (int | float): font size + + """ + + def __init__(self) -> None: + self.transform_stack: TextStateManagerChainMapType = ChainMap( + self.new_transform() + ) + self.q_queue: CounterType[int] = Counter() + self.q_depth = [0] + self.Tc: float = 0.0 + self.Tw: float = 0.0 + self.Tz: float = 100.0 + self.TL: float = 0.0 + self.Ts: float = 0.0 + self.font_stack: list[tuple[Union[Font, None], Union[int, float]]] = [] + self.font: Union[Font, None] = None + self.font_size: Union[int, float] = 0 + + def set_state_param(self, op: bytes, value: Union[float, list[Any]]) -> None: + """ + Set a text state parameter. Supports Tc, Tz, Tw, TL, and Ts operators. + + Args: + op: operator read from PDF stream as bytes. No action is taken + for unsupported operators (see supported operators above). + value (float | List[Any]): new parameter value. If a list, + value[0] is used. + + """ + if op not in [b"Tc", b"Tz", b"Tw", b"TL", b"Ts"]: + return + self.__setattr__(op.decode(), value[0] if isinstance(value, list) else value) + + def set_font(self, font: Font, size: float) -> None: + """ + Set the current font and font_size. + + Args: + font (Font): a layout mode Font + size (float): font size + + """ + self.font = font + self.font_size = size + + def text_state_params(self, value: Union[bytes, str] = "") -> TextStateParams: + """ + Create a TextStateParams instance to display a text string. Type[bytes] values + will be decoded implicitly. + + Args: + value (str | bytes): text to associate with the captured state. + + Raises: + PdfReadError: if font not set (no Tf operator in incoming pdf content stream) + + Returns: + TextStateParams: current text state parameters + + """ + if not isinstance(self.font, Font): + raise PdfReadError( + "font not set: is PDF missing a Tf operator?" + ) # pragma: no cover + return TextStateParams( + value, + self.font, + self.font_size, + self.Tc, + self.Tw, + self.Tz, + self.TL, + self.Ts, + self.effective_transform, + ) + + @staticmethod + def raw_transform( + _a: float = 1.0, + _b: float = 0.0, + _c: float = 0.0, + _d: float = 1.0, + _e: float = 0.0, + _f: float = 0.0, + ) -> TextStateManagerDictType: + """Only a/b/c/d/e/f matrix params""" + return dict(zip(range(6), map(float, (_a, _b, _c, _d, _e, _f)))) + + @staticmethod + def new_transform( + _a: float = 1.0, + _b: float = 0.0, + _c: float = 0.0, + _d: float = 1.0, + _e: float = 0.0, + _f: float = 0.0, + is_text: bool = False, + is_render: bool = False, + ) -> TextStateManagerDictType: + """Standard a/b/c/d/e/f matrix params + 'is_text' and 'is_render' keys""" + result = TextStateManager.raw_transform(_a, _b, _c, _d, _e, _f) + result.update({"is_text": is_text, "is_render": is_render}) + return result + + def reset_tm(self) -> TextStateManagerChainMapType: + """Clear all transforms from chainmap having is_text==True or is_render==True""" + while ( + self.transform_stack.maps[0]["is_text"] + or self.transform_stack.maps[0]["is_render"] + ): + self.transform_stack = self.transform_stack.parents + return self.transform_stack + + def reset_trm(self) -> TextStateManagerChainMapType: + """Clear all transforms from chainmap having is_render==True""" + while self.transform_stack.maps[0]["is_render"]: + self.transform_stack = self.transform_stack.parents + return self.transform_stack + + def remove_q(self) -> TextStateManagerChainMapType: + """Rewind to stack prior state after closing a 'q' with internal 'cm' ops""" + self.font, self.font_size = self.font_stack.pop(-1) + self.transform_stack = self.reset_tm() + self.transform_stack.maps = self.transform_stack.maps[ + self.q_queue.pop(self.q_depth.pop(), 0) : + ] + return self.transform_stack + + def add_q(self) -> None: + """Add another level to q_queue""" + self.font_stack.append((self.font, self.font_size)) + self.q_depth.append(len(self.q_depth)) + + def add_cm(self, *args: Any) -> TextStateManagerChainMapType: + """Concatenate an additional transform matrix""" + self.transform_stack = self.reset_tm() + self.q_queue.update(self.q_depth[-1:]) + self.transform_stack = self.transform_stack.new_child(self.new_transform(*args)) + return self.transform_stack + + def _complete_matrix(self, operands: list[float]) -> list[float]: + """Adds a, b, c, and d to an "e/f only" operand set (e.g Td)""" + if len(operands) == 2: # this is a Td operator or equivalent + operands = [1.0, 0.0, 0.0, 1.0, *operands] + return operands + + def add_tm(self, operands: list[float]) -> TextStateManagerChainMapType: + """Append a text transform matrix""" + self.transform_stack = self.transform_stack.new_child( + self.new_transform( # type: ignore[misc] + *self._complete_matrix(operands), is_text=True # type: ignore[arg-type] + ) + ) + return self.transform_stack + + def add_trm(self, operands: list[float]) -> TextStateManagerChainMapType: + """Append a text rendering transform matrix""" + self.transform_stack = self.transform_stack.new_child( + self.new_transform( # type: ignore[misc] + *self._complete_matrix(operands), is_text=True, is_render=True # type: ignore[arg-type] + ) + ) + return self.transform_stack + + @property + def effective_transform(self) -> list[float]: + """Current effective transform accounting for cm, tm, and trm transforms""" + eff_transform = [*self.transform_stack.maps[0].values()] + for transform in self.transform_stack.maps[1:]: + eff_transform = mult(eff_transform, transform) + return eff_transform diff --git a/app/vendor/pypdf/_text_extraction/_layout_mode/_text_state_params.py b/app/vendor/pypdf/_text_extraction/_layout_mode/_text_state_params.py new file mode 100644 index 0000000..ca104af --- /dev/null +++ b/app/vendor/pypdf/_text_extraction/_layout_mode/_text_state_params.py @@ -0,0 +1,161 @@ +"""A dataclass that captures the CTM and Text State for a tj operation""" + +import math +from dataclasses import dataclass, field +from typing import Any, Union + +from ..._font import Font +from .. import mult, orient + + +@dataclass +class TextStateParams: + """ + Text state parameters and operator values for a single text value in a + TJ or Tj PDF operation. + + Attributes: + value (bytes | str): the raw text to be rendered. + font (Font): font object + font_size (int | float): font size + Tc (float): character spacing. Defaults to 0.0. + Tw (float): word spacing. Defaults to 0.0. + Tz (float): horizontal scaling. Defaults to 100.0. + TL (float): leading, vertical displacement between text lines. Defaults to 0.0. + Ts (float): text rise. Used for super/subscripts. Defaults to 0.0. + transform (List[float]): effective transformation matrix. + tx (float): x cood of rendered text, i.e. self.transform[4] + ty (float): y cood of rendered text. May differ from self.transform[5] per self.Ts. + displaced_tx (float): x coord immediately following rendered text + space_tx (float): tx for a space character + font_height (float): effective font height accounting for CTM + flip_vertical (bool): True if y axis has been inverted (i.e. if self.transform[3] < 0.) + rotated (bool): True if the text orientation is rotated with respect to the page. + + """ + + value: Union[bytes, str] + font: Font + font_size: Union[int, float] + Tc: float = 0.0 + Tw: float = 0.0 + Tz: float = 100.0 + TL: float = 0.0 + Ts: float = 0.0 + transform: list[float] = field( + default_factory=lambda: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + ) + tx: float = field(default=0.0, init=False) + ty: float = field(default=0.0, init=False) + displaced_tx: float = field(default=0.0, init=False) + space_tx: float = field(default=0.0, init=False) + font_height: float = field(default=0.0, init=False) + flip_vertical: bool = field(default=False, init=False) + rotated: bool = field(default=False, init=False) + text: str = "" + _decoded_value: str = "" + + def __post_init__(self) -> None: + if isinstance(self.value, bytes): + try: + if isinstance(self.font.encoding, str): + self._decoded_value = self.value.decode(self.font.encoding, "surrogatepass") + else: + self._decoded_value = "".join( + self.font.encoding[x] + if x in self.font.encoding + else bytes((x,)).decode() + for x in self.value + ) + except UnicodeDecodeError: + self._decoded_value = self.value.decode("utf-8", "replace") + self.text = "".join( + self.font.character_map.get(x, x) for x in self._decoded_value + ) + else: + self.text = self.value + + if orient(self.transform) in (90, 270): + self.transform = mult( + [1.0, -self.transform[1], -self.transform[2], 1.0, 0.0, 0.0], + self.transform, + ) + self.rotated = True + # self.transform[0] AND self.transform[3] < 0 indicates true rotation. + # If only self.transform[3] < 0, the y coords are simply inverted. + if orient(self.transform) == 180 and self.transform[0] < -1e-6: + self.transform = mult([-1.0, 0.0, 0.0, -1.0, 0.0, 0.0], self.transform) + self.rotated = True + self.displaced_tx = self.displaced_transform()[4] + self.tx = self.transform[4] + self.ty = self.render_transform()[5] + self.space_tx = round(self.word_tx(self.font.space_char), 3) + if self.space_tx < 1e-6: + # if the " " char is assigned 0 width (e.g. for fine tuned spacing + # with TJ int operators a la crazyones.pdf), calculate space_tx as + # a td_offset of -1 * font.space_width where font.space_width is + # the space_width calculated in _font.py. + self.space_tx = round(self.word_tx("", -self.font.space_width), 3) + self.font_height = self.font_size * math.sqrt( + self.transform[1] ** 2 + self.transform[3] ** 2 + ) + # flip_vertical handles PDFs generated by Microsoft Word's "publish" command. + self.flip_vertical = self.transform[3] < -1e-6 # inverts y axis + + def font_size_matrix(self) -> list[float]: + """Font size matrix""" + return [ + self.font_size * (self.Tz / 100.0), + 0.0, + 0.0, + self.font_size, + 0.0, + self.Ts, + ] + + def displaced_transform(self) -> list[float]: + """Effective transform matrix after text has been rendered.""" + return mult(self.displacement_matrix(), self.transform) + + def render_transform(self) -> list[float]: + """Effective transform matrix accounting for font size, Tz, and Ts.""" + return mult(self.font_size_matrix(), self.transform) + + def displacement_matrix( + self, word: Union[bytes, str, None] = None, td_offset: float = 0.0 + ) -> list[float]: + """ + Text displacement matrix + + Args: + word (bytes | str, optional): Defaults to None in which case self.text displacement is + returned. + td_offset (float, optional): translation applied by TD operator. Defaults to 0.0. + + """ + word = word if word is not None else self.value + return [1.0, 0.0, 0.0, 1.0, self.word_tx(word, td_offset), 0.0] + + def word_tx(self, word: Union[bytes, str], td_offset: float = 0.0) -> float: + """Horizontal text displacement for any word according this text state""" + width: float = 0.0 + + if isinstance(word, bytes): + word = self._decoded_value + + for char in word: + if char == self.font.space_char: + width += self.font.space_width + else: + width += self.font.get_text_width(char) + + return ( + (self.font_size * ((width - td_offset) / 1000.0)) + + len(word) * self.Tc + + word.count(self.font.space_char) * self.Tw + ) * (self.Tz / 100.0) + + @staticmethod + def to_dict(inst: "TextStateParams") -> dict[str, Any]: + """Dataclass to dict for json.dumps serialization""" + return {k: getattr(inst, k) for k in inst.__dataclass_fields__ if k != "font"} diff --git a/app/vendor/pypdf/_text_extraction/_text_extractor.py b/app/vendor/pypdf/_text_extraction/_text_extractor.py new file mode 100644 index 0000000..6f3d179 --- /dev/null +++ b/app/vendor/pypdf/_text_extraction/_text_extractor.py @@ -0,0 +1,360 @@ +# Copyright (c) 2006, Mathieu Fenniak +# Copyright (c) 2007, Ashish Kulkarni +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from typing import Any, Callable, Optional, Union + +from .._codecs import encoding_dict_from_named_encoding +from .._font import Font, FontDescriptor +from ..generic import DictionaryObject, TextStringObject +from . import OrientationNotFoundError, crlf_space_check, get_display_str, get_text_operands, mult + + +class TextExtraction: + """ + A class to handle PDF text extraction operations. + + This class encapsulates all the state and operations needed for extracting + text from PDF content streams, replacing the nested functions and nonlocal + variables in the original implementation. + """ + + def __init__(self) -> None: + # Text extraction state variables + self.cm_matrix: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + self.tm_matrix: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + self.cm_stack: list[ + tuple[ + list[float], + Optional[DictionaryObject], + Font, + float, + float, + float, + float, + ] + ] = [] + + # Store the last modified matrices; can be an intermediate position + self.cm_prev: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + self.tm_prev: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + + # Store the position at the beginning of building the text + self.memo_cm: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + self.memo_tm: list[float] = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + + self.char_scale = 1.0 + self.space_scale = 1.0 + self._space_width: float = 500.0 # will be set correctly at first Tf + self._actual_str_size: dict[str, float] = { + "str_widths": 0.0, + "str_height": 0.0, + } # will be set to string length calculation result + self.TL = 0.0 + self.font_size = 12.0 # init just in case of + + # Text extraction variables + self.text: str = "" + self.output: str = "" + self.rtl_dir: bool = False # right-to-left + self.font_resource: Optional[DictionaryObject] = None + self.font = Font( + name = "NotInitialized", + sub_type="Unknown", + encoding=encoding_dict_from_named_encoding("cp1252"), # WinAnsiEncoding + font_descriptor=FontDescriptor(), + ) + self.orientations: tuple[int, ...] = (0, 90, 180, 270) + self.visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None + self.font_resources: dict[str, DictionaryObject] = {} + self.fonts: dict[str, Font] = {} + + self.operation_handlers = { + b"BT": self._handle_bt, + b"ET": self._handle_et, + b"q": self._handle_save_graphics_state, + b"Q": self._handle_restore_graphics_state, + b"cm": self._handle_cm, + b"Tz": self._handle_tz, + b"Tw": self._handle_tw, + b"TL": self._handle_tl, + b"Tf": self._handle_tf, + b"Td": self._handle_td, + b"Tm": self._handle_tm, + b"T*": self._handle_t_star, + b"Tj": self._handle_tj_operation, + } + + def initialize_extraction( + self, + orientations: tuple[int, ...] = (0, 90, 180, 270), + visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]] = None, + font_resources: Optional[dict[str, DictionaryObject]] = None, + fonts: Optional[dict[str, Font]] = None + ) -> None: + """Initialize the extractor with extraction parameters.""" + self.orientations = orientations + self.visitor_text = visitor_text + self.font_resources = font_resources or {} + self.fonts = fonts or {} + + # Reset state + self.text = "" + self.output = "" + self.rtl_dir = False + + def compute_str_widths(self, str_widths: float) -> float: + return str_widths / 1000 + + def process_operation(self, operator: bytes, operands: list[Any]) -> None: + if operator in self.operation_handlers: + handler = self.operation_handlers[operator] + str_widths = handler(operands) + + # Post-process operations that affect text positioning + if operator in {b"Td", b"Tm", b"T*", b"Tj"}: + self._post_process_text_operation(str_widths or 0.0) + + def _post_process_text_operation(self, str_widths: float) -> None: + """Handle common post-processing for text positioning operations.""" + text_was_empty = self.text == "" + try: + self.text, self.output, self.cm_prev, self.tm_prev = crlf_space_check( + self.text, + (self.cm_prev, self.tm_prev), + (self.cm_matrix, self.tm_matrix), + (self.memo_cm, self.memo_tm), + self.font_resource, + self.orientations, + self.output, + self.font_size, + self.visitor_text, + str_widths, + self.compute_str_widths(self.font_size * self._space_width), + self._actual_str_size["str_height"], + ) + if text_was_empty or self.text == "": + self.memo_cm = self.cm_matrix.copy() + self.memo_tm = self.tm_matrix.copy() + except OrientationNotFoundError: + pass + + def _handle_tj( + self, + text: str, + operands: list[Union[str, TextStringObject]], + cm_matrix: list[float], + tm_matrix: list[float], + font_resource: Optional[DictionaryObject], + font: Font, + orientations: tuple[int, ...], + font_size: float, + rtl_dir: bool, + visitor_text: Optional[Callable[[Any, Any, Any, Any, Any], None]], + actual_str_size: dict[str, float], + ) -> tuple[str, bool, dict[str, float]]: + text_operands, is_str_operands = get_text_operands( + operands, cm_matrix, tm_matrix, font, orientations + ) + if is_str_operands: + text += text_operands + font_widths = sum( + [font.space_width if x == font.space_char else font.get_text_width(x) for x in text_operands] + ) + else: + text, rtl_dir, font_widths = get_display_str( + text, + cm_matrix, + tm_matrix, # text matrix + font_resource, + font, + text_operands, + font_size, + rtl_dir, + visitor_text, + ) + actual_str_size["str_widths"] += font_widths * font_size + actual_str_size["str_height"] = font_size + return text, rtl_dir, actual_str_size + + def _flush_text(self) -> None: + """Flush accumulated text to output and call visitor if present.""" + self.output += self.text + if self.visitor_text is not None: + self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size) + self.text = "" + self.memo_cm = self.cm_matrix.copy() + self.memo_tm = self.tm_matrix.copy() + + # Operation handlers + + def _handle_bt(self, operands: list[Any]) -> None: + """Handle BT (Begin Text) operation - Table 5.4 page 405.""" + self.tm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + self._flush_text() + + def _handle_et(self, operands: list[Any]) -> None: + """Handle ET (End Text) operation - Table 5.4 page 405.""" + self._flush_text() + + def _handle_save_graphics_state(self, operands: list[Any]) -> None: + """Handle q (Save graphics state) operation - Table 4.7 page 219.""" + self.cm_stack.append( + ( + self.cm_matrix, + self.font_resource, + self.font, + self.font_size, + self.char_scale, + self.space_scale, + self.TL, + ) + ) + + def _handle_restore_graphics_state(self, operands: list[Any]) -> None: + """Handle Q (Restore graphics state) operation - Table 4.7 page 219.""" + try: + ( + self.cm_matrix, + self.font_resource, + self.font, + self.font_size, + self.char_scale, + self.space_scale, + self.TL, + ) = self.cm_stack.pop() + except Exception: + self.cm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + + def _handle_cm(self, operands: list[Any]) -> None: + """Handle cm (Modify current matrix) operation - Table 4.7 page 219.""" + self.output += self.text + if self.visitor_text is not None: + self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size) + self.text = "" + try: + self.cm_matrix = mult([float(operand) for operand in operands[:6]], self.cm_matrix) + except Exception: + self.cm_matrix = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + self.memo_cm = self.cm_matrix.copy() + self.memo_tm = self.tm_matrix.copy() + + def _handle_tz(self, operands: list[Any]) -> None: + """Handle Tz (Set horizontal text scaling) operation - Table 5.2 page 398.""" + self.char_scale = float(operands[0]) / 100 if operands else 1.0 + + def _handle_tw(self, operands: list[Any]) -> None: + """Handle Tw (Set word spacing) operation - Table 5.2 page 398.""" + self.space_scale = 1.0 + float(operands[0] if operands else 0.0) + + def _handle_tl(self, operands: list[Any]) -> None: + """Handle TL (Set Text Leading) operation - Table 5.2 page 398.""" + # The leading is measured in unscaled text space units (PDF 32000-1, 9.3.5) + # and T* applies the text matrix to it, so it must not be scaled here. + self.TL = float(operands[0] if operands else 0.0) + + def _handle_tf(self, operands: list[Any]) -> None: + """Handle Tf (Set font size) operation - Table 5.2 page 398.""" + if self.text != "": + self.output += self.text # .translate(cmap) + if self.visitor_text is not None: + self.visitor_text(self.text, self.memo_cm, self.memo_tm, self.font_resource, self.font_size) + self.text = "" + self.memo_cm = self.cm_matrix.copy() + self.memo_tm = self.tm_matrix.copy() + try: + self.font_resource = self.font_resources[operands[0]] + self.font = self.fonts[operands[0]] + except (KeyError, IndexError): # font not found / operand missing + self.font_resource = None + font_descriptor = FontDescriptor() + self.font = Font( + "Unknown", + space_width=250, + encoding=dict.fromkeys(range(256), "�"), + font_descriptor=font_descriptor, + character_map={}, + ) + + self._space_width = self.font.space_width / 2 # Actually the width of _half_ a space... + try: + self.font_size = float(operands[1]) + except Exception: + pass # keep previous size + + def _handle_td(self, operands: list[Any]) -> float: + """Handle Td (Move text position) operation - Table 5.5 page 406.""" + # A special case is a translating only tm: + # tm = [1, 0, 0, 1, e, f] + # i.e. tm[4] += tx, tm[5] += ty. + tx = float(operands[0]) if len(operands) > 0 else 0.0 + ty = float(operands[1]) if len(operands) > 1 else 0.0 + self.tm_matrix[4] += tx * self.tm_matrix[0] + ty * self.tm_matrix[2] + self.tm_matrix[5] += tx * self.tm_matrix[1] + ty * self.tm_matrix[3] + str_widths = self.compute_str_widths(self._actual_str_size["str_widths"]) + self._actual_str_size["str_widths"] = 0.0 + return str_widths + + def _handle_tm(self, operands: list[Any]) -> float: + """Handle Tm (Set text matrix) operation - Table 5.5 page 406.""" + try: + tm_matrix = [float(operand) for operand in operands[:6]] + except (TypeError, ValueError): + tm_matrix = [] + # Fall back to the identity matrix when Tm carries the wrong number of + # operands, mirroring _handle_cm, so the text matrix stays a six-element + # list and later positioning operators do not read past its end. + self.tm_matrix = tm_matrix if len(tm_matrix) == 6 else [1.0, 0.0, 0.0, 1.0, 0.0, 0.0] + str_widths = self.compute_str_widths(self._actual_str_size["str_widths"]) + self._actual_str_size["str_widths"] = 0.0 + return str_widths + + def _handle_t_star(self, operands: list[Any]) -> float: + """Handle T* (Move to next line) operation - Table 5.5 page 406.""" + self.tm_matrix[4] -= self.TL * self.tm_matrix[2] + self.tm_matrix[5] -= self.TL * self.tm_matrix[3] + str_widths = self.compute_str_widths(self._actual_str_size["str_widths"]) + self._actual_str_size["str_widths"] = 0.0 + return str_widths + + def _handle_tj_operation(self, operands: list[Any]) -> float: + """Handle Tj (Show text) operation - Table 5.5 page 406.""" + self.text, self.rtl_dir, self._actual_str_size = self._handle_tj( + self.text, + operands, + self.cm_matrix, + self.tm_matrix, + self.font_resource, + self.font, + self.orientations, + self.font_size, + self.rtl_dir, + self.visitor_text, + self._actual_str_size, + ) + return 0.0 # str_widths will be handled in post-processing diff --git a/app/vendor/pypdf/_utils.py b/app/vendor/pypdf/_utils.py new file mode 100644 index 0000000..5e41f8d --- /dev/null +++ b/app/vendor/pypdf/_utils.py @@ -0,0 +1,690 @@ +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Utility functions for PDF library.""" +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + +import functools +import logging +import re +import sys +import warnings +from dataclasses import dataclass +from datetime import datetime, timezone +from io import DEFAULT_BUFFER_SIZE +from os import SEEK_CUR +from re import Pattern +from typing import ( + IO, + Any, + NoReturn, + Optional, + Union, +) + +if sys.version_info[:2] >= (3, 10): + # Python 3.10+: https://www.python.org/dev/peps/pep-0484/ + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +from .errors import ( + STREAM_TRUNCATED_PREMATURELY, + DeprecationError, + LimitReachedError, + PdfStreamError, +) + +TransformationMatrixType: TypeAlias = tuple[ + tuple[float, float, float], tuple[float, float, float], tuple[float, float, float] +] +CompressedTransformationMatrix: TypeAlias = tuple[ + float, float, float, float, float, float +] + +StreamType = IO[Any] +BinaryStreamType = IO[bytes] +StrByteType = Union[str, StreamType] + + +def parse_iso8824_date(text: Optional[str]) -> Optional[datetime]: + orgtext = text + if not text: + return None + if text[0].isdigit(): + text = "D:" + text + if text.endswith(("Z", "z")): + text += "0000" + text = text.replace("z", "+").replace("Z", "+").replace("'", "") + i = max(text.find("+"), text.find("-")) + if i > 0 and i != len(text) - 5: + text += "00" + for f in ( + "D:%Y", + "D:%Y%m", + "D:%Y%m%d", + "D:%Y%m%d%H", + "D:%Y%m%d%H%M", + "D:%Y%m%d%H%M%S", + "D:%Y%m%d%H%M%S%z", + ): + try: + d = datetime.strptime(text, f) # noqa: DTZ007 + except ValueError: + continue + else: + if text.endswith("+0000"): + d = d.replace(tzinfo=timezone.utc) + return d + raise ValueError(f"Can not convert date: {orgtext}") + + +def format_iso8824_date(dt: datetime) -> str: + """ + Convert a datetime object to PDF date string format. + + Converts datetime to the PDF date format D:YYYYMMDDHHmmSSOHH'mm + as specified in the PDF Reference. + + Args: + dt: A datetime object to convert. + + Returns: + A date string in PDF format. + """ + date_str = dt.strftime("D:%Y%m%d%H%M%S") + if dt.tzinfo is not None: + offset = dt.utcoffset() + assert offset is not None + total_seconds = int(offset.total_seconds()) + hours, remainder = divmod(abs(total_seconds), 3600) + minutes = remainder // 60 + sign = "+" if total_seconds >= 0 else "-" + date_str += f"{sign}{hours:02d}'{minutes:02d}'" + return date_str + + +def _get_max_pdf_version_header(header1: str, header2: str) -> str: + versions = ( + "%PDF-1.3", + "%PDF-1.4", + "%PDF-1.5", + "%PDF-1.6", + "%PDF-1.7", + "%PDF-2.0", + ) + pdf_header_indices = [] + if header1 in versions: + pdf_header_indices.append(versions.index(header1)) + if header2 in versions: + pdf_header_indices.append(versions.index(header2)) + if len(pdf_header_indices) == 0: + raise ValueError(f"Neither {header1!r} nor {header2!r} are proper headers") + return versions[max(pdf_header_indices)] + + +WHITESPACES = (b"\x00", b"\t", b"\n", b"\f", b"\r", b" ") +WHITESPACES_AS_BYTES = b"".join(WHITESPACES) +WHITESPACES_AS_REGEXP = b"[" + WHITESPACES_AS_BYTES + b"]" + + +def read_until_whitespace(stream: StreamType, max_bytes: Optional[int] = None, strict: bool = False) -> bytes: + """ + Read non-whitespace characters and return them. + + Stops upon encountering whitespace or when max_bytes is reached. + + Args: + stream: The data stream from which was read. + max_bytes: The maximum number of bytes which are considered valid. One more byte + will always be read from the stream. + strict: Whether to raise an exception if the token exceeds max_bytes. + If False, a warning is logged and only the first max_bytes are returned. + + Returns: + The data which was read. + + """ + txt = bytearray() + while True: + tok = stream.read(1) + if tok.isspace() or tok in WHITESPACES or not tok: + break + if max_bytes is not None and len(txt) >= max_bytes: + if strict: + raise LimitReachedError(f"Token exceeds maximum length of {max_bytes} bytes.") + logger_warning( + "Token exceeds maximum length of %(max_bytes)d bytes.", source=__name__, max_bytes=max_bytes + ) + break + txt += tok + return bytes(txt) + + +def read_non_whitespace(stream: BinaryStreamType) -> bytes: + """ + Find and read the next non-whitespace character (ignores whitespace). + + Args: + stream: The data stream from which was read. + + Returns: + The data which was read. + + """ + tok = stream.read(1) + while tok in WHITESPACES: + tok = stream.read(1) + return tok + + +def skip_over_whitespace(stream: StreamType) -> bool: + """ + Similar to read_non_whitespace, but return a boolean if at least one + whitespace character was read. + + Args: + stream: The data stream from which was read. + + Returns: + True if one or more whitespace was skipped, otherwise return False. + + """ + tok = stream.read(1) + cnt = 0 + while tok in WHITESPACES: + cnt += 1 + tok = stream.read(1) + return cnt > 0 + + +def check_if_whitespace_only(value: bytes) -> bool: + """ + Check if the given value consists of whitespace characters only. + + Args: + value: The bytes to check. + + Returns: + True if the value only has whitespace characters, otherwise return False. + + """ + return all(b in WHITESPACES_AS_BYTES for b in value) + + +NEUTRAL_CHARACTER_RANGES = ( + ("\x00", "\x2F"), # ASCII control codes, space, and early punctuation (!"#$%) + ("\x3A", "\x40"), # ASCII operators and punctuation between digits & A (:;<=>?@) + ("\u0660", "\u066D"), # Arabic-Indic digits and Arabic numeric separators + ("\u06F0", "\u06F9"), # Extended Arabic-Indic (Persian) digits + ("\u2000", "\u206F"), # General punctuation + ("\u20A0", "\u21FF"), # Currency symbols, diacritical marks, letter-like symbols, number forms, arrows +) + + +def is_char_neutral(char: str, custom_special_characters: str = "") -> bool: + """Check if a character is part of neutral character ranges""" + if any(start <= char <= end for start, end in NEUTRAL_CHARACTER_RANGES): + return True + + return bool(custom_special_characters and char in custom_special_characters) + + +RTL_CHARACTER_RANGES = ( + ("\u0590", "\u08FF"), # Hebrew, Arabic, Syriac, Thaana, N'Ko, etc. + ("\uFB1D", "\uFDFF"), # Hebrew & Arabic Presentation Forms-A + ("\uFE70", "\uFEFF"), # Arabic Presentation Forms-B +) + + +def is_char_rtl(char: str, custom_rtl_min: str = "", custom_rtl_max: str = "") -> bool: + """Check if a character is part of RTL character ranges""" + if any(start <= char <= end for start, end in RTL_CHARACTER_RANGES): + return True + + return bool(custom_rtl_min and custom_rtl_max and (custom_rtl_min <= char <= custom_rtl_max)) + + +def skip_over_comment(stream: StreamType) -> None: + tok = stream.read(1) + stream.seek(-1, 1) + if tok == b"%": + while tok not in (b"\n", b"\r"): + tok = stream.read(1) + if tok == b"": + raise PdfStreamError("File ended unexpectedly.") + + +def read_until_regex(*, stream: StreamType, regex: Pattern[bytes], length: int = sys.maxsize) -> bytes: + """ + Read until the regular expression pattern matched (ignore the match). + Treats EOF on the underlying stream as the end of the token to be matched. + + Args: + stream: The stream to read from. + regex: The pattern to search for. + length: The (approximated) maximum number of bytes to read before raising an exception. + + Returns: + The read bytes. + + """ + parts: list[bytes] = [] + total_length = 0 + tail = b"" + chunk_size = 16 + while True: + token = stream.read(chunk_size) + if not token: + return b"".join(parts) + token_length = len(token) + if (current_length := total_length + token_length) >= length: + raise LimitReachedError( + f"Read stream length of {current_length} exceeds maximum allowed length of {length}." + ) + + # Search overlap of previous tail + new chunk to catch + # multi-byte regex matches spanning chunk boundaries. + current_buffer = tail + token + search_match = regex.search(current_buffer) + parts.append(token) + if search_match is not None: + overlap = len(tail) + actual_start = total_length - overlap + search_match.start() + stream.seek(actual_start - total_length - token_length, 1) + return b"".join(parts)[:actual_start] + total_length += token_length + + # Fixed overlap: 16 bytes is sufficient for the short + # delimiter patterns used in PDF parsing. + tail = token[-16:] + if chunk_size < 8192: + chunk_size <<= 1 + + +def read_block_backwards(stream: BinaryStreamType, to_read: int) -> bytes: + """ + Given a stream at position X, read a block of size to_read ending at position X. + + This changes the stream's position to the beginning of where the block was + read. + + Args: + stream: + to_read: + + Returns: + The data which was read. + + """ + if stream.tell() < to_read: + raise PdfStreamError("Could not read malformed PDF file") + # Seek to the start of the block we want to read. + stream.seek(-to_read, SEEK_CUR) + read = stream.read(to_read) + # Seek to the start of the block we read after reading it. + stream.seek(-to_read, SEEK_CUR) + return read + + +def read_previous_line(stream: StreamType) -> bytes: + """ + Given a byte stream with current position X, return the previous line. + + All characters between the first CR/LF byte found before X + (or, the start of the file, if no such byte is found) and position X + After this call, the stream will be positioned one byte after the + first non-CRLF character found beyond the first CR/LF byte before X, + or, if no such byte is found, at the beginning of the stream. + + Args: + stream: StreamType: + + Returns: + The data which was read. + + """ + line_content = [] + found_crlf = False + if stream.tell() == 0: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + while True: + to_read = min(DEFAULT_BUFFER_SIZE, stream.tell()) + if to_read == 0: + break + # Read the block. After this, our stream will be one + # beyond the initial position. + block = read_block_backwards(stream, to_read) + idx = len(block) - 1 + if not found_crlf: + # We haven't found our first CR/LF yet. + # Read off characters until we hit one. + while idx >= 0 and block[idx] not in b"\r\n": + idx -= 1 + if idx >= 0: + found_crlf = True + if found_crlf: + # We found our first CR/LF already (on this block or + # a previous one). + # Our combined line is the remainder of the block + # plus any previously read blocks. + line_content.append(block[idx + 1 :]) + # Continue to read off any more CRLF characters. + while idx >= 0 and block[idx] in b"\r\n": + idx -= 1 + else: + # Didn't find CR/LF yet - add this block to our + # previously read blocks and continue. + line_content.append(block) + if idx >= 0: + # We found the next non-CRLF character. + # Set the stream position correctly, then break + stream.seek(idx + 1, SEEK_CUR) + break + # Join all the blocks in the line (which are in reverse order) + return b"".join(line_content[::-1]) + + +def matrix_multiply( + a: TransformationMatrixType, b: TransformationMatrixType +) -> TransformationMatrixType: + return tuple( # type: ignore[return-value] + tuple(sum(float(i) * float(j) for i, j in zip(row, col)) for col in zip(*b)) + for row in a + ) + + +def mark_location(stream: StreamType) -> None: + """Create text file showing current location in context.""" + # Mainly for debugging + radius = 5000 + stream.seek(-radius, 1) + with open("pypdf_pdfLocation.txt", "wb") as output_fh: + output_fh.write(stream.read(radius)) + output_fh.write(b"HERE") + output_fh.write(stream.read(radius)) + stream.seek(-radius, 1) + + +def deprecate(msg: str, stacklevel: int = 3) -> None: + warnings.warn(msg, DeprecationWarning, stacklevel=stacklevel) + + +def deprecation(msg: str) -> NoReturn: + raise DeprecationError(msg) + + +def deprecate_with_replacement(old_name: str, new_name: str, removed_in: str) -> None: + """Issue a warning that a feature will be removed, but has a replacement.""" + deprecate( + f"{old_name} is deprecated and will be removed in pypdf {removed_in}. Use {new_name} instead.", + 4, + ) + + +def deprecation_with_replacement(old_name: str, new_name: str, removed_in: str) -> NoReturn: + """Raise an exception that a feature was already removed, but has a replacement.""" + deprecation( + f"{old_name} is deprecated and was removed in pypdf {removed_in}. Use {new_name} instead." + ) + + +def deprecate_no_replacement(name: str, removed_in: str) -> None: + """Issue a warning that a feature will be removed without replacement.""" + deprecate(f"{name} is deprecated and will be removed in pypdf {removed_in}.", 4) + + +def deprecation_no_replacement(name: str, removed_in: str) -> NoReturn: + """Raise an exception that a feature was already removed without replacement.""" + deprecation(f"{name} is deprecated and was removed in pypdf {removed_in}.") + + +def logger_error(message: str, *, source: str, **values: Any) -> None: + """ + Use this instead of logger.error directly. + + That allows people to overwrite it more easily. + + See the docs on when to use which: + https://pypdf.readthedocs.io/en/latest/user/suppress-warnings.html + """ + if values: + logging.getLogger(source).error(message, values) + else: + logging.getLogger(source).error(message) + + +def logger_warning(message: str, *, source: str, **values: Any) -> None: + """ + Use this instead of logger.warning directly. + + That allows people to overwrite it more easily. + + ## Exception, warnings.warn, logger_warning + - Exceptions should be used if the user should write code that deals with + an error case, e.g. the PDF being completely broken. + - warnings.warn should be used if the user needs to fix their code, e.g. + DeprecationWarnings + - logger_warning should be used if the user needs to know that an issue was + handled by pypdf, e.g. a non-compliant PDF being read in a way that + pypdf could apply a robustness fix to still read it. This applies mainly + to strict=False mode. + """ + if values: + logging.getLogger(source).warning(message, values) + else: + # Keep parity with logger_error and support plain warning messages. + # Passing an empty dict to logging is not equivalent to passing no args: + # plain messages would fail while being formatted. + logging.getLogger(source).warning(message) + + +def rename_kwargs( + func_name: str, kwargs: dict[str, Any], aliases: dict[str, str], fail: bool = False +) -> None: + """ + Helper function to deprecate arguments. + + Args: + func_name: Name of the function to be deprecated + kwargs: + aliases: + fail: + + """ + for old_term, new_term in aliases.items(): + if old_term in kwargs: + if fail: + raise DeprecationError( + f"{old_term} is deprecated as an argument. Use {new_term} instead" + ) + if new_term in kwargs: + raise TypeError( + f"{func_name} received both {old_term} and {new_term} as " + f"an argument. {old_term} is deprecated. " + f"Use {new_term} instead." + ) + kwargs[new_term] = kwargs.pop(old_term) + warnings.warn( + message=( + f"{old_term} is deprecated as an argument. Use {new_term} instead" + ), + category=DeprecationWarning, + stacklevel=3, + ) + + +def _human_readable_bytes(bytes: int) -> str: + if bytes < 10**3: + return f"{bytes} Byte" + if bytes < 10**6: + return f"{bytes / 10**3:.1f} kB" + if bytes < 10**9: + return f"{bytes / 10**6:.1f} MB" + return f"{bytes / 10**9:.1f} GB" + + +# The following class has been copied from Django: +# https://github.com/django/django/blob/adae619426b6f50046b3daaa744db52989c9d6db/django/utils/functional.py#L51-L65 +# It received some modifications to comply with our own coding standards. +# +# Original license: +# +# --------------------------------------------------------------------------------- +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without modification, +# are permitted provided that the following conditions are met: +# +# 1. Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of Django nor the names of its contributors may be used +# to endorse or promote products derived from this software without +# specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +# --------------------------------------------------------------------------------- +class classproperty: # noqa: N801 + """ + Decorator that converts a method with a single cls argument into a property + that can be accessed directly from the class. + """ + + def __init__(self, method=None) -> None: # type: ignore # noqa: ANN001 + self.fget = method + + def __get__(self, instance, cls=None) -> Any: # type: ignore # noqa: ANN001 + return self.fget(cls) + + def getter(self, method) -> Self: # type: ignore # noqa: ANN001 + self.fget = method + return self + + +@dataclass +class File: + from .generic import IndirectObject # noqa: PLC0415 + + name: str = "" + """ + Filename as identified within the PDF file. + """ + data: bytes = b"" + """ + Data as bytes. + """ + indirect_reference: Optional[IndirectObject] = None + """ + Reference to the object storing the stream. + """ + + def __str__(self) -> str: + return f"{self.__class__.__name__}(name={self.name}, data: {_human_readable_bytes(len(self.data))})" + + def __repr__(self) -> str: + return self.__str__()[:-1] + f", hash: {hash(self.data)})" + + +@functools.total_ordering +class Version: + COMPONENT_PATTERN = re.compile(r"^(\d+)(.*)$") + + def __init__(self, version_str: str) -> None: + self.version_str = version_str + self.components = self._parse_version(version_str) + + def _parse_version(self, version_str: str) -> list[tuple[int, str]]: + components = version_str.split(".") + parsed_components = [] + for component in components: + match = Version.COMPONENT_PATTERN.match(component) + if not match: + parsed_components.append((0, component)) + continue + integer_prefix = match.group(1) + suffix = match.group(2) + if integer_prefix is None: + integer_prefix = 0 + parsed_components.append((int(integer_prefix), suffix)) + return parsed_components + + def __eq__(self, other: object) -> bool: + if not isinstance(other, Version): + return False + return self.components == other.components + + def __hash__(self) -> int: + # Convert to tuple as lists cannot be hashed. + return hash((self.__class__, tuple(self.components))) + + def __lt__(self, other: Any) -> bool: + if not isinstance(other, Version): + raise ValueError(f"Version cannot be compared against {type(other)}") + + for self_component, other_component in zip(self.components, other.components): + self_value, self_suffix = self_component + other_value, other_suffix = other_component + + if self_value < other_value: + return True + if self_value > other_value: + return False + + if self_suffix < other_suffix: + return True + if self_suffix > other_suffix: + return False + + return len(self.components) < len(other.components) + + +@dataclass +class _TraversalState: + """Sometimes we need mutable objects which just count something.""" + entry_count: int = 0 + has_logged: bool = False diff --git a/app/vendor/pypdf/_version.py b/app/vendor/pypdf/_version.py new file mode 100644 index 0000000..fa7b9e6 --- /dev/null +++ b/app/vendor/pypdf/_version.py @@ -0,0 +1 @@ +__version__ = "6.19.0" diff --git a/app/vendor/pypdf/_writer.py b/app/vendor/pypdf/_writer.py new file mode 100644 index 0000000..128dea1 --- /dev/null +++ b/app/vendor/pypdf/_writer.py @@ -0,0 +1,3520 @@ +# Copyright (c) 2006, Mathieu Fenniak +# Copyright (c) 2007, Ashish Kulkarni +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import decimal +import enum +import hashlib +import re +import struct +import sys +from collections.abc import Iterable, Mapping, Sequence +from io import BytesIO, FileIO, IOBase +from itertools import compress +from pathlib import Path +from re import Pattern +from types import TracebackType +from typing import ( + IO, + Any, + Callable, + Literal, + Optional, + Union, + cast, +) + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +from ._doc_common import DocumentInformation, PdfDocCommon +from ._encryption import EncryptAlgorithm, Encryption +from ._page import PageObject, Transformation +from ._page_labels import nums_clear_range, nums_insert, nums_next +from ._reader import PdfReader +from ._utils import ( + StrByteType, + StreamType, + _get_max_pdf_version_header, + deprecate_with_replacement, + deprecation_no_replacement, + logger_warning, +) +from .actions import Action, JavaScript +from .constants import AnnotationDictionaryAttributes as AA +from .constants import ( + CatalogAttributes, + Core, + GoToActionArguments, + ImageType, + InteractiveFormDictEntries, + OutlineFontFlag, + PageLabelStyle, + PagesAttributes, + TypFitArguments, + UserAccessPermissions, +) +from .constants import FieldDictionaryAttributes as FA +from .constants import PageAttributes as PG +from .constants import TrailerKeys as TK +from .errors import LimitReachedError, PdfReadError, PyPdfError +from .generic import ( + PAGE_FIT, + ArrayObject, + BooleanObject, + ByteStringObject, + ContentStream, + Destination, + DictionaryObject, + EmbeddedFile, + Fit, + FloatObject, + IndirectObject, + NameObject, + NullObject, + NumberObject, + PdfObject, + RectangleObject, + ReferenceLink, + StreamObject, + TextStringObject, + TreeObject, + ViewerPreferences, + create_string_object, + extract_links, + hex_to_rgb, + is_null_or_none, +) +from .generic._appearance_stream import TextStreamAppearance +from .pagerange import PageRange, PageRangeSpec +from .types import ( + AnnotationSubtype, + BorderArrayType, + LayoutType, + OutlineItemType, + OutlineType, + PagemodeType, +) +from .xmp import XmpInformation + +ALL_DOCUMENT_PERMISSIONS = UserAccessPermissions.all() + + +class ObjectDeletionFlag(enum.IntFlag): + NONE = 0 + TEXT = enum.auto() + LINKS = enum.auto() + ATTACHMENTS = enum.auto() + OBJECTS_3D = enum.auto() + ALL_ANNOTATIONS = enum.auto() + XOBJECT_IMAGES = enum.auto() + INLINE_IMAGES = enum.auto() + DRAWING_IMAGES = enum.auto() + IMAGES = XOBJECT_IMAGES | INLINE_IMAGES | DRAWING_IMAGES + + +def _rolling_checksum(stream: BytesIO, blocksize: int = 65536) -> str: + hash = hashlib.md5(usedforsecurity=False) + for block in iter(lambda: stream.read(blocksize), b""): + hash.update(block) + return hash.hexdigest() + + +class PdfWriter(PdfDocCommon): + """ + Write a PDF file out, given pages produced by another class or through + cloning a PDF file during initialization. + + Typically data is added from a :class:`PdfReader`. + + Args: + clone_from: identical to fileobj (for compatibility) + + incremental: If true, loads the document and set the PdfWriter in incremental mode. + + When writing incrementally, the original document is written first and new/modified + content is appended. To be used for signed document/forms to keep signature valid. + + full: If true, loads all the objects (always full if incremental = True). + This parameter may allow loading large PDFs. + + strict: If true, pypdf will raise an exception if a PDF does not follow the specification. + If false, pypdf will try to be forgiving and do something reasonable, but it will log + a warning message. It is a best-effort approach. + + keep_initial_header: If true, the PDF header of the cloned document is kept + when using ``clone_from`` in non-incremental mode. + + """ + + def __init__( + self, + fileobj: Union[PdfReader, StrByteType, Path, None] = "", + clone_from: Union[PdfReader, StrByteType, Path, None] = None, + incremental: bool = False, + full: bool = False, + strict: bool = False, + *, + keep_initial_header: bool = False, + incremental_clone_object_count_limit: Optional[int] = 500_000, + incremental_clone_object_id_limit: Optional[int] = 1_000_000, + ) -> None: + self.strict = strict + """ + If true, pypdf will raise an exception if a PDF does not follow the specification. + If false, pypdf will try to be forgiving and do something reasonable, but it will log + a warning message. It is a best-effort approach. + """ + + self.incremental = incremental or full + """ + Returns if the PdfWriter object has been started in incremental mode. + """ + + self._objects: list[Optional[PdfObject]] = [] + """ + The indirect objects in the PDF. + For the incremental case, it will be filled with None + in clone_reader_document_root. + """ + + self._original_hash: list[int] = [] + """ + List of hashes after import; used to identify changes. + """ + + self._idnum_hash: dict[bytes, tuple[IndirectObject, list[IndirectObject]]] = {} + """ + Maps hash values of indirect objects to the list of IndirectObjects. + This is used for compression. + """ + + self._id_translated: dict[int, dict[Union[int, Literal["PreventGC"]], Any]] = {} + """List of already translated IDs. + dict[id(pdf)][(idnum, generation)] + """ + + self._info_obj: Optional[PdfObject] + """The PDF files's document information dictionary, + defined by Info in the PDF file's trailer dictionary.""" + + self._reader: Optional[PdfReader] = None + """The document being appended to, in incremental mode only.""" + + self._ID: Union[ArrayObject, None] = None + """The PDF file identifier, + defined by the ID in the PDF file's trailer dictionary.""" + + self._unresolved_links: list[tuple[ReferenceLink, ReferenceLink]] = [] + "Tracks links in pages added to the writer for resolving later." + self._merged_in_pages: dict[Optional[IndirectObject], Optional[IndirectObject]] = {} + "Tracks pages added to the writer and what page they turned into." + + # Security parameters. + self._incremental_clone_object_count_limit = ( + incremental_clone_object_count_limit + if isinstance(incremental_clone_object_count_limit, int) + else sys.maxsize + ) + self._incremental_clone_object_id_limit = ( + incremental_clone_object_id_limit if isinstance(incremental_clone_object_id_limit, int) else sys.maxsize + ) + + if self.incremental: + if isinstance(fileobj, (str, Path)): + with open(fileobj, "rb") as f: + fileobj = BytesIO(f.read(-1)) + if isinstance(fileobj, BytesIO): + fileobj = PdfReader(fileobj) + if not isinstance(fileobj, PdfReader): + raise PyPdfError("Invalid type for incremental mode") + self._reader = fileobj # prev content is in _reader.stream + self._header = fileobj.pdf_header.encode() + self._readonly = True # TODO: to be analysed + else: + self._header = b"%PDF-1.3" + self._info_obj = self._add_object( + DictionaryObject( + {NameObject("/Producer"): create_string_object("pypdf")} + ) + ) + + def _get_clone_from( + fileobj: Union[PdfReader, str, Path, IO[Any], BytesIO, None], + clone_from: Union[PdfReader, str, Path, IO[Any], BytesIO, None], + ) -> Union[PdfReader, str, Path, IO[Any], BytesIO, None]: + if isinstance(fileobj, (str, Path, IO, BytesIO)) and ( + fileobj == "" or clone_from is not None + ): + return clone_from + cloning = True + if isinstance(fileobj, (str, Path)): + fileobj_path = Path(fileobj) + if not fileobj_path.exists() or fileobj_path.stat().st_size == 0: + cloning = False + elif isinstance(fileobj, (IOBase, BytesIO)): + t = fileobj.tell() + if fileobj.seek(0, 2) == 0: + cloning = False + fileobj.seek(t, 0) + if cloning: + clone_from = fileobj + return clone_from + + clone_from = _get_clone_from(fileobj, clone_from) + # To prevent overwriting + self.temp_fileobj = fileobj + self.fileobj = "" + self._with_as_usage = False + self._cloned = False + # The root of our page tree node + pages = DictionaryObject( + { + NameObject(PagesAttributes.TYPE): NameObject("/Pages"), + NameObject(PagesAttributes.COUNT): NumberObject(0), + NameObject(PagesAttributes.KIDS): ArrayObject(), + } + ) + self.flattened_pages = [] + self._encryption: Optional[Encryption] = None + self._encrypt_entry: Optional[DictionaryObject] = None + + if clone_from is not None: + if not isinstance(clone_from, PdfReader): + clone_from = PdfReader(clone_from) + self.clone_document_from_reader(clone_from) + self._cloned = True + if keep_initial_header and not self.incremental: + self._header = clone_from.pdf_header.encode() + else: + self._pages = self._add_object(pages) + self._root_object = DictionaryObject( + { + NameObject(PagesAttributes.TYPE): NameObject(Core.CATALOG), + NameObject(Core.PAGES): self._pages, + } + ) + self._add_object(self._root_object) + if full and not incremental: + self.incremental = False + if isinstance(self._ID, list): + if isinstance(self._ID[0], TextStringObject): + self._ID[0] = ByteStringObject(self._ID[0].get_original_bytes()) + if isinstance(self._ID[1], TextStringObject): + self._ID[1] = ByteStringObject(self._ID[1].get_original_bytes()) + + # for commonality + @property + def is_encrypted(self) -> bool: + """ + Read-only boolean property showing whether this PDF file is encrypted. + + Note that this property, if true, will remain true even after the + :meth:`decrypt()` method is called. + """ + return False + + @property + def root_object(self) -> DictionaryObject: + """ + Provide direct access to PDF Structure. + + Note: + Recommended only for read access. + + """ + return self._root_object + + @property + def _info(self) -> Optional[DictionaryObject]: + """ + Provide access to "/Info". Standardized with PdfReader. + + Returns: + /Info Dictionary; None if the entry does not exist + + """ + return ( + None + if self._info_obj is None + else cast(DictionaryObject, self._info_obj.get_object()) + ) + + @_info.setter + def _info(self, value: Optional[Union[IndirectObject, DictionaryObject]]) -> None: + if value is None: + try: + self._objects[self._info_obj.indirect_reference.idnum - 1] = None # type: ignore[union-attr] + except (KeyError, AttributeError): + pass + self._info_obj = None + else: + if self._info_obj is None: + self._info_obj = self._add_object(DictionaryObject()) + obj = cast(DictionaryObject, self._info_obj.get_object()) + obj.clear() + obj.update(cast(DictionaryObject, value.get_object())) + + @property + def xmp_metadata(self) -> Optional[XmpInformation]: + """XMP (Extensible Metadata Platform) data.""" + return cast(XmpInformation, self.root_object.xmp_metadata) + + @xmp_metadata.setter + def xmp_metadata(self, value: Union[XmpInformation, bytes, None]) -> None: + """XMP (Extensible Metadata Platform) data.""" + if value is None: + if "/Metadata" in self.root_object: + del self.root_object["/Metadata"] + return + + metadata = self.root_object.get("/Metadata", None) + if not isinstance(metadata, IndirectObject): + if metadata is not None: + del self.root_object["/Metadata"] + metadata_stream = StreamObject() + stream_reference = self._add_object(metadata_stream) + self.root_object[NameObject("/Metadata")] = stream_reference + else: + metadata_stream = cast(StreamObject, metadata.get_object()) + + if isinstance(value, XmpInformation): + bytes_data = value.stream.get_data() + else: + bytes_data = value + metadata_stream.set_data(bytes_data) + + @property + def with_as_usage(self) -> bool: + deprecation_no_replacement("with_as_usage", "5.0") + + @with_as_usage.setter + def with_as_usage(self, value: bool) -> None: + deprecation_no_replacement("with_as_usage", "5.0") + + def __enter__(self) -> Self: + """Store how writer is initialized by 'with'.""" + c: bool = self._cloned + t = self.temp_fileobj + self.__init__() # type: ignore[misc] + self._cloned = c + self._with_as_usage = True + self.fileobj = t # type: ignore[assignment] + return self + + def __exit__( + self, + exc_type: Optional[type[BaseException]], + exc: Optional[BaseException], + traceback: Optional[TracebackType], + ) -> None: + """Write data to the fileobj.""" + if self.fileobj and not self._cloned: + self.write(self.fileobj) + + @property + def pdf_header(self) -> str: + """ + Read/Write property of the PDF header that is written. + + This should be something like ``'%PDF-1.5'``. It is recommended to set + the lowest version that supports all features which are used within the + PDF file. + + Note: `pdf_header` returns a string but accepts bytes or str for writing + """ + return self._header.decode() + + @pdf_header.setter + def pdf_header(self, new_header: Union[str, bytes]) -> None: + if isinstance(new_header, str): + new_header = new_header.encode() + self._header = new_header + + def _add_object(self, obj: PdfObject) -> IndirectObject: + if ( + getattr(obj, "indirect_reference", None) is not None + and obj.indirect_reference.pdf == self # type: ignore[union-attr] + ): + return obj.indirect_reference # type: ignore[return-value] + # check for /Contents in Pages (/Contents in annotations are strings) + if isinstance(obj, DictionaryObject) and isinstance( + obj.get(PG.CONTENTS, None), (ArrayObject, DictionaryObject) + ): + obj[NameObject(PG.CONTENTS)] = self._add_object(obj[PG.CONTENTS]) + self._objects.append(obj) + obj.indirect_reference = IndirectObject(len(self._objects), 0, self) + return obj.indirect_reference + + def get_object( + self, + indirect_reference: Union[int, IndirectObject], + ) -> PdfObject: + if isinstance(indirect_reference, int): + obj = self._objects[indirect_reference - 1] + elif indirect_reference.pdf != self: + raise ValueError("PDF must be self") + else: + obj = self._objects[indirect_reference.idnum - 1] + if obj is None: + raise PdfReadError(f"Object {indirect_reference!r} not found!") + return obj + + def _replace_object( + self, + indirect_reference: Union[int, IndirectObject], + obj: PdfObject, + ) -> PdfObject: + if isinstance(indirect_reference, IndirectObject): + if indirect_reference.pdf != self: + raise ValueError("PDF must be self") + indirect_reference = indirect_reference.idnum + gen = self._objects[indirect_reference - 1].indirect_reference.generation # type: ignore[union-attr] + if ( + getattr(obj, "indirect_reference", None) is not None + and obj.indirect_reference.pdf != self # type: ignore[union-attr] + ): + obj = obj.clone(self) + self._objects[indirect_reference - 1] = obj + obj.indirect_reference = IndirectObject(indirect_reference, gen, self) + + assert isinstance(obj, PdfObject), "mypy" + return obj + + def _add_page( + self, + page: PageObject, + index: int, + excluded_keys: Iterable[str] = (), + ) -> PageObject: + if not isinstance(page, PageObject) or page.get(PagesAttributes.TYPE, None) != Core.PAGE: + raise ValueError("Invalid page object") + assert self.flattened_pages is not None, "mypy" + page_org = page + excluded_keys = list(excluded_keys) + excluded_keys += [PagesAttributes.PARENT, "/StructParents"] + # Acrobat does not accept two indirect references pointing on the same + # page; therefore in order to add multiple copies of the same + # page, we need to create a new dictionary for the page, however the + # objects below (including content) are not duplicated: + try: # delete an already existing page + del self._id_translated[id(page_org.indirect_reference.pdf)][ # type: ignore[union-attr] + page_org.indirect_reference.idnum # type: ignore[union-attr] + ] + except Exception: + pass + + page = cast( + "PageObject", page_org.clone(self, False, excluded_keys).get_object() + ) + if page_org.pdf is not None: + other = page_org.pdf.pdf_header + self.pdf_header = _get_max_pdf_version_header(self.pdf_header, other) + + node, idx = self._get_page_in_node(index) + page[NameObject(PagesAttributes.PARENT)] = node.indirect_reference + + if idx >= 0: + cast(ArrayObject, node[PagesAttributes.KIDS]).insert(idx, page.indirect_reference) + self.flattened_pages.insert(index, page) + else: + cast(ArrayObject, node[PagesAttributes.KIDS]).append(page.indirect_reference) + self.flattened_pages.append(page) + current: Optional[PdfObject] = node + recurse = 0 + while not is_null_or_none(current): + assert current is not None, "mypy" # guarded by is_null_or_none + node_dict = cast(DictionaryObject, current.get_object()) + node_dict[NameObject(PagesAttributes.COUNT)] = NumberObject(cast(int, node_dict[PagesAttributes.COUNT]) + 1) + current = node_dict.get(PagesAttributes.PARENT, None) + recurse += 1 + if recurse > 1000: + raise PyPdfError("Too many recursive calls!") + + if page_org.pdf is not None: + # the page may contain links to other pages, and those other + # pages may or may not already be added. we store the + # information we need, so that we can resolve the references + # later. + self._unresolved_links.extend(extract_links(page, page_org)) + self._merged_in_pages[page_org.indirect_reference] = page.indirect_reference + + return page + + def set_need_appearances_writer(self, state: bool = True) -> None: + """ + Sets the "NeedAppearances" flag in the PDF writer. + + The "NeedAppearances" flag indicates whether the appearance dictionary + for form fields should be automatically generated by the PDF viewer or + if the embedded appearance should be used. + + Args: + state: The actual value of the NeedAppearances flag. + + Returns: + None + + """ + # See §12.7.2 and §7.7.2 for more information: + # https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf + try: + # get the AcroForm tree + if CatalogAttributes.ACRO_FORM not in self._root_object: + self._root_object[ + NameObject(CatalogAttributes.ACRO_FORM) + ] = self._add_object(DictionaryObject()) + + need_appearances = NameObject(InteractiveFormDictEntries.NeedAppearances) + cast(DictionaryObject, self._root_object[CatalogAttributes.ACRO_FORM])[ + need_appearances + ] = BooleanObject(state) + except Exception as exc: # pragma: no cover + logger_warning( + "set_need_appearances_writer(%(state)s) catch : %(exc)s", + source=__name__, + state=state, + exc=exc, + ) + + def create_viewer_preferences(self) -> ViewerPreferences: + o = ViewerPreferences() + self._root_object[ + NameObject(CatalogAttributes.VIEWER_PREFERENCES) + ] = self._add_object(o) + return o + + def add_page( + self, + page: PageObject, + excluded_keys: Iterable[str] = (), + ) -> PageObject: + """ + Add a page to this PDF file. + + Recommended for advanced usage including the adequate excluded_keys. + + The page is usually acquired from a :class:`PdfReader` + instance. + + Args: + page: The page to add to the document. Should be + an instance of :class:`PageObject` + excluded_keys: + + Returns: + The added PageObject. + + """ + assert self.flattened_pages is not None, "mypy" + return self._add_page(page, len(self.flattened_pages), excluded_keys) + + def insert_page( + self, + page: PageObject, + index: int = 0, + excluded_keys: Iterable[str] = (), + ) -> PageObject: + """ + Insert a page in this PDF file. The page is usually acquired from a + :class:`PdfReader` instance. + + Args: + page: The page to add to the document. + index: Position at which the page will be inserted. + excluded_keys: + + Returns: + The added PageObject. + + """ + assert self.flattened_pages is not None, "mypy" + if index < 0: + index += len(self.flattened_pages) + if index < 0: + raise ValueError("Invalid index value") + if index >= len(self.flattened_pages): + return self.add_page(page, excluded_keys) + return self._add_page(page, index, excluded_keys) + + def _get_page_number_by_indirect( + self, indirect_reference: Union[int, NullObject, IndirectObject, None] + ) -> Optional[int]: + """ + Generate _page_id2num. + + Args: + indirect_reference: + + Returns: + The page number or None + + """ + # To provide same function as in PdfReader + if is_null_or_none(indirect_reference): + return None + assert indirect_reference is not None, "mypy" + if isinstance(indirect_reference, int): + indirect_reference = IndirectObject(indirect_reference, 0, self) + obj = indirect_reference.get_object() + if isinstance(obj, PageObject): + return obj.page_number + return None + + def add_blank_page( + self, width: Optional[float] = None, height: Optional[float] = None + ) -> PageObject: + """ + Append a blank page to this PDF file and return it. + + If no page size is specified, use the size of the last page. + + Args: + width: The width of the new page expressed in default user + space units. + height: The height of the new page expressed in default + user space units. + + Returns: + The newly appended page. + + Raises: + PageSizeNotDefinedError: if width and height are not defined + and previous page does not exist. + + """ + page = PageObject.create_blank_page(self, width, height) + return self.add_page(page) + + def insert_blank_page( + self, + width: Optional[Union[float, decimal.Decimal]] = None, + height: Optional[Union[float, decimal.Decimal]] = None, + index: int = 0, + ) -> PageObject: + """ + Insert a blank page to this PDF file and return it. + + If no page size is specified for a dimension, use the size of the last page. + + Args: + width: The width of the new page in default user space units. + height: The height of the new page in default user space units. + index: Position to add the page. + + Returns: + The newly inserted page. + + Raises: + PageSizeNotDefinedError: if width and height are not defined + and previous page does not exist. + IndexError: Index is outside of [-self.get_num_pages(), self.get_num_pages()] + """ + num_pages = self.get_num_pages() + if abs(index) > num_pages: + raise IndexError(f"Index should be in range [-{num_pages}, {num_pages}]") + + if num_pages: + # Use the chosen index, but do not exceed the available pages + fixed_index = min(index, num_pages - 1) + mediabox = self.pages[fixed_index].mediabox + if width is None or width <= 0: + width = mediabox.width + if height is None or height <= 0: + height = mediabox.height + + page = PageObject.create_blank_page(self, width, height) + self.insert_page(page, index) + return page + + @property + def open_destination( + self, + ) -> Union[Destination, TextStringObject, ByteStringObject, None]: + return super().open_destination + + @open_destination.setter + def open_destination(self, dest: Union[str, Destination, PageObject, None]) -> None: + if dest is None: + try: + del self._root_object["/OpenAction"] + except KeyError: + pass + elif isinstance(dest, str): + self._root_object[NameObject("/OpenAction")] = TextStringObject(dest) + elif isinstance(dest, Destination): + self._root_object[NameObject("/OpenAction")] = dest.dest_array + elif isinstance(dest, PageObject): + self._root_object[NameObject("/OpenAction")] = Destination( + "Opening", + dest.indirect_reference + if dest.indirect_reference is not None + else NullObject(), + PAGE_FIT, + ).dest_array + + def add_js(self, javascript: str) -> None: + """ + Add JavaScript which will launch upon opening this PDF. + + Args: + javascript: Your JavaScript. + + Example: + This will launch the print window when the PDF is opened. + + >>> from pypdf import PdfWriter + >>> output = PdfWriter() + >>> output.add_js("this.print({bUI:true,bSilent:false,bShrinkToFit:true});") + + """ + deprecate_with_replacement("add_js", "add_open_action", "7.0.0") + self.add_open_action(JavaScript(javascript)) + + def add_open_action(self, action: Action) -> None: + """ + Add an action to the document-level JavaScript name tree. + + Args: + action: The action to add. + + Example: + This will launch the print window when the PDF is opened. + + >>> from pypdf import PdfWriter + >>> from pypdf.actions import JavaScript + >>> output = PdfWriter() + >>> output.add_open_action(JavaScript("this.print({bUI:true,bSilent:false,bShrinkToFit:true});")) + """ + return Action._create_open_action(self, action) + + def add_attachment(self, filename: str, data: Union[str, bytes]) -> "EmbeddedFile": + """ + Embed a file inside the PDF. + + Reference: + https://opensource.adobe.com/dc-acrobat-sdk-docs/pdfstandards/PDF32000_2008.pdf + Section 7.11.3 + + Args: + filename: The filename to display. + data: The data in the file. + + Returns: + EmbeddedFile instance for the newly created embedded file. + + """ + return EmbeddedFile._create_new(self, filename, data) + + def append_pages_from_reader( + self, + reader: PdfReader, + after_page_append: Optional[Callable[[PageObject], None]] = None, + ) -> None: + """ + Copy pages from reader to writer. Includes an optional callback + parameter which is invoked after pages are appended to the writer. + + ``append`` should be preferred. + + Args: + reader: a PdfReader object from which to copy page + annotations to this writer object. The writer's annots + will then be updated. + after_page_append: + Callback function that is invoked after each page is appended to + the writer. Signature includes a reference to the appended page + (delegates to append_pages_from_reader). The single parameter of + the callback is a reference to the page just appended to the + document. + + """ + reader_num_pages = len(reader.pages) + # Copy pages from reader to writer + for reader_page_number in range(reader_num_pages): + reader_page = reader.pages[reader_page_number] + writer_page = self.add_page(reader_page) + # Trigger callback, pass writer page as parameter + if callable(after_page_append): + after_page_append(writer_page) + + def _merge_content_stream_to_page( + self, + page: PageObject, + new_content_data: bytes, + ) -> None: + """ + Combines existing content stream(s) with new content (as bytes). + + Args: + page: The page to which the new content data will be added. + new_content_data: A binary-encoded new content stream, for + instance the commands to draw an XObject. + """ + # First resolve the existing page content. This always is an IndirectObject: + # PDF Explained by John Whitington + # https://www.oreilly.com/library/view/pdf-explained/9781449321581/ch04.html + if NameObject("/Contents") in page: + existing_content_ref = page[NameObject("/Contents")] + existing_content = existing_content_ref.get_object() + + if isinstance(existing_content, ArrayObject): + # Create a new StreamObject for the new_content_data + new_stream_obj = StreamObject() + new_stream_obj.set_data(new_content_data) + existing_content.append(self._add_object(new_stream_obj)) + page[NameObject("/Contents")] = self._add_object(existing_content) + if isinstance(existing_content, StreamObject): + # Merge new content to existing StreamObject + merged_data = existing_content.get_data() + b"\n" + new_content_data + new_stream = StreamObject() + new_stream.set_data(merged_data) + page[NameObject("/Contents")] = self._add_object(new_stream) + else: + # If no existing content, then we have an empty page. + # Create a new StreamObject in a new /Contents entry. + new_stream = StreamObject() + new_stream.set_data(new_content_data) + page[NameObject("/Contents")] = self._add_object(new_stream) + + def _add_apstream_object( + self, + page: PageObject, + appearance_stream_obj: StreamObject, + object_name: str, + x_offset: float, + y_offset: float, + ) -> None: + """ + Adds an appearance stream to the page content in the form of + an XObject. + + Args: + page: The page to which to add the appearance stream. + appearance_stream_obj: The appearance stream. + object_name: The name of the appearance stream. + x_offset: The horizontal offset for the appearance stream. + y_offset: The vertical offset for the appearance stream. + """ + pg_res = cast(DictionaryObject, page[PG.RESOURCES]) + # Always add the resolved stream object to the writer to get a new IndirectObject. + # This ensures we have a valid IndirectObject managed by *this* writer. + xobject_ref = self._add_object(appearance_stream_obj) + xobject_name = NameObject(f"/Fm_{object_name}")._sanitize() + if "/XObject" not in pg_res: + pg_res[NameObject("/XObject")] = DictionaryObject() + pg_xo_res = cast(DictionaryObject, pg_res["/XObject"]) + if xobject_name not in pg_xo_res: + pg_xo_res[xobject_name] = xobject_ref + else: + logger_warning( + "XObject %(xobject_name)r already added to page resources. This might be an issue.", + source=__name__, + xobject_name=xobject_name, + ) + xobject_cm = Transformation().translate(x_offset, y_offset) + xobject_drawing_commands = f"q\n{xobject_cm._to_cm()}\n{xobject_name} Do\nQ".encode() + self._merge_content_stream_to_page(page, xobject_drawing_commands) + + FFBITS_NUL = FA.FfBits(0) + + def update_page_form_field_values( + self, + page: Union[PageObject, list[PageObject], None], + fields: Mapping[str, Union[str, list[str], tuple[str, str, float]]], + flags: FA.FfBits = FFBITS_NUL, + auto_regenerate: Optional[bool] = True, + flatten: bool = False, + ) -> None: + """ + Update the form field values for a given page from a fields dictionary. + + Copy field texts and values from fields to page. + If the field links to a parent object, add the information to the parent. + + Args: + page: `PageObject` - references **PDF writer's page** where the + annotations and field data will be updated. + `List[Pageobject]` - provides list of pages to be processed. + `None` - all pages. + fields: a Python dictionary of: + + * field names (/T) as keys and text values (/V) as value + * field names (/T) as keys and list of text values (/V) for multiple choice list + * field names (/T) as keys and tuple of: + * text values (/V) + * font id (e.g. /F1, the font id must exist) + * font size (0 for autosize) + + flags: A set of flags from :class:`~pypdf.constants.FieldDictionaryAttributes.FfBits`. + + auto_regenerate: Set/unset the need_appearances flag; + the flag is unchanged if auto_regenerate is None. + + flatten: Whether or not to flatten the annotation. If True, this adds the annotation's + appearance stream to the page contents. Note that this option does not remove the + annotation itself. + + """ + if CatalogAttributes.ACRO_FORM not in self._root_object: + raise PyPdfError("No /AcroForm dictionary in PDF of PdfWriter Object") + acro_form = cast(DictionaryObject, self._root_object[CatalogAttributes.ACRO_FORM]) + if InteractiveFormDictEntries.Fields not in acro_form: + raise PyPdfError("No /Fields dictionary in PDF of PdfWriter Object") + if isinstance(auto_regenerate, bool): + self.set_need_appearances_writer(auto_regenerate) + # Iterate through pages, update field values + if page is None: + page = list(self.pages) + if isinstance(page, list): + for p in page: + if PG.ANNOTS in p: # just to prevent warnings + self.update_page_form_field_values(p, fields, flags, None, flatten=flatten) + return + if PG.ANNOTS not in page: + logger_warning("No fields to update on this page", source=__name__) + return + appearance_stream_obj: Optional[StreamObject] = None + + for annotation in page[PG.ANNOTS]: # type: ignore[attr-defined] + annotation = cast(DictionaryObject, annotation.get_object()) + if annotation.get("/Subtype", "") != "/Widget": + continue + if "/FT" in annotation and "/T" in annotation: + parent_annotation = annotation + else: + parent_annotation = annotation.get( + PG.PARENT, DictionaryObject() + ).get_object() + + for field, value in fields.items(): + rectangle = cast(RectangleObject, annotation[AA.Rect]) + if not ( + self._get_qualified_field_name(parent=parent_annotation) == field + or parent_annotation.get("/T", None) == field + ): + continue + if ( + parent_annotation.get("/FT", None) == "/Ch" + and "/I" in parent_annotation + ): + del parent_annotation["/I"] + if flags: + annotation[NameObject(FA.Ff)] = NumberObject(flags) + # Set the field value + if not (value is None and flatten): # Only change values if given by user and not flattening. + if isinstance(value, list): + lst = ArrayObject(TextStringObject(v) for v in value) + parent_annotation[NameObject(FA.V)] = lst + elif isinstance(value, tuple): + annotation[NameObject(FA.V)] = TextStringObject( + value[0], + ) + else: + parent_annotation[NameObject(FA.V)] = TextStringObject(value) + # Get or create the field's appearance stream object + if parent_annotation.get(FA.FT) == "/Btn": + # Checkbox button (no /FT found in Radio widgets); + # We can find the associated appearance stream object + # within the annotation. + v = NameObject(value) + ap = cast(DictionaryObject, annotation[NameObject(AA.AP)]) + normal_ap = cast(DictionaryObject, ap["/N"]) + if v not in normal_ap: + v = NameObject("/Off") + appearance_stream_obj = normal_ap.get(v) + # Other cases will be updated through the for loop + annotation[NameObject(AA.AS)] = v + annotation[NameObject(FA.V)] = v + elif ( + parent_annotation.get(FA.FT) == "/Tx" + or parent_annotation.get(FA.FT) == "/Ch" + ): + # Textbox; we need to generate the appearance stream object + if isinstance(value, tuple): + appearance_stream_obj = TextStreamAppearance.from_text_annotation( + self, page, flatten, acro_form, parent_annotation, annotation, value[1], value[2] + ) + else: + appearance_stream_obj = TextStreamAppearance.from_text_annotation( + self, page, flatten, acro_form, parent_annotation, annotation + ) + # Add the appearance stream object + if AA.AP not in annotation: + annotation[NameObject(AA.AP)] = DictionaryObject( + {NameObject("/N"): self._add_object(appearance_stream_obj)} + ) + elif "/N" not in (ap:= cast(DictionaryObject, annotation[AA.AP])): + cast(DictionaryObject, annotation[NameObject(AA.AP)])[ + NameObject("/N") + ] = self._add_object(appearance_stream_obj) + else: # [/AP][/N] exists + n = annotation[AA.AP]["/N"].indirect_reference.idnum # type: ignore[index] + self._objects[n - 1] = appearance_stream_obj + appearance_stream_obj.indirect_reference = IndirectObject(n, 0, self) + elif ( + annotation.get(FA.FT) == "/Sig" + ): # deprecated # not implemented yet + logger_warning("Signature forms not implemented yet", source=__name__) + + if appearance_stream_obj and flatten: + self._add_apstream_object(page, appearance_stream_obj, field, rectangle[0], rectangle[1]) + + def reattach_fields( + self, page: Optional[PageObject] = None + ) -> list[DictionaryObject]: + """ + Parse annotations within the page looking for orphan fields and + reattach then into the Fields Structure. + + Args: + page: page to analyze. + If none is provided, all pages will be analyzed. + + Returns: + list of reattached fields. + + """ + lst = [] + if page is None: + for p in self.pages: + lst += self.reattach_fields(p) + return lst + + try: + af = cast(DictionaryObject, self._root_object[CatalogAttributes.ACRO_FORM]) + except KeyError: + af = DictionaryObject() + self._root_object[NameObject(CatalogAttributes.ACRO_FORM)] = af + try: + fields = cast(ArrayObject, af[InteractiveFormDictEntries.Fields]) + except KeyError: + fields = ArrayObject() + af[NameObject(InteractiveFormDictEntries.Fields)] = fields + + if "/Annots" not in page: + return lst + annotations = cast(ArrayObject, page["/Annots"]) + for idx, annotation in enumerate(annotations): + is_indirect = isinstance(annotation, IndirectObject) + annotation = cast(DictionaryObject, annotation.get_object()) + if annotation.get("/Subtype", "") == "/Widget" and "/FT" in annotation: + if ( + "indirect_reference" in annotation.__dict__ + and annotation.indirect_reference in fields + ): + continue + if not is_indirect: + annotations[idx] = self._add_object(annotation) + fields.append(annotation.indirect_reference) + lst.append(annotation) + return lst + + def _collect_incremental_clone_object_ids(self, reader: PdfReader) -> list[int]: + object_ids: set[int] = set() + for xref_entry in reader.xref.values(): + object_ids.update(filter(None, xref_entry)) + object_ids.update(filter(None, reader.xref_objStm)) + + object_count = len(object_ids) + if object_count > self._incremental_clone_object_count_limit: + raise LimitReachedError( + f"Incremental clone object count {object_count} exceeds " + f"maximum allowed count {self._incremental_clone_object_count_limit}." + ) + + max_object_id = max(object_ids, default=0) + if max_object_id > self._incremental_clone_object_id_limit: + raise LimitReachedError( + f"Incremental clone object ID {max_object_id} exceeds " + f"maximum allowed ID {self._incremental_clone_object_id_limit}." + ) + + return sorted(object_ids) + + def clone_reader_document_root(self, reader: PdfReader) -> None: + """ + Copy the reader document root to the writer and all sub-elements, + including pages, threads, outlines,... For partial insertion, ``append`` + should be considered. + + Args: + reader: PdfReader from which the document root should be copied. + + """ + self._info_obj = None + if self.incremental: + object_ids = self._collect_incremental_clone_object_ids(reader) + self._objects = [None] * (object_ids[-1] if object_ids else 0) + for object_id in object_ids: + reader_object = reader.get_object(object_id) + if reader_object is not None: + self._objects[object_id - 1] = reader_object.replicate(self) + else: + self._objects.clear() + self._root_object = reader.root_object.clone(self) + self._pages = self._root_object.raw_get("/Pages") + + trailer_size = cast(int, reader.trailer["/Size"]) + if len(self._objects) > trailer_size: + if self.strict: + raise PdfReadError( + f"Object count {len(self._objects)} exceeds defined trailer size {trailer_size}" + ) + logger_warning( + "Object count %(object_count)d exceeds defined trailer size %(trailer_size)d", + source=__name__, + object_count=len(self._objects), + trailer_size=trailer_size, + ) + + # must be done here before rewriting + if self.incremental: + self._original_hash = [ + (obj.hash_bin() if obj is not None else 0) for obj in self._objects + ] + + try: + self._flatten() + except IndexError: + raise PdfReadError("Got index error while flattening.") + + assert self.flattened_pages is not None + for p in self.flattened_pages: + self._replace_object(cast(IndirectObject, p.indirect_reference).idnum, p) + if not self.incremental: + p[NameObject("/Parent")] = self._pages + if not self.incremental: + cast(DictionaryObject, self._pages.get_object())[ + NameObject("/Kids") + ] = ArrayObject([p.indirect_reference for p in self.flattened_pages]) + + def clone_document_from_reader( + self, + reader: PdfReader, + after_page_append: Optional[Callable[[PageObject], None]] = None, + ) -> None: + """ + Create a copy (clone) of a document from a PDF file reader cloning + section '/Root' and '/Info' and '/ID' of the pdf. + + Args: + reader: PDF file reader instance from which the clone + should be created. + after_page_append: + Callback function that is invoked after each page is appended to + the writer. Signature includes a reference to the appended page + (delegates to append_pages_from_reader). The single parameter of + the callback is a reference to the page just appended to the + document. + + """ + self.clone_reader_document_root(reader) + inf = reader._info + if self.incremental: + if inf is not None: + self._info_obj = cast( + IndirectObject, inf.clone(self).indirect_reference + ) + assert isinstance(self._info, DictionaryObject), "mypy" + self._original_hash[ + self._info_obj.indirect_reference.idnum - 1 + ] = self._info.hash_bin() + elif inf is not None: + self._info_obj = self._add_object( + DictionaryObject(cast(DictionaryObject, inf.get_object())) + ) + # else: _info_obj = None done in clone_reader_document_root() + + try: + self._ID = cast(ArrayObject, reader._ID).clone(self) + except AttributeError: + pass + + if callable(after_page_append): + for page in cast( + ArrayObject, cast(DictionaryObject, self._pages.get_object())["/Kids"] + ): + after_page_append(page.get_object()) + + def _compute_document_identifier(self) -> ByteStringObject: + stream = BytesIO() + self._write_pdf_structure(stream) + stream.seek(0) + return ByteStringObject(_rolling_checksum(stream).encode("utf8")) + + def generate_file_identifiers(self) -> None: + """ + Generate an identifier for the PDF that will be written. + + The only point of this is ensuring uniqueness. Reproducibility is not + required. + When a file is first written, both identifiers shall be set to the same value. + If both identifiers match when a file reference is resolved, it is very + likely that the correct and unchanged file has been found. If only the first + identifier matches, a different version of the correct file has been found. + see §14.4 "File Identifiers". + """ + if self._ID: + id1 = self._ID[0] + id2 = self._compute_document_identifier() + else: + id1 = self._compute_document_identifier() + id2 = id1 + self._ID = ArrayObject((id1, id2)) + + def encrypt( + self, + user_password: str, + owner_password: Optional[str] = None, + use_128bit: bool = True, + permissions_flag: UserAccessPermissions = ALL_DOCUMENT_PERMISSIONS, + *, + algorithm: Optional[str] = None, + ) -> None: + """ + Encrypt this PDF file with the PDF Standard encryption handler. + + Args: + user_password: The password which allows for opening + and reading the PDF file with the restrictions provided. + owner_password: The password which allows for + opening the PDF files without any restrictions. By default, + the owner password is the same as the user password. + use_128bit: flag as to whether to use 128bit + encryption. When false, 40bit encryption will be used. + By default, this flag is on. + permissions_flag: permissions as described in + Table 3.20 of the PDF 1.7 specification. A bit value of 1 means + the permission is granted. + Hence an integer value of -1 will set all flags. + Bit position 3 is for printing, 4 is for modifying content, + 5 and 6 control annotations, 9 for form fields, + 10 for extraction of text and graphics. + algorithm: encrypt algorithm. Values may be one of "RC4-40", "RC4-128", + "AES-128", "AES-256-R5", "AES-256". If it is valid, + `use_128bit` will be ignored. + + """ + if self.incremental: + raise NotImplementedError("Encrypting incremental PDF files is currently not supported.") + + if owner_password is None: + owner_password = user_password + + if algorithm is not None: + try: + alg = getattr(EncryptAlgorithm, algorithm.replace("-", "_")) + except AttributeError: + raise ValueError(f"Algorithm '{algorithm}' NOT supported") + else: + alg = EncryptAlgorithm.RC4_128 + if not use_128bit: + alg = EncryptAlgorithm.RC4_40 + self.generate_file_identifiers() + assert self._ID + self._encryption = Encryption.make(alg, permissions_flag, self._ID[0]) + # in case call `encrypt` again + entry = self._encryption.write_entry(user_password, owner_password, strict=self.strict) + if self._encrypt_entry: + # replace old encrypt_entry + assert self._encrypt_entry.indirect_reference is not None + entry.indirect_reference = self._encrypt_entry.indirect_reference + self._objects[entry.indirect_reference.idnum - 1] = entry + else: + self._add_object(entry) + self._encrypt_entry = entry + + def _resolve_links(self) -> None: + """Patch up links that were added to the document earlier, to + make sure they still point to the same pages. + """ + for (new_link, old_link) in self._unresolved_links: + old_page = old_link.find_referenced_page() + if not old_page: + continue + new_page = self._merged_in_pages.get(old_page) + if new_page is None: + continue + new_link.patch_reference(self, new_page) + + def write_stream(self, stream: StreamType) -> None: + if hasattr(stream, "mode") and "b" not in stream.mode: + logger_warning( + "File <%(stream_name)s> to write to is not in binary mode. " + "It may not be written to correctly.", + source=__name__, + stream_name=stream.name, + ) + self._resolve_links() + + if self.incremental: + assert self._reader is not None, "mypy" + self._reader.stream.seek(0) + stream.write(self._reader.stream.read(-1)) + if len(self.list_objects_in_increment()) > 0: + self._write_increment(stream) # writes objs, xref stream and startxref + else: + object_positions, free_objects = self._write_pdf_structure(stream) + xref_location = self._write_xref_table( + stream, object_positions, free_objects + ) + self._write_trailer(stream, xref_location) + + def write(self, stream: Union[Path, StrByteType]) -> tuple[bool, IO[Any]]: + """ + Write the collection of pages added to this object out as a PDF file. + + Args: + stream: An object to write the file to. The object can support + the write method and the tell method, similar to a file object, or + be a file path, just like the fileobj, just named it stream to keep + existing workflow. + + Returns: + A tuple (bool, IO). + + """ + my_file = False + + if stream == "": + raise ValueError(f"Output({stream=}) is empty.") + + if isinstance(stream, (str, Path)): + stream = FileIO(stream, "wb") + my_file = True + + self.write_stream(stream) + + if my_file: + stream.close() + else: + stream.flush() + + return my_file, stream + + def list_objects_in_increment(self) -> list[IndirectObject]: + """ + For analysis or debugging. + Provides the list of new or modified objects that will be written + in the increment. + Deleted objects will not be freed but will become orphans. + + Returns: + List of new or modified IndirectObjects + + """ + original_hash_count = len(self._original_hash) + return [ + cast(IndirectObject, obj).indirect_reference + for i, obj in enumerate(self._objects) + if ( + obj is not None + and ( + i >= original_hash_count + or obj.hash_bin() != self._original_hash[i] + ) + ) + ] + + def _write_increment(self, stream: StreamType) -> None: + # Only reached from `write()` inside `if self.incremental`. + assert self._reader is not None, "mypy" + object_positions = {} + object_blocks = [] + current_start = -1 + current_stop = -2 + original_hash_count = len(self._original_hash) + for i, obj in enumerate(self._objects): + if obj is not None and ( + i >= original_hash_count + or obj.hash_bin() != self._original_hash[i] + ): + idnum = i + 1 + assert isinstance(obj, PdfObject), "mypy" + # first write new/modified object + object_positions[idnum] = stream.tell() + stream.write(f"{idnum} 0 obj\n".encode()) + """ encryption is not operational + if self._encryption and obj != self._encrypt_entry: + obj = self._encryption.encrypt_object(obj, idnum, 0) + """ + obj.write_to_stream(stream) + stream.write(b"\nendobj\n") + + # prepare xref + if idnum != current_stop: + if current_start > 0: + object_blocks.append( + [current_start, current_stop - current_start] + ) + current_start = idnum + current_stop = idnum + 1 + assert current_start > 0, "for pytest only" + object_blocks.append([current_start, current_stop - current_start]) + # write incremented xref + xref_location = stream.tell() + xr_id = len(self._objects) + 1 + stream.write(f"{xr_id} 0 obj".encode()) + init_data = { + NameObject("/Type"): NameObject("/XRef"), + NameObject("/Size"): NumberObject(xr_id + 1), + NameObject("/Root"): self.root_object.indirect_reference, + NameObject("/Filter"): NameObject("/FlateDecode"), + NameObject("/Index"): ArrayObject( + [NumberObject(_it) for _su in object_blocks for _it in _su] + ), + NameObject("/W"): ArrayObject( + [NumberObject(1), NumberObject(4), NumberObject(1)] + ), + "__streamdata__": b"", + } + if self._info is not None and ( + self._info.indirect_reference.idnum - 1 # type: ignore[union-attr] + >= len(self._original_hash) + or cast(IndirectObject, self._info).hash_bin() # kept for future + != self._original_hash[ + self._info.indirect_reference.idnum - 1 # type: ignore[union-attr] + ] + ): + init_data[NameObject(TK.INFO)] = self._info.indirect_reference + init_data[NameObject(TK.PREV)] = NumberObject(self._reader._startxref) + if self._ID: + init_data[NameObject(TK.ID)] = self._ID + xr = StreamObject.initialize_from_dictionary(init_data) + xr.set_data( + b"".join( + [struct.pack(b">BIB", 1, _pos, 0) for _pos in object_positions.values()] + ) + ) + xr.write_to_stream(stream) + stream.write(f"\nendobj\nstartxref\n{xref_location}\n%%EOF\n".encode()) # eof + + def _write_pdf_structure(self, stream: StreamType) -> tuple[list[int], list[int]]: + object_positions = [] + free_objects = [] + stream.write(self.pdf_header.encode() + b"\n") + stream.write(b"%\xE2\xE3\xCF\xD3\n") + + for idnum, obj in enumerate(self._objects, start=1): + if obj is not None: + object_positions.append(stream.tell()) + stream.write(f"{idnum} 0 obj\n".encode()) + if self._encryption and obj != self._encrypt_entry: + obj = self._encryption.encrypt_object(obj, idnum, 0) + obj.write_to_stream(stream) + stream.write(b"\nendobj\n") + else: + object_positions.append(-1) + free_objects.append(idnum) + free_objects.append(0) # add 0 to loop in accordance with specification + return object_positions, free_objects + + def _write_xref_table( + self, stream: StreamType, object_positions: list[int], free_objects: list[int] + ) -> int: + xref_location = stream.tell() + stream.write(b"xref\n") + stream.write(f"0 {len(self._objects) + 1}\n".encode()) + stream.write(f"{free_objects[0]:0>10} {65535:0>5} f \n".encode()) + free_idx = 1 + for offset in object_positions: + if offset > 0: + stream.write(f"{offset:0>10} {0:0>5} n \n".encode()) + else: + stream.write(f"{free_objects[free_idx]:0>10} {1:0>5} f \n".encode()) + free_idx += 1 + return xref_location + + def _write_trailer(self, stream: StreamType, xref_location: int) -> None: + """ + Write the PDF trailer to the stream. + + To quote the PDF specification: + [The] trailer [gives] the location of the cross-reference table and + of certain special objects within the body of the file. + """ + stream.write(b"trailer\n") + trailer = DictionaryObject( + { + NameObject(TK.SIZE): NumberObject(len(self._objects) + 1), + NameObject(TK.ROOT): self.root_object.indirect_reference, + } + ) + if self._info is not None: + trailer[NameObject(TK.INFO)] = self._info.indirect_reference + if self._ID is not None: + trailer[NameObject(TK.ID)] = self._ID + if self._encrypt_entry: + trailer[NameObject(TK.ENCRYPT)] = self._encrypt_entry.indirect_reference + trailer.write_to_stream(stream) + stream.write(f"\nstartxref\n{xref_location}\n%%EOF\n".encode()) # eof + + @property + def metadata(self) -> Optional[DocumentInformation]: + """ + Retrieve/set the PDF file's document information dictionary, if it exists. + + Args: + value: dict with the entries to be set. if None : remove the /Info entry from the pdf. + + Note that some PDF files use (XMP) metadata streams instead of document + information dictionaries, and these metadata streams will not be + accessed by this function, but by :meth:`~xmp_metadata`. + + """ + return super().metadata + + @metadata.setter + def metadata( + self, + value: Optional[Union[DocumentInformation, DictionaryObject, dict[Any, Any]]], + ) -> None: + if value is None: + self._info = None + else: + if self._info is not None: + self._info.clear() + + self.add_metadata(value) + + def add_metadata(self, infos: dict[str, Any]) -> None: + """ + Add custom metadata to the output. + + Args: + infos: a Python dictionary where each key is a field + and each value is your new metadata. + + """ + args = {} + if isinstance(infos, PdfObject): + infos = cast(DictionaryObject, infos.get_object()) + for key, value in list(infos.items()): + if isinstance(value, PdfObject): + value = value.get_object() + args[NameObject(key)] = create_string_object(str(value)) + if self._info is None: + self._info = DictionaryObject() + self._info.update(args) + + _UNSET = object() + + def compress_identical_objects( + self, + remove_identicals: Any = _UNSET, + remove_orphans: Any = _UNSET, + *, + remove_duplicates: bool = True, + remove_unreferenced: bool = True, + ) -> None: + """ + Parse the PDF file and merge objects that have the same hash. + This will make objects common to multiple pages. + Recommended to be used just before writing output. + + Args: + remove_identicals: Deprecated. + remove_orphans: Deprecated. + remove_duplicates: Remove duplicate objects. + remove_unreferenced: Remove unreferenced objects. + + """ + if remove_identicals != self._UNSET: + deprecate_with_replacement("remove_identicals", "remove_duplicates", "7.0.0") + assert isinstance(remove_identicals, bool) + remove_duplicates = remove_identicals + if remove_orphans != self._UNSET: + deprecate_with_replacement("remove_orphans", "remove_unreferenced", "7.0.0") + assert isinstance(remove_orphans, bool) + remove_unreferenced = remove_orphans + + def replace_in_obj( + obj: PdfObject, crossref: dict[IndirectObject, IndirectObject] + ) -> None: + if isinstance(obj, DictionaryObject): + key_val = obj.items() + elif isinstance(obj, ArrayObject): + key_val = enumerate(obj) # type: ignore[assignment] + else: + return + assert isinstance(obj, (DictionaryObject, ArrayObject)) + for k, v in key_val: + if isinstance(v, IndirectObject): + unreferenced[v.idnum - 1] = False + if v in crossref: + obj[k] = crossref[v] + else: + """The filtering on DictionaryObject and ArrayObject only + will be performed within replace_in_obj""" + replace_in_obj(v, crossref) + + # _idnum_hash: dict[hash] = (1st_ind_obj, [2nd_ind_obj,...]) + self._idnum_hash = {} + unreferenced = [True] * len(self._objects) + # look for similar objects + for idx, obj in enumerate(self._objects): + if is_null_or_none(obj): + continue + assert obj is not None, "mypy" # mypy: TypeGuard of `is_null_or_none` does not help here. + assert isinstance(obj.indirect_reference, IndirectObject) + h = obj.hash_value() + if remove_duplicates and h in self._idnum_hash: + self._idnum_hash[h][1].append(obj.indirect_reference) + self._objects[idx] = None + else: + self._idnum_hash[h] = (obj.indirect_reference, []) + + # generate the dict converting others to 1st + cnv = {v[0]: v[1] for v in self._idnum_hash.values() if len(v[1]) > 0} + cnv_rev: dict[IndirectObject, IndirectObject] = {} + for k, v in cnv.items(): + cnv_rev.update(zip(v, (k,) * len(v))) + + # replace reference to merged objects + for obj in self._objects: + if isinstance(obj, (DictionaryObject, ArrayObject)): + replace_in_obj(obj, cnv_rev) + + if remove_unreferenced: + unreferenced[self.root_object.indirect_reference.idnum - 1] = False # type: ignore[union-attr] + + if not is_null_or_none(self._info): + unreferenced[self._info.indirect_reference.idnum - 1] = False # type: ignore[union-attr] + + try: + unreferenced[self._ID.indirect_reference.idnum - 1] = False # type: ignore[union-attr] + except AttributeError: + pass + + for i in compress(range(len(self._objects)), unreferenced): + self._objects[i] = None + + def get_reference(self, obj: PdfObject) -> IndirectObject: + idnum = self._objects.index(obj) + 1 + ref = IndirectObject(idnum, 0, self) + assert ref.get_object() == obj + return ref + + def get_outline_root(self) -> TreeObject: + if Core.OUTLINES in self._root_object: + # Entries in the catalog dictionary + outline = cast(TreeObject, self._root_object[Core.OUTLINES]) + if not isinstance(outline, TreeObject): + t = TreeObject(outline) + self._replace_object(outline.indirect_reference.idnum, t) + outline = t + idnum = self._objects.index(outline) + 1 + outline_ref = IndirectObject(idnum, 0, self) + assert outline_ref.get_object() == outline + else: + outline = TreeObject() + outline.update({}) + outline_ref = self._add_object(outline) + self._root_object[NameObject(Core.OUTLINES)] = outline_ref + + return outline + + def get_threads_root(self) -> ArrayObject: + """ + The list of threads. + + See §12.4.3 of the PDF 1.7 or PDF 2.0 specification. + + Returns: + An array (possibly empty) of Dictionaries with an ``/F`` key, + and optionally information about the thread in ``/I`` or ``/Metadata`` keys. + + """ + if Core.THREADS in self._root_object: + # Entries in the catalog dictionary + threads = cast(ArrayObject, self._root_object[Core.THREADS]) + else: + threads = ArrayObject() + self._root_object[NameObject(Core.THREADS)] = threads + return threads + + @property + def threads(self) -> ArrayObject: + """ + Read-only property for the list of threads. + + See §12.4.3 of the PDF 1.7 or PDF 2.0 specification. + + Each element is a dictionary with an ``/F`` key, and optionally + information about the thread in ``/I`` or ``/Metadata`` keys. + """ + return self.get_threads_root() + + def add_outline_item_destination( + self, + page_destination: Union[IndirectObject, PageObject, TreeObject], + parent: Union[TreeObject, IndirectObject, None] = None, + before: Union[TreeObject, IndirectObject, None] = None, + is_open: bool = True, + ) -> IndirectObject: + page_destination = cast(PageObject, page_destination.get_object()) + if isinstance(page_destination, PageObject): + return self.add_outline_item_destination( + Destination( + f"page #{page_destination.page_number}", + cast(IndirectObject, page_destination.indirect_reference), + Fit.fit(), + ) + ) + + if parent is None: + parent = self.get_outline_root() + + page_destination[NameObject("/%is_open%")] = BooleanObject(is_open) + parent = cast(TreeObject, parent.get_object()) + page_destination_ref = self._add_object(page_destination) + if before is not None: + before = before.indirect_reference + parent.insert_child( + page_destination_ref, + before, + self, + page_destination.inc_parent_counter_outline, + ) + if "/Count" not in page_destination: + page_destination[NameObject("/Count")] = NumberObject(0) + + return page_destination_ref + + def add_outline_item_dict( + self, + outline_item: OutlineItemType, + parent: Union[TreeObject, IndirectObject, None] = None, + before: Union[TreeObject, IndirectObject, None] = None, + is_open: bool = True, + ) -> IndirectObject: + outline_item_object = TreeObject() + outline_item_object.update(outline_item) + + """code currently unreachable + if "/A" in outline_item: + action = DictionaryObject() + a_dict = cast(DictionaryObject, outline_item["/A"]) + for k, v in list(a_dict.items()): + action[NameObject(str(k))] = v + action_ref = self._add_object(action) + outline_item_object[NameObject("/A")] = action_ref + """ + return self.add_outline_item_destination( + outline_item_object, parent, before, is_open + ) + + def add_outline_item( + self, + title: str, + page_number: Union[PageObject, IndirectObject, int, None], + parent: Union[TreeObject, IndirectObject, None] = None, + before: Union[TreeObject, IndirectObject, None] = None, + color: Optional[Union[tuple[float, float, float], str]] = None, + bold: bool = False, + italic: bool = False, + fit: Fit = PAGE_FIT, + is_open: bool = True, + ) -> IndirectObject: + """ + Add an outline item (commonly referred to as a "Bookmark") to the PDF file. + + Args: + title: Title to use for this outline item. + page_number: Page number this outline item will point to. + parent: A reference to a parent outline item to create nested + outline items. + before: + color: Color of the outline item's font as a red, green, blue tuple + from 0.0 to 1.0 or as a Hex String (#RRGGBB) + bold: Outline item font is bold + italic: Outline item font is italic + fit: The fit of the destination page. + + Returns: + The added outline item as an indirect object. + + """ + page_ref: Union[NullObject, IndirectObject, NumberObject, None] + if isinstance(italic, Fit): # it means that we are on the old params + if fit is not None and page_number is None: + page_number = fit + return self.add_outline_item( + title, page_number, parent, None, before, color, bold, italic, is_open=is_open + ) + if page_number is None: + action_ref = None + else: + if isinstance(page_number, IndirectObject): + page_ref = page_number + elif isinstance(page_number, PageObject): + page_ref = page_number.indirect_reference + elif isinstance(page_number, int): + try: + page_ref = self.pages[page_number].indirect_reference + except IndexError: + page_ref = NumberObject(page_number) + if page_ref is None: + logger_warning( + "can not find reference of page %(page_number)s", + source=__name__, + page_number=page_number, + ) + page_ref = NullObject() + dest = Destination( + NameObject("/" + title + " outline item"), + page_ref, + fit, + ) + + action_ref = self._add_object( + DictionaryObject( + { + NameObject(GoToActionArguments.D): dest.dest_array, + NameObject(GoToActionArguments.S): NameObject("/GoTo"), + } + ) + ) + outline_item = self._add_object( + _create_outline_item(action_ref, title, color, italic, bold) + ) + + if parent is None: + parent = self.get_outline_root() + return self.add_outline_item_destination(outline_item, parent, before, is_open) + + def add_outline(self) -> None: + raise NotImplementedError( + "This method is not yet implemented. Use :meth:`add_outline_item` instead." + ) + + def add_named_destination_array( + self, title: TextStringObject, destination: Union[IndirectObject, ArrayObject] + ) -> None: + named_dest = self.get_named_dest_root() + i = 0 + while i < len(named_dest): + if title < named_dest[i]: + named_dest.insert(i, destination) + named_dest.insert(i, TextStringObject(title)) + return + i += 2 + named_dest.extend([TextStringObject(title), destination]) + return + + def add_named_destination_object( + self, + page_destination: PdfObject, + ) -> IndirectObject: + page_destination_ref = self._add_object(page_destination.dest_array) # type: ignore[attr-defined] + self.add_named_destination_array( + cast("TextStringObject", page_destination["/Title"]), page_destination_ref # type: ignore[index] + ) + + return page_destination_ref + + def _get_page_reference(self, page_number: int) -> Any: + """Look up a page by number, reporting a bad index rather than an IndexError from the kids array.""" + pages = cast(DictionaryObject, self.get_object(self._pages)) + kids = cast(ArrayObject, pages[PagesAttributes.KIDS]) + count = len(kids) + if not (-count <= page_number < count): + raise IndexError(f"Page number {page_number} is out of range") + return kids[page_number] + + def add_named_destination( + self, + title: str, + page_number: int, + ) -> IndirectObject: + page_ref = self._get_page_reference(page_number) + dest = DictionaryObject() + dest.update( + { + NameObject(GoToActionArguments.D): ArrayObject( + [page_ref, NameObject(TypFitArguments.FIT_H), NumberObject(826)] + ), + NameObject(GoToActionArguments.S): NameObject("/GoTo"), + } + ) + + dest_ref = self._add_object(dest) + if not isinstance(title, TextStringObject): + title = TextStringObject(str(title)) + + self.add_named_destination_array(title, dest_ref) + return dest_ref + + def remove_links(self) -> None: + """Remove links and annotations from this output.""" + for page in self.pages: + self.remove_objects_from_page(page, ObjectDeletionFlag.ALL_ANNOTATIONS) + + def remove_annotations( + self, subtypes: Optional[Union[AnnotationSubtype, Iterable[AnnotationSubtype]]] + ) -> None: + """ + Remove annotations by annotation subtype. + + Args: + subtypes: subtype or list of subtypes to be removed. + Examples are: "/Link", "/FileAttachment", "/Sound", + "/Movie", "/Screen", ... + If you want to remove all annotations, use subtypes=None. + + """ + for page in self.pages: + self._remove_annots_from_page(page, subtypes) + + def _remove_annots_from_page( + self, + page: Union[IndirectObject, PageObject, DictionaryObject], + subtypes: Optional[Iterable[str]], + ) -> None: + page = cast(DictionaryObject, page.get_object()) + if PG.ANNOTS in page: + i = 0 + while i < len(cast(ArrayObject, page[PG.ANNOTS])): + an = cast(ArrayObject, page[PG.ANNOTS])[i] + obj = cast(DictionaryObject, an.get_object()) + if subtypes is None or cast(str, obj["/Subtype"]) in subtypes: + if isinstance(an, IndirectObject): + self._objects[an.idnum - 1] = NullObject() # to reduce PDF size + del page[PG.ANNOTS][i] # type:ignore + else: + i += 1 + + def remove_objects_from_page( + self, + page: Union[PageObject, DictionaryObject], + to_delete: Union[ObjectDeletionFlag, Iterable[ObjectDeletionFlag]], + text_filters: Optional[dict[str, Any]] = None + ) -> None: + """ + Remove objects specified by ``to_delete`` from the given page. + + Args: + page: Page object to clean up. + to_delete: Objects to be deleted; can be a ``ObjectDeletionFlag`` + or a list of ObjectDeletionFlag + text_filters: Properties of text to be deleted, if applicable. Optional. + This is a Python dictionary with the following properties: + + * font_ids: List of font resource IDs (such as /F1 or /T1_0) to be deleted. + + """ + if isinstance(to_delete, (list, tuple)): + for to_d in to_delete: + self.remove_objects_from_page(page, to_d) + return None + assert isinstance(to_delete, ObjectDeletionFlag) + + if to_delete & ObjectDeletionFlag.LINKS: + return self._remove_annots_from_page(page, ("/Link",)) + if to_delete & ObjectDeletionFlag.ATTACHMENTS: + return self._remove_annots_from_page( + page, ("/FileAttachment", "/Sound", "/Movie", "/Screen") + ) + if to_delete & ObjectDeletionFlag.OBJECTS_3D: + return self._remove_annots_from_page(page, ("/3D",)) + if to_delete & ObjectDeletionFlag.ALL_ANNOTATIONS: + return self._remove_annots_from_page(page, None) + + jump_operators = [] + if to_delete & ObjectDeletionFlag.DRAWING_IMAGES: + jump_operators = [ + b"w", b"J", b"j", b"M", b"d", b"i", + b"W", b"W*", + b"b", b"b*", b"B", b"B*", b"S", b"s", b"f", b"f*", b"F", b"n", + b"m", b"l", b"c", b"v", b"y", b"h", b"re", + b"sh" + ] + if to_delete & ObjectDeletionFlag.TEXT: + jump_operators = [b"Tj", b"TJ", b"'", b'"'] + + if not isinstance(page, PageObject): + page = PageObject(self, page.indirect_reference) # pragma: no cover + if "/Contents" in page: + content = cast(ContentStream, page.get_contents()) + + images, forms = self._remove_objects_from_page__clean_forms( + elt=page, stack=[], jump_operators=jump_operators, to_delete=to_delete, text_filters=text_filters, + ) + + self._remove_objects_from_page__clean( + content=content, images=images, forms=forms, + jump_operators=jump_operators, to_delete=to_delete, + text_filters=text_filters + ) + page.replace_contents(content) + return None + + def _remove_objects_from_page__clean( + self, + content: ContentStream, + images: list[str], + forms: list[str], + jump_operators: list[bytes], + to_delete: ObjectDeletionFlag, + text_filters: Optional[dict[str, Any]] = None, + ) -> None: + font_id = None + font_ids_to_delete = [] + if text_filters and to_delete & ObjectDeletionFlag.TEXT: + font_ids_to_delete = text_filters.get("font_ids", []) + + i = 0 + while i < len(content.operations): + operands, operator = content.operations[i] + if operator == b"Tf": + font_id = operands[0] + if ( + ( + operator == b"INLINE IMAGE" + and (to_delete & ObjectDeletionFlag.INLINE_IMAGES) + ) + or (operator in jump_operators) + or ( + operator == b"Do" + and (to_delete & ObjectDeletionFlag.XOBJECT_IMAGES) + and (operands[0] in images) + ) + ): + if ( + not to_delete & ObjectDeletionFlag.TEXT + or (to_delete & ObjectDeletionFlag.TEXT and not text_filters) + or (to_delete & ObjectDeletionFlag.TEXT and font_id in font_ids_to_delete) + ): + del content.operations[i] + else: + i += 1 + else: + i += 1 + content.get_data() # this ensures ._data is rebuilt from the .operations + + def _remove_objects_from_page__clean_forms( + self, + elt: DictionaryObject, + stack: list[DictionaryObject], + jump_operators: list[bytes], + to_delete: ObjectDeletionFlag, + text_filters: Optional[dict[str, Any]] = None, + ) -> tuple[list[str], list[str]]: + # elt in recursive call is a new ContentStream object, so we have to check the indirect_reference + if (elt in stack) or ( + hasattr(elt, "indirect_reference") and any( + elt.indirect_reference == getattr(x, "indirect_reference", -1) + for x in stack + ) + ): + # to prevent infinite looping + return [], [] # pragma: no cover + try: + d = cast( + dict[Any, Any], + cast(DictionaryObject, elt["/Resources"])["/XObject"], + ) + except KeyError: + d = {} + images = [] + forms = [] + for k, v in d.items(): + o = v.get_object() + try: + content: Any = None + if ( + to_delete & ObjectDeletionFlag.XOBJECT_IMAGES + and o["/Subtype"] == "/Image" + ): + content = NullObject() # to delete the image keeping the entry + images.append(k) + if o["/Subtype"] == "/Form": + forms.append(k) + if isinstance(o, ContentStream): + content = o + else: + content = ContentStream(o, self) + content.update( + { + k1: v1 + for k1, v1 in o.items() + if k1 not in ["/Length", "/Filter", "/DecodeParms"] + } + ) + try: + content.indirect_reference = o.indirect_reference + except AttributeError: # pragma: no cover + pass + stack.append(elt) + + # clean subforms + self._remove_objects_from_page__clean_forms( + elt=content, stack=stack, jump_operators=jump_operators, to_delete=to_delete, + text_filters=text_filters, + ) + if content is not None: + if isinstance(v, IndirectObject): + self._objects[v.idnum - 1] = content + else: + # should only occur in a PDF not respecting PDF spec + # where streams must be indirected. + d[k] = self._add_object(content) # pragma: no cover + except (TypeError, KeyError): + pass + for im in images: + del d[im] # for clean-up + if isinstance(elt, StreamObject): # for /Form + if not isinstance(elt, ContentStream): # pragma: no cover + e = ContentStream(elt, self) + e.update(elt.items()) + elt = e + # clean the content + self._remove_objects_from_page__clean( + content=elt, images=images, forms=forms, jump_operators=jump_operators, + to_delete=to_delete, text_filters=text_filters + ) + return images, forms + + def remove_images( + self, + to_delete: ImageType = ImageType.ALL, + ) -> None: + """ + Remove images from this output. + + Args: + to_delete: The type of images to be deleted + (default = all images types) + + """ + if isinstance(to_delete, bool): + to_delete = ImageType.ALL + + i = ObjectDeletionFlag.NONE + + for image in ("XOBJECT_IMAGES", "INLINE_IMAGES", "DRAWING_IMAGES"): + if to_delete & ImageType[image]: + i |= ObjectDeletionFlag[image] + + for page in self.pages: + self.remove_objects_from_page(page, i) + + def remove_text(self, font_names: Optional[list[str]] = None) -> None: + """ + Remove text from the PDF. + + Args: + font_names: List of font names to remove, such as "Helvetica-Bold". + Optional. If not specified, all text will be removed. + """ + if not font_names: + font_names = [] + + for page in self.pages: + resource_ids_to_remove = [] + + # Content streams reference fonts and other resources with names like "/F1" or "/T1_0" + # Font names need to be converted to resource names/IDs for easier removal + if font_names: + # Recursively loop through page objects to gather font info + def get_font_info( + obj: Any, + font_info: Optional[dict[str, Any]] = None, + key: Optional[str] = None + ) -> dict[str, Any]: + if font_info is None: + font_info = {} + if isinstance(obj, IndirectObject): + obj = obj.get_object() + if isinstance(obj, dict): + if obj.get("/Type") == "/Font": + font_name = obj.get("/BaseFont", "") + # Normalize font names like "/RRXFFV+Palatino-Bold" to "Palatino-Bold" + normalized_font_name = font_name.lstrip("/").split("+")[-1] + if normalized_font_name not in font_info: + font_info[normalized_font_name] = { + "normalized_font_name": normalized_font_name, + "resource_ids": [], + } + if key not in font_info[normalized_font_name]["resource_ids"]: + font_info[normalized_font_name]["resource_ids"].append(key) + for k in obj: + font_info = get_font_info(obj[k], font_info, k) + elif isinstance(obj, (list, ArrayObject)): + for child_obj in obj: + font_info = get_font_info(child_obj, font_info) + return font_info + + # Add relevant resource names for removal + font_info = get_font_info(page.get("/Resources")) + for font_name in font_names: + if font_name in font_info: + resource_ids_to_remove.extend(font_info[font_name]["resource_ids"]) + + text_filters = {} + if font_names: + text_filters["font_ids"] = resource_ids_to_remove + self.remove_objects_from_page(page, ObjectDeletionFlag.TEXT, text_filters=text_filters) + + def add_uri( + self, + page_number: int, + uri: str, + rect: RectangleObject, + border: Optional[Sequence[Any]] = None, + ) -> None: + """ + Add an URI from a rectangular area to the specified page. + + Args: + page_number: index of the page on which to place the URI action. + uri: URI of resource to link to. + rect: :class:`RectangleObject` or + array of four integers specifying the clickable rectangular area + ``[xLL, yLL, xUR, yUR]``, or string in the form + ``"[ xLL yLL xUR yUR ]"``. + border: if provided, an array describing border-drawing + properties. See the PDF spec for details. No border will be + drawn if this argument is omitted. + + """ + page_link = self._get_page_reference(page_number) + page_ref = cast(dict[str, Any], self.get_object(page_link)) + + border_arr: BorderArrayType + if border is not None: + border_arr = [NumberObject(n) for n in border[:3]] + if len(border) == 4: + dash_pattern = ArrayObject([NumberObject(n) for n in border[3]]) + border_arr.append(dash_pattern) + else: + border_arr = [NumberObject(2), NumberObject(2), NumberObject(2)] + + if isinstance(rect, str): + coords = [float(n) for n in rect.strip().strip("[]").split()] + rect = RectangleObject(coords) + elif isinstance(rect, RectangleObject): + pass + else: + rect = RectangleObject(rect) + + lnk2 = DictionaryObject() + lnk2.update( + { + NameObject("/S"): NameObject("/URI"), + NameObject("/URI"): TextStringObject(uri), + } + ) + lnk = DictionaryObject() + lnk.update( + { + NameObject(AA.Type): NameObject("/Annot"), + NameObject(AA.Subtype): NameObject("/Link"), + NameObject(AA.P): page_link, + NameObject(AA.Rect): rect, + NameObject("/H"): NameObject("/I"), + NameObject(AA.Border): ArrayObject(border_arr), + NameObject("/A"): lnk2, + } + ) + lnk_ref = self._add_object(lnk) + + if PG.ANNOTS in page_ref: + page_ref[PG.ANNOTS].append(lnk_ref) + else: + page_ref[NameObject(PG.ANNOTS)] = ArrayObject([lnk_ref]) + + _valid_layouts = ( + "/NoLayout", + "/SinglePage", + "/OneColumn", + "/TwoColumnLeft", + "/TwoColumnRight", + "/TwoPageLeft", + "/TwoPageRight", + ) + + def _get_page_layout(self) -> Optional[LayoutType]: + try: + return cast(LayoutType, self._root_object["/PageLayout"]) + except KeyError: + return None + + def _set_page_layout(self, layout: Union[NameObject, LayoutType]) -> None: + """ + Set the page layout. + + Args: + layout: The page layout to be used. + + .. list-table:: Valid ``layout`` arguments + :widths: 50 200 + + * - /NoLayout + - Layout explicitly not specified + * - /SinglePage + - Show one page at a time + * - /OneColumn + - Show one column at a time + * - /TwoColumnLeft + - Show pages in two columns, odd-numbered pages on the left + * - /TwoColumnRight + - Show pages in two columns, odd-numbered pages on the right + * - /TwoPageLeft + - Show two pages at a time, odd-numbered pages on the left + * - /TwoPageRight + - Show two pages at a time, odd-numbered pages on the right + + """ + if not isinstance(layout, NameObject): + if layout not in self._valid_layouts: + logger_warning( + "Layout should be one of: %(layouts)s", + source=__name__, + layouts=", ".join(self._valid_layouts), + ) + layout = NameObject(layout) + self._root_object.update({NameObject("/PageLayout"): layout}) + + def set_page_layout(self, layout: LayoutType) -> None: + """ + Set the page layout. + + Args: + layout: The page layout to be used + + .. list-table:: Valid ``layout`` arguments + :widths: 50 200 + + * - /NoLayout + - Layout explicitly not specified + * - /SinglePage + - Show one page at a time + * - /OneColumn + - Show one column at a time + * - /TwoColumnLeft + - Show pages in two columns, odd-numbered pages on the left + * - /TwoColumnRight + - Show pages in two columns, odd-numbered pages on the right + * - /TwoPageLeft + - Show two pages at a time, odd-numbered pages on the left + * - /TwoPageRight + - Show two pages at a time, odd-numbered pages on the right + + """ + self._set_page_layout(layout) + + @property + def page_layout(self) -> Optional[LayoutType]: + """ + Page layout property. + + .. list-table:: Valid ``layout`` values + :widths: 50 200 + + * - /NoLayout + - Layout explicitly not specified + * - /SinglePage + - Show one page at a time + * - /OneColumn + - Show one column at a time + * - /TwoColumnLeft + - Show pages in two columns, odd-numbered pages on the left + * - /TwoColumnRight + - Show pages in two columns, odd-numbered pages on the right + * - /TwoPageLeft + - Show two pages at a time, odd-numbered pages on the left + * - /TwoPageRight + - Show two pages at a time, odd-numbered pages on the right + """ + return self._get_page_layout() + + @page_layout.setter + def page_layout(self, layout: LayoutType) -> None: + self._set_page_layout(layout) + + _valid_modes = ( + "/UseNone", + "/UseOutlines", + "/UseThumbs", + "/FullScreen", + "/UseOC", + "/UseAttachments", + ) + + def _get_page_mode(self) -> Optional[PagemodeType]: + try: + return cast(PagemodeType, self._root_object["/PageMode"]) + except KeyError: + return None + + @property + def page_mode(self) -> Optional[PagemodeType]: + """ + Page mode property. + + .. list-table:: Valid ``mode`` values + :widths: 50 200 + + * - /UseNone + - Do not show outline or thumbnails panels + * - /UseOutlines + - Show outline (aka bookmarks) panel + * - /UseThumbs + - Show page thumbnails panel + * - /FullScreen + - Fullscreen view + * - /UseOC + - Show Optional Content Group (OCG) panel + * - /UseAttachments + - Show attachments panel + """ + return self._get_page_mode() + + @page_mode.setter + def page_mode(self, mode: PagemodeType) -> None: + if isinstance(mode, NameObject): + mode_name: NameObject = mode + else: + if mode not in self._valid_modes: + logger_warning( + "Mode should be one of: %(modes)s", + source=__name__, + modes=", ".join(self._valid_modes), + ) + mode_name = NameObject(mode) + self._root_object.update({NameObject("/PageMode"): mode_name}) + + def add_annotation( + self, + page_number: Union[int, PageObject], + annotation: dict[str, Any], + ) -> DictionaryObject: + """ + Add a single annotation to the page. + The added annotation must be a new annotation. + It cannot be recycled. + + Args: + page_number: PageObject or page index. + annotation: Annotation to be added (created with annotation). + + Returns: + The inserted object. + This can be used for popup creation, for example. + + """ + page = page_number + if isinstance(page, int): + page = self.pages[page] + elif not isinstance(page, PageObject): + raise TypeError("page: invalid type") + + to_add = cast(DictionaryObject, _pdf_objectify(annotation)) + to_add[NameObject("/P")] = page.indirect_reference + + if page.annotations is None: + page[NameObject("/Annots")] = ArrayObject() + assert page.annotations is not None + + # Resolve pypdf's intermediate internal-link destination. + destination = to_add.get("/Dest") + if ( + to_add.get("/Subtype") == "/Link" + and isinstance(destination, DictionaryObject) + and "target_page_index" in destination + ): + target_page_reference: Union[IndirectObject, NumberObject] + target_page_index = cast(int, destination["target_page_index"]) + fit_type = cast(str, destination["fit"]) + # Work around the intermediate destination containing native Python + # objects instead of PdfObject instances. + fit_args = cast(Sequence[Any], dict(destination)["fit_args"]) + + if 0 <= target_page_index < len(self.pages): + target_page_reference = cast( + IndirectObject, + self.pages[target_page_index].indirect_reference, + ) + else: + # Preserve support for referencing a page that may be added + # later. See #2450. + target_page_reference = NumberObject(target_page_index) + + to_add[NameObject("/Dest")] = Destination( + NameObject("/LinkName"), + target_page_reference, + Fit(fit_type=fit_type, fit_args=fit_args), + ).dest_array + + page.annotations.append(self._add_object(to_add)) + + if to_add.get("/Subtype") == "/Popup" and NameObject("/Parent") in to_add: + cast(DictionaryObject, to_add["/Parent"].get_object())[ + NameObject("/Popup") + ] = to_add.indirect_reference + + return to_add + + def clean_page(self, page: Union[PageObject, IndirectObject]) -> PageObject: + """ + Perform some clean up in the page. + Currently: convert NameObject named destination to TextStringObject + (required for names/dests list) + + Args: + page: + + Returns: + The cleaned PageObject + + """ + page = cast("PageObject", page.get_object()) + for a in page.get("/Annots", []): + a_obj = a.get_object() + d = a_obj.get("/Dest", None) + act = a_obj.get("/A", None) + if isinstance(d, NameObject): + a_obj[NameObject("/Dest")] = TextStringObject(d) + elif act is not None: + act = act.get_object() + d = act.get("/D", None) + if isinstance(d, NameObject): + act[NameObject("/D")] = TextStringObject(d) + return page + + def _create_stream( + self, fileobj: Union[Path, StrByteType, PdfReader] + ) -> tuple[IOBase, Optional[Encryption]]: + # If the fileobj parameter is a string, assume it is a path + # and create a file object at that location. If it is a file, + # copy the file's contents into a BytesIO stream object; if + # it is a PdfReader, copy that reader's stream into a + # BytesIO stream. + # If fileobj is none of the above types, it is not modified + encryption_obj = None + stream: IOBase + if isinstance(fileobj, (str, Path)): + with FileIO(fileobj, "rb") as f: + stream = BytesIO(f.read()) + elif isinstance(fileobj, PdfReader): + if fileobj._encryption: + encryption_obj = fileobj._encryption + orig_tell = fileobj.stream.tell() + fileobj.stream.seek(0) + stream = BytesIO(fileobj.stream.read()) + + # reset the stream to its original location + fileobj.stream.seek(orig_tell) + elif hasattr(fileobj, "seek") and hasattr(fileobj, "read"): + fileobj.seek(0) + filecontent = fileobj.read() + stream = BytesIO(filecontent) + else: + raise NotImplementedError( + "Merging requires an object that PdfReader can parse. " + "Typically, that is a Path or a string representing a Path, " + "a file object, or an object implementing .seek and .read. " + "Passing a PdfReader directly works as well." + ) + return stream, encryption_obj + + def append( + self, + fileobj: Union[StrByteType, PdfReader, Path], + outline_item: Union[str, PageRange, tuple[int, int], tuple[int, int, int], list[int], None] = None, + pages: Union[PageRange, tuple[int, int], tuple[int, int, int], list[int], list[PageObject], None] = None, + import_outline: bool = True, + excluded_fields: Optional[Union[list[str], tuple[str, ...]]] = None, + ) -> None: + """ + Identical to the :meth:`merge()` method, but assumes you want to + concatenate all pages onto the end of the file instead of specifying a + position. + + Args: + fileobj: A File Object or an object that supports the standard + read and seek methods similar to a File Object. Could also be a + string representing a path to a PDF file. + outline_item: Optionally, you may specify a string to build an + outline (aka 'bookmark') to identify the beginning of the + included file. + pages: Can be a :class:`PageRange` + or a ``(start, stop[, step])`` tuple + or a list of pages to be processed + to merge only the specified range of pages from the source + document into the output document. + import_outline: You may prevent the source document's + outline (collection of outline items, previously referred to as + 'bookmarks') from being imported by specifying this as ``False``. + excluded_fields: Provide the list of fields/keys to be ignored + if ``/Annots`` is part of the list, the annotation will be ignored + if ``/B`` is part of the list, the articles will be ignored + + """ + if excluded_fields is None: + excluded_fields = () + if isinstance(outline_item, (tuple, list, PageRange)): + if isinstance(pages, bool): + if not isinstance(import_outline, bool): + excluded_fields = import_outline + import_outline = pages + pages = outline_item + self.merge( + None, + fileobj, + None, + pages, + import_outline, + excluded_fields, + ) + else: # if isinstance(outline_item, str): + self.merge( + None, + fileobj, + outline_item, + pages, + import_outline, + excluded_fields, + ) + + def merge( + self, + position: Optional[int], + fileobj: Union[Path, StrByteType, PdfReader], + outline_item: Optional[str] = None, + pages: Optional[Union[PageRangeSpec, list[PageObject]]] = None, + import_outline: bool = True, + excluded_fields: Optional[Union[list[str], tuple[str, ...]]] = (), + ) -> None: + """ + Merge the pages from the given file into the output file at the + specified page number. + + Args: + position: The *page number* to insert this file. File will + be inserted after the given number. + fileobj: A File Object or an object that supports the standard + read and seek methods similar to a File Object. Could also be a + string representing a path to a PDF file. + outline_item: Optionally, you may specify a string to build an outline + (aka 'bookmark') to identify the + beginning of the included file. + pages: can be a :class:`PageRange` + or a ``(start, stop[, step])`` tuple + or a list of pages to be processed + to merge only the specified range of pages from the source + document into the output document. + import_outline: You may prevent the source document's + outline (collection of outline items, previously referred to as + 'bookmarks') from being imported by specifying this as ``False``. + excluded_fields: provide the list of fields/keys to be ignored + if ``/Annots`` is part of the list, the annotation will be ignored + if ``/B`` is part of the list, the articles will be ignored + + Raises: + TypeError: The pages attribute is not configured properly + + """ + if isinstance(fileobj, PdfDocCommon): + reader = fileobj + else: + stream, _encryption_obj = self._create_stream(fileobj) + # Create a new PdfReader instance using the stream + # (either file or BytesIO or StringIO) created above + reader = PdfReader(stream, strict=False) # type: ignore[arg-type] + + if excluded_fields is None: + excluded_fields = () + # Find the range of pages to merge. + if pages is None: + pages = list(range(len(reader.pages))) + elif isinstance(pages, PageRange): + pages = list(range(*pages.indices(len(reader.pages)))) + elif isinstance(pages, list): + pass # keep unchanged + elif isinstance(pages, tuple) and len(pages) <= 3: + pages = list(range(*pages)) + elif not isinstance(pages, tuple): + raise TypeError( + '"pages" must be a tuple of (start, stop[, step]) or a list' + ) + + srcpages = {} + for page in pages: + if isinstance(page, PageObject): + pg = page + else: + pg = reader.pages[page] + assert pg.indirect_reference is not None + if position is None: + # numbers in the exclude list identifies that the exclusion is + # only applicable to 1st level of cloning + srcpages[pg.indirect_reference.idnum] = self.add_page( + pg, [*list(excluded_fields), 1, "/B", 1, "/Annots"] # type: ignore[list-item] + ) + else: + srcpages[pg.indirect_reference.idnum] = self.insert_page( + pg, position, [*list(excluded_fields), 1, "/B", 1, "/Annots"] # type: ignore[list-item] + ) + position += 1 + srcpages[pg.indirect_reference.idnum].original_page = pg + + reader._named_destinations = ( + reader.named_destinations + ) # need for the outline processing below + + arr: Any + + for dest in reader._named_destinations.values(): + self._merge__process_named_dests(dest=dest, reader=reader, srcpages=srcpages) + + outline_item_typ: TreeObject + if outline_item is not None: + outline_item_typ = cast( + "TreeObject", + self.add_outline_item( + TextStringObject(outline_item), + next(iter(srcpages.values())).indirect_reference, + fit=PAGE_FIT, + ).get_object(), + ) + else: + outline_item_typ = self.get_outline_root() + + _ro = reader.root_object + if import_outline and Core.OUTLINES in _ro: + outline = self._get_filtered_outline( + node=_ro.get(Core.OUTLINES, None), pages=srcpages, reader=reader + ) + self._insert_filtered_outline( + outline, outline_item_typ, None + ) # TODO: use before parameter + + if "/Annots" not in excluded_fields: + parent_fields: dict[int, DictionaryObject] = {} + for pag in srcpages.values(): + lst = self._insert_filtered_annotations( + annots=pag.original_page.get("/Annots", []), + page=pag, + pages=srcpages, + reader=reader, + parent_fields=parent_fields, + ) + if len(lst) > 0: + pag[NameObject("/Annots")] = lst + self.clean_page(pag) + self._set_cloned_kids(parent_fields, reader) + + if "/AcroForm" in _ro and not is_null_or_none(_ro["/AcroForm"]): + if "/AcroForm" not in self._root_object: + self._root_object[NameObject("/AcroForm")] = self._add_object( + cast( + DictionaryObject, + reader.root_object["/AcroForm"], + ).clone(self, False, ("/Fields",)) + ) + arr = ArrayObject() + else: + arr = cast( + ArrayObject, + cast(DictionaryObject, self._root_object["/AcroForm"])["/Fields"], + ) + trslat = self._id_translated[id(reader)] + try: + for f in reader.root_object["/AcroForm"]["/Fields"]: # type: ignore[index] + try: + ind = IndirectObject(trslat[f.idnum], 0, self) + if ind not in arr: + arr.append(ind) + except KeyError: + # for trslat[] which mean the field has not be copied + # through the page + pass + except KeyError: # for /Acroform or /Fields are not existing + arr = self._add_object(ArrayObject()) + cast(DictionaryObject, self._root_object["/AcroForm"])[ + NameObject("/Fields") + ] = arr + + if "/B" not in excluded_fields: + self.add_filtered_articles("", srcpages, reader) + + def _merge__process_named_dests(self, dest: Any, reader: PdfDocCommon, srcpages: dict[int, PageObject]) -> None: + arr: Any = dest.dest_array + if "/Names" in self._root_object and dest["/Title"] in cast( + list[Any], + cast( + DictionaryObject, + cast(DictionaryObject, self._root_object["/Names"]).get("/Dests", DictionaryObject()), + ).get("/Names", DictionaryObject()), + ): + # already exists: should not duplicate it + pass + elif dest["/Page"] is None or isinstance(dest["/Page"], NullObject): + pass + elif isinstance(dest["/Page"], int): + # the page reference is a page number normally not a PDF Reference + # page numbers as int are normally accepted only in external goto + try: + p = reader.pages[dest["/Page"]] + except IndexError: + return + assert p.indirect_reference is not None + try: + arr[NumberObject(0)] = NumberObject( + srcpages[p.indirect_reference.idnum].page_number + ) + self.add_named_destination_array(dest["/Title"], arr) + except KeyError: + pass + elif dest["/Page"].indirect_reference.idnum in srcpages: + arr[NumberObject(0)] = srcpages[ + dest["/Page"].indirect_reference.idnum + ].indirect_reference + self.add_named_destination_array(dest["/Title"], arr) + + def _add_articles_thread( + self, + thread: DictionaryObject, + pages: dict[int, PageObject], + reader: PdfReader, + ) -> IndirectObject: + """ + Clone the thread with only the applicable articles. + + Args: + thread: Thread entry from the reader's array of threads + pages: Mapping of object numbers to page objects. + reader: The corresponding reader. + + Returns: + The added thread as an indirect reference + + """ + new_thread = thread.clone( + self, force_duplicate=True, ignore_fields=("/F",) + ) # use of clone to keep link between reader and writer + self.threads.append(new_thread.indirect_reference) + first_article = cast("DictionaryObject", thread["/F"]) + current_article: Optional[DictionaryObject] = first_article + new_article: Optional[DictionaryObject] = None + + visited: set[int] = set() + while current_article is not None: + article_id = id(current_article) + if article_id in visited: + raise LimitReachedError("Detected cyclic article structure.") + visited.add(article_id) + + page = self._get_cloned_page( + cast("PageObject", current_article["/P"]), pages, reader + ) + if page is not None: + if new_article is None: + new_article = cast( + "DictionaryObject", + self._add_object(DictionaryObject()).get_object(), + ) + new_first = new_article + new_thread[NameObject("/F")] = new_article.indirect_reference + else: + new_article2 = cast( + "DictionaryObject", + self._add_object( + DictionaryObject( + {NameObject("/V"): new_article.indirect_reference} + ) + ).get_object(), + ) + new_article[NameObject("/N")] = new_article2.indirect_reference + new_article = new_article2 + new_article[NameObject("/P")] = page + new_article[NameObject("/T")] = new_thread.indirect_reference + new_article[NameObject("/R")] = current_article["/R"] + page_object = cast("PageObject", page.get_object()) + if "/B" not in page_object: + page_object[NameObject("/B")] = ArrayObject() + cast("ArrayObject", page_object["/B"]).append( + new_article.indirect_reference + ) + + current_article = cast("DictionaryObject", current_article["/N"]) + if current_article == first_article: + new_article[NameObject("/N")] = new_first.indirect_reference # type: ignore[index] + new_first[NameObject("/V")] = new_article.indirect_reference # type: ignore[union-attr] + current_article = None + + assert new_thread.indirect_reference is not None + return new_thread.indirect_reference + + def add_filtered_articles( + self, + fltr: Union[ + Pattern[Any], str + ], # thread entry from the reader's array of threads + pages: dict[int, PageObject], + reader: PdfReader, + ) -> None: + """ + Add articles matching the defined criteria. + + Args: + fltr: + pages: + reader: + + """ + if isinstance(fltr, str): + fltr = re.compile(fltr) + elif not isinstance(fltr, Pattern): + fltr = re.compile("") + for p in pages.values(): + pp = p.original_page + for a in pp.get("/B", ()): + a_obj = a.get_object() + if is_null_or_none(a_obj): + continue + thr = a_obj.get("/T") + if thr is None: + continue + thr = thr.get_object() + if thr.indirect_reference.idnum not in self._id_translated[ + id(reader) + ] and fltr.search((thr.get("/I", {})).get("/Title", "")): + self._add_articles_thread(thr, pages, reader) + + def _get_cloned_page( + self, + page: Union[IndirectObject, PageObject, NullObject, int, None], + pages: dict[int, PageObject], + reader: PdfReader, + ) -> Optional[IndirectObject]: + if isinstance(page, NullObject): + return None + if isinstance(page, int): + # An explicit destination may reference the target page by its + # (zero-based) index in the source document rather than by an + # indirect reference; this is what `Link(target_page_index=...)` + # produces. Resolve the index through the reader's page list so it + # can be remapped like a regular page reference. A destination that + # points past the end of the source document is dropped. + try: + page = reader.pages[page].indirect_reference + except IndexError: + return None + if isinstance(page, DictionaryObject) and page.get("/Type", "") == "/Page": + _i = page.indirect_reference + elif isinstance(page, IndirectObject): + _i = page + try: + return pages[_i.idnum].indirect_reference # type: ignore[union-attr] + except Exception: + return None + + def _insert_filtered_annotations( + self, + *, + annots: Union[IndirectObject, list[PdfObject], None], + page: PageObject, + pages: dict[int, PageObject], + reader: PdfReader, + parent_fields: Optional[dict[int, DictionaryObject]] = None, + ) -> list[Destination]: + outlist = ArrayObject() + if isinstance(annots, IndirectObject): + annots = cast("list[Any]", annots.get_object()) + if annots is None: + return outlist + if not isinstance(annots, list): + logger_warning( + "Expected list of annotations, got %(annots)s of type %(annots_type)s.", + source=__name__, + annots=annots, + annots_type=annots.__class__.__name__, + ) + return outlist + for an in annots: + ano = cast("DictionaryObject", an.get_object()) + if ( + ano.get("/Subtype") != "/Link" + or "/A" not in ano + or cast("DictionaryObject", ano["/A"])["/S"] != "/GoTo" # type: ignore[comparison-overlap] + or "/Dest" in ano + ): + if ( + parent_fields is not None + and ano.get("/Subtype") == "/Widget" + and isinstance(ano.get("/Parent"), IndirectObject) + ): + outlist.append(self._clone_widget_annotation(ano, parent_fields)) + elif "/Dest" not in ano: + outlist.append(self._add_object(ano.clone(self))) + else: + d = ano["/Dest"] + if isinstance(d, str): + # it is a named dest + if str(d) in self.get_named_dest_root(): + outlist.append(ano.clone(self).indirect_reference) + else: + if not isinstance(d, ArrayObject) or not d: + continue + p = self._get_cloned_page(d[0], pages, reader) + if p is not None: + anc = ano.clone(self, ignore_fields=("/Dest",)) + anc[NameObject("/Dest")] = ArrayObject([p, *d[1:]]) + outlist.append(self._add_object(anc)) + else: + d = cast("DictionaryObject", ano["/A"]).get("/D", NullObject()) + if is_null_or_none(d): + continue + if isinstance(d, str): + # it is a named dest + if str(d) in self.get_named_dest_root(): + outlist.append(ano.clone(self).indirect_reference) + else: + if not isinstance(d, ArrayObject) or not d: + continue + p = self._get_cloned_page(d[0], pages, reader) + if p is not None: + anc = ano.clone(self, ignore_fields=("/D",)) + cast("DictionaryObject", anc["/A"])[ + NameObject("/D") + ] = ArrayObject([p, *d[1:]]) + outlist.append(self._add_object(anc)) + return outlist + + def _clone_widget_annotation( + self, annotation: DictionaryObject, parent_fields: dict[int, DictionaryObject] + ) -> IndirectObject: + """ + Clone a widget annotation and link it to its parent fields. + + Cloning ``/Parent`` directly would also clone the ``/Kids`` of the + parent fields, and with them the widgets and pages of sibling fields + which are not part of the merge. Instead, the parent fields are cloned + without ``/Kids`` and collected in ``parent_fields``, so that their kids + can be set by :meth:`_set_cloned_kids` once all annotations are cloned. + """ + clone = annotation.clone(self, ignore_fields=(1, "/Parent")) + source, target = annotation, clone + while isinstance(source.get("/Parent"), IndirectObject): + parent_reference = cast(IndirectObject, source.get("/Parent")) + source_parent = parent_reference.get_object() + if not isinstance(source_parent, DictionaryObject): + break + parent = source_parent.clone(self, ignore_fields=(1, "/Kids", 1, "/Parent")) + target[NameObject("/Parent")] = parent.indirect_reference + if parent_reference.idnum in parent_fields: + # The ancestors have already been linked. + break + parent_fields[parent_reference.idnum] = source_parent + source, target = source_parent, parent + assert clone.indirect_reference is not None + return clone.indirect_reference + + def _set_cloned_kids(self, parent_fields: dict[int, DictionaryObject], reader: PdfReader) -> None: + """Set the ``/Kids`` of the cloned parent fields to the cloned kids, in their original order.""" + translated = self._id_translated.get(id(reader), {}) + for idnum, source_parent in parent_fields.items(): + kids = source_parent.get("/Kids", ArrayObject()) + parent = cast(DictionaryObject, self.get_object(translated[idnum])) + parent[NameObject("/Kids")] = ArrayObject( + IndirectObject(translated[kid.idnum], 0, self) + for kid in cast(ArrayObject, kids) + if isinstance(kid, IndirectObject) and kid.idnum in translated + ) + + def _get_filtered_outline( + self, + *, + node: Any, + pages: dict[int, PageObject], + reader: PdfReader, + visited: Optional[set[int]] = None, + ) -> list[Destination]: + """ + Extract outline item entries that are part of the specified page set. + + Returns: + A list of destination objects. + + """ + if visited is None: + visited = set() + new_outline: list[Destination] = [] + if node is None: + return new_outline + node = node.get_object() + if is_null_or_none(node): + node = DictionaryObject() + + if node.get("/Type", "") == "/Outlines" or "/Title" not in node: + node_id = id(node) + if node_id in visited: + logger_warning("Detected cycle in outlines.", source=__name__) + return [] + visited.add(node_id) + + node = node.get("/First", None) + if node is not None: + node = node.get_object() + new_outline += self._get_filtered_outline(node=node, pages=pages, reader=reader, visited=visited) + else: + cloned_page: Union[IndirectObject, NullObject, None] + while True: + node = node.get_object() + node_id = id(node) + if node_id in visited: + logger_warning("Detected cycle in outlines.", source=__name__) + break + visited.add(node_id) + + destination = cast("Destination", reader._build_outline_item(node)) + cloned_page = self._get_cloned_page(cast("PageObject", destination["/Page"]), pages, reader) + if cloned_page is None: + cloned_page = NullObject() + destination[NameObject("/Page")] = cloned_page + if "/First" in node: + destination._filtered_children = self._get_filtered_outline( + node=node["/First"], pages=pages, reader=reader, visited=visited + ) + else: + destination._filtered_children = [] + if ( + not isinstance(cloned_page, NullObject) + or len(destination._filtered_children) > 0 + ): + new_outline.append(destination) + + if "/Next" not in node: + break + node = node["/Next"] + return new_outline + + def _clone_outline(self, dest: Destination) -> TreeObject: + n_ol = TreeObject() + self._add_object(n_ol) + n_ol[NameObject("/Title")] = TextStringObject(dest["/Title"]) + if not isinstance(dest["/Page"], NullObject): + if dest.node is not None and "/A" in dest.node: + n_ol[NameObject("/A")] = dest.node["/A"].clone(self) + else: + n_ol[NameObject("/Dest")] = dest.dest_array + # TODO: /SE + if dest.node is not None: + n_ol[NameObject("/F")] = NumberObject(dest.node.get("/F", 0)) + color = dest.node.get("/C", NullObject()).get_object() + if not isinstance(color, list): + color = [FloatObject(0.0), FloatObject(0.0), FloatObject(0.0)] + n_ol[NameObject("/C")] = ArrayObject(color) + return n_ol + + def _insert_filtered_outline( + self, + outlines: list[Destination], + parent: Union[TreeObject, IndirectObject], + before: Union[TreeObject, IndirectObject, None] = None, + ) -> None: + for dest in outlines: + # TODO: can be improved to keep A and SE entries (ignored for the moment) + # with np=self.add_outline_item_destination(dest,parent,before) + if dest.get("/Type", "") == "/Outlines" or "/Title" not in dest: + np = parent + else: + np = self._clone_outline(dest) + cast(TreeObject, parent.get_object()).insert_child(np, before, self) + self._insert_filtered_outline(dest._filtered_children, np, None) + + def close(self) -> None: + """Implemented for API harmonization.""" + return + + def find_outline_item( + self, + outline_item: dict[str, Any], + root: Optional[OutlineType] = None, + ) -> Optional[list[int]]: + if root is None: + o = self.get_outline_root() + else: + o = cast("TreeObject", root) + + i = 0 + while o is not None: + if ( + o.indirect_reference == outline_item + or o.get("/Title", None) == outline_item + ): + return [i] + if "/First" in o: + res = self.find_outline_item( + outline_item, cast(OutlineType, o["/First"]) + ) + if res: + return ([i] if "/Title" in o else []) + res + if "/Next" in o: + i += 1 + o = cast(TreeObject, o["/Next"]) + else: + return None + raise PyPdfError("This line is theoretically unreachable.") # pragma: no cover + + def reset_translation( + self, reader: Union[PdfReader, IndirectObject, None] = None + ) -> None: + """ + Reset the translation table between reader and the writer object. + + Late cloning will create new independent objects. + + Args: + reader: PdfReader or IndirectObject referencing a PdfReader object. + if set to None or omitted, all tables will be reset. + + """ + if reader is None: + self._id_translated = {} + elif isinstance(reader, PdfReader): + try: + del self._id_translated[id(reader)] + except Exception: + pass + elif isinstance(reader, IndirectObject): + try: + del self._id_translated[id(reader.pdf)] + except Exception: + pass + else: + raise TypeError(f"Invalid parameter {reader}") + + def set_page_label( + self, + page_index_from: int, + page_index_to: int, + style: Optional[PageLabelStyle] = None, + prefix: Optional[str] = None, + start: Optional[int] = 0, + ) -> None: + """ + Set a page label to a range of pages. + + Page indexes must be given starting from 0. + Labels must have a style, a prefix or both. + If a range is not assigned any page label, a decimal label starting from 1 is applied. + + Args: + page_index_from: page index of the beginning of the range starting from 0 + page_index_to: page index of the beginning of the range starting from 0 + style: The numbering style to be used for the numeric portion of each page label: + + * ``/D`` Decimal Arabic numerals + * ``/R`` Uppercase Roman numerals + * ``/r`` Lowercase Roman numerals + * ``/A`` Uppercase letters (A to Z for the first 26 pages, + AA to ZZ for the next 26, and so on) + * ``/a`` Lowercase letters (a to z for the first 26 pages, + aa to zz for the next 26, and so on) + + prefix: The label prefix for page labels in this range. + start: The value of the numeric portion for the first page label + in the range. + Subsequent pages are numbered sequentially from this value, + which must be greater than or equal to 1. + Default value: 1. + + """ + if style is None and prefix is None: + raise ValueError("At least one of style and prefix must be given") + if style is not None and style not in tuple(PageLabelStyle): + raise ValueError( + f"style must be one of: {', '.join(PageLabelStyle)}, got {style!r}" + ) + if page_index_from < 0: + raise ValueError("page_index_from must be greater or equal than 0") + if page_index_to < page_index_from: + raise ValueError( + "page_index_to must be greater or equal than page_index_from" + ) + if page_index_to >= len(self.pages): + raise ValueError("page_index_to exceeds number of pages") + if start is not None and start != 0 and start < 1: + raise ValueError("If given, start must be greater or equal than one") + + self._set_page_label(page_index_from, page_index_to, style, prefix, start) + + def _set_page_label( + self, + page_index_from: int, + page_index_to: int, + style: Optional[PageLabelStyle] = None, + prefix: Optional[str] = None, + start: Optional[int] = 0, + ) -> None: + """ + Set a page label to a range of pages. + + Page indexes must be given starting from 0. + Labels must have a style, a prefix or both. + If a range is not assigned any page label a decimal label starting from 1 is applied. + + Args: + page_index_from: page index of the beginning of the range starting from 0 + page_index_to: page index of the beginning of the range starting from 0 + style: The numbering style to be used for the numeric portion of each page label: + /D Decimal Arabic numerals + /R Uppercase Roman numerals + /r Lowercase Roman numerals + /A Uppercase letters (A to Z for the first 26 pages, + AA to ZZ for the next 26, and so on) + /a Lowercase letters (a to z for the first 26 pages, + aa to zz for the next 26, and so on) + prefix: The label prefix for page labels in this range. + start: The value of the numeric portion for the first page label + in the range. + Subsequent pages are numbered sequentially from this value, + which must be greater than or equal to 1. Default value: 1. + + """ + default_page_label = DictionaryObject() + default_page_label[NameObject("/S")] = NameObject("/D") + + new_page_label = DictionaryObject() + if style is not None: + new_page_label[NameObject("/S")] = NameObject(style) + if prefix is not None: + new_page_label[NameObject("/P")] = TextStringObject(prefix) + if start != 0: + new_page_label[NameObject("/St")] = NumberObject(start) + + if NameObject(CatalogAttributes.PAGE_LABELS) not in self._root_object: + nums = ArrayObject() + nums_insert(NumberObject(0), default_page_label, nums) + page_labels = TreeObject() + page_labels[NameObject("/Nums")] = nums + self._root_object[NameObject(CatalogAttributes.PAGE_LABELS)] = page_labels + + page_labels = cast( + TreeObject, self._root_object[NameObject(CatalogAttributes.PAGE_LABELS)] + ) + nums = cast(ArrayObject, page_labels[NameObject("/Nums")]) + + nums_insert(NumberObject(page_index_from), new_page_label, nums) + nums_clear_range(NumberObject(page_index_from), page_index_to, nums) + next_label_pos, *_ = nums_next(NumberObject(page_index_from), nums) + if next_label_pos != page_index_to + 1 and page_index_to + 1 < len(self.pages): + nums_insert(NumberObject(page_index_to + 1), default_page_label, nums) + + page_labels[NameObject("/Nums")] = nums + self._root_object[NameObject(CatalogAttributes.PAGE_LABELS)] = page_labels + + def _repr_mimebundle_( + self, + include: Union[Iterable[str], None] = None, + exclude: Union[Iterable[str], None] = None, + ) -> dict[str, Any]: + """ + Integration into Jupyter Notebooks. + + This method returns a dictionary that maps a mime-type to its + representation. + + .. seealso:: + + https://ipython.readthedocs.io/en/stable/config/integrating.html + """ + pdf_data = BytesIO() + self.write(pdf_data) + data = { + "application/pdf": pdf_data, + } + + if include is not None: + # Filter representations based on include list + data = {k: v for k, v in data.items() if k in include} + + if exclude is not None: + # Remove representations based on exclude list + data = {k: v for k, v in data.items() if k not in exclude} + + return data + + +def _pdf_objectify(obj: Union[dict[str, Any], str, float, list[Any]]) -> PdfObject: + if isinstance(obj, PdfObject): + return obj + if isinstance(obj, dict): + to_add = DictionaryObject() + for key, value in obj.items(): + to_add[NameObject(key)] = _pdf_objectify(value) + return to_add + if isinstance(obj, str): + if obj.startswith("/"): + return NameObject(obj) + return TextStringObject(obj) + if isinstance(obj, (float, int)): + return FloatObject(obj) + if isinstance(obj, list): + return ArrayObject(_pdf_objectify(i) for i in obj) + raise NotImplementedError( + f"{type(obj)=} could not be cast to a PdfObject" + ) + + +def _create_outline_item( + action_ref: Union[IndirectObject, None], + title: str, + color: Union[tuple[float, float, float], str, None], + italic: bool, + bold: bool, +) -> TreeObject: + outline_item = TreeObject() + if action_ref is not None: + outline_item[NameObject("/A")] = action_ref + outline_item.update( + { + NameObject("/Title"): create_string_object(title), + } + ) + if color: + if isinstance(color, str): + color = hex_to_rgb(color) + outline_item.update( + {NameObject("/C"): ArrayObject([FloatObject(c) for c in color])} + ) + if italic or bold: + format_flag = 0 + if italic: + format_flag += OutlineFontFlag.italic + if bold: + format_flag += OutlineFontFlag.bold + outline_item.update({NameObject("/F"): NumberObject(format_flag)}) + return outline_item diff --git a/app/vendor/pypdf/actions/__init__.py b/app/vendor/pypdf/actions/__init__.py new file mode 100644 index 0000000..5cb5ce5 --- /dev/null +++ b/app/vendor/pypdf/actions/__init__.py @@ -0,0 +1,17 @@ +""" +PDF includes a wide variety of standard action types, whose characteristics and +behaviour are defined by an action dictionary. These are defined in this +submodule. + +Trigger events are the other component of actions, and are specific to their +associated object. +""" + + +from ._actions import Action, JavaScript, PageTrigger + +__all__ = [ + "Action", + "JavaScript", + "PageTrigger", +] diff --git a/app/vendor/pypdf/actions/_actions.py b/app/vendor/pypdf/actions/_actions.py new file mode 100644 index 0000000..8c68c35 --- /dev/null +++ b/app/vendor/pypdf/actions/_actions.py @@ -0,0 +1,217 @@ +"""Action types""" +import sys +import uuid +from abc import ABC +from enum import Enum, unique +from typing import ( + TYPE_CHECKING, + cast, +) + +from .._utils import logger_warning +from ..constants import CatalogAttributes +from ..errors import ParseError +from ..generic import ( + ArrayObject, + DictionaryObject, + NameObject, + NullObject, + TextStringObject, + create_string_object, + is_null_or_none, +) + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + class StrEnum(str, Enum): + def __str__(self) -> str: + return str(self.value) + +if TYPE_CHECKING: + from .._page import PageObject + from .._writer import PdfWriter + + +@unique +class PageTrigger(StrEnum): + """Trigger event entries in a page object's additional-actions dictionary.""" + OPEN = "open" + """Trigger an action when the page is opened.""" + CLOSE = "close" + """Trigger an action when the page is closed.""" + + @property + def _name_object(self) -> NameObject: + """Returns the corresponding NameObject for the trigger.""" + mapping = { + PageTrigger.OPEN: NameObject("/O"), + PageTrigger.CLOSE: NameObject("/C"), + } + return mapping[self] + + +class Action(DictionaryObject, ABC): + """An action dictionary defines the characteristics and behaviour of an action.""" + def __init__(self) -> None: + super().__init__() + self[NameObject("/Type")] = NameObject("/Action") + # The next action or sequence of actions that shall be performed after the action + # represented by this dictionary. The value is either a single action dictionary + # or an array of action dictionaries that shall be performed in order. + self[NameObject("/Next")] = NullObject() # Optional + + @classmethod + def _create_open_action(cls, writer: "PdfWriter", action: "Action") -> None: + """ + Create an action that shall be performed when the document is opened. + + Args: + writer: The writer to add the action to. + action: The action to add. + """ + if "/Names" not in writer.root_object: + writer.root_object[NameObject(CatalogAttributes.NAMES)] = DictionaryObject() + + names = cast(DictionaryObject, writer.root_object[CatalogAttributes.NAMES]) + if "/JavaScript" not in names: + names[NameObject("/JavaScript")] = DictionaryObject( + {NameObject("/Names"): ArrayObject()} + ) + + js = cast( + ArrayObject, cast(DictionaryObject, names["/JavaScript"])["/Names"] + ) + + # We need a name for parameterized JavaScript in the PDF file, but it can be anything + js.append(create_string_object(str(uuid.uuid4()))) + js.append(writer._add_object(action)) + + @classmethod + def _create_new(cls, page: "PageObject", trigger: PageTrigger, action: "Action") -> None: + """ + Create a new action and add it to the page. + + Args: + page: The page to add the action to. + trigger: An open or close trigger. + action: The action to be done. + """ + trigger_name = trigger._name_object + + if "/AA" not in page: + # Additional actions key not present + page[NameObject("/AA")] = DictionaryObject( + {trigger_name: action} + ) + return + + if isinstance(page["/AA"], NullObject): + page[NameObject("/AA")] = DictionaryObject() + + if not isinstance(page["/AA"].get_object(), DictionaryObject): + current_type = type(page["/AA"]).__name__ + if page.pdf is not None and getattr(page.pdf, "strict", False): + raise ParseError( + f"The type in a page object's additional-actions key must be a DictionaryObject: " + f"received type {current_type}." + ) + logger_warning( + "The type in a page object's additional-actions key must be a DictionaryObject: " + "received type %(type)s.", + source=__name__, + type=current_type + ) + return + + additional_actions = cast(DictionaryObject, page["/AA"]) + + if is_null_or_none(additional_actions.get(trigger_name)): + additional_actions.update({trigger_name: action}) + return + + # The action dictionary's Next entry allows sequences of actions to be + # chained together. For example, the effect of clicking a link + # annotation with the mouse can be to play a sound, jump to a new + # page, and start up a movie. Note that the Next entry is not + # restricted to a single action but can contain an array of actions, + # each of which in turn can have a Next entry of its own. + # §12.6.2 Action dictionaries ISO 32000-2:2020 + head = current = additional_actions.get(trigger_name) + if not isinstance(head, DictionaryObject): + raise ParseError( + f"The type in a page object's additional-actions key must be a DictionaryObject: " + f"received type {type(head).__name__}." + ) + current = cast(DictionaryObject, current) + + visited = set() + while True: + next_node = current.get("/Next", None) + + if is_null_or_none(next_node): + break + + if not isinstance(next_node, (ArrayObject, DictionaryObject)): + raise TypeError( + f"An action dictionary’s Next entry must be an Action dictionary " + f"or an array of Action dictionaries: received type {type(next_node).__name__}." + ) + + id_next = id(next_node) + if id_next in visited: + logger_warning("Detected cycle in the action tree for %(current)s", source=__name__, current=current) + break + visited.add(id_next) + + if isinstance(next_node, ArrayObject): + current = next_node[-1] + else: + current = next_node + + if not is_null_or_none(next_node := current.get("/Next")) and id(next_node) in visited: + logger_warning("Detected cycle in the action tree for %(current)s", source=__name__, current=current) + + current[NameObject("/Next")] = action + additional_actions.update({trigger_name: head}) + + @classmethod + def _delete(cls, page: "PageObject", trigger: PageTrigger) -> None: + """ + Delete an action from the page. + + Args: + page: The page to delete the action. + trigger: An open or close trigger. + """ + if "/AA" not in page: + return + + trigger_name = trigger._name_object + + additional_actions = cast(DictionaryObject, page["/AA"]) + + if trigger_name not in additional_actions: + return + + del additional_actions[trigger_name] + + if not additional_actions: + del page["/AA"] + + +class JavaScript(Action): + """ + Upon invocation of an ECMAScript action, a PDF processor shall execute a + script that is written in the ECMAScript programming language. ECMAScript + extensions described in ISO/DIS 21757-1 shall also be allowed. + + Args: + java_script: A text string containing the ECMAScript script to be executed. + """ + + def __init__(self, java_script: str) -> None: + """Initialize JavaScript with a string.""" + super().__init__() + self[NameObject("/S")] = NameObject("/JavaScript") + self[NameObject("/JS")] = TextStringObject(java_script) diff --git a/app/vendor/pypdf/annotations/__init__.py b/app/vendor/pypdf/annotations/__init__.py new file mode 100644 index 0000000..44ed1da --- /dev/null +++ b/app/vendor/pypdf/annotations/__init__.py @@ -0,0 +1,42 @@ +""" +PDF specifies several annotation types which pypdf makes available here. + +The names of the annotations and their attributes do not reflect the names in +the specification in all cases. For example, the PDF standard defines a +'Square' annotation that does not actually need to be square. For this reason, +pypdf calls it 'Rectangle'. + +At their core, all annotation types are DictionaryObjects. That means if pypdf +does not implement a feature, users can easily extend the given functionality. +""" + + +from ._base import NO_FLAGS, AnnotationDictionary +from ._markup_annotations import ( + Ellipse, + FreeText, + Highlight, + Line, + MarkupAnnotation, + Polygon, + PolyLine, + Rectangle, + Text, +) +from ._non_markup_annotations import Link, Popup + +__all__ = [ + "NO_FLAGS", + "AnnotationDictionary", + "Ellipse", + "FreeText", + "Highlight", + "Line", + "Link", + "MarkupAnnotation", + "PolyLine", + "Polygon", + "Popup", + "Rectangle", + "Text", +] diff --git a/app/vendor/pypdf/annotations/_base.py b/app/vendor/pypdf/annotations/_base.py new file mode 100644 index 0000000..7731b18 --- /dev/null +++ b/app/vendor/pypdf/annotations/_base.py @@ -0,0 +1,29 @@ +from abc import ABC + +from ..constants import AnnotationFlag +from ..generic import NameObject, NumberObject +from ..generic._data_structures import DictionaryObject + + +class AnnotationDictionary(DictionaryObject, ABC): + def __init__(self) -> None: + super().__init__() + + from ..generic._base import NameObject # noqa: PLC0415 + + # /Rect should not be added here as Polygon and PolyLine can automatically set it + self[NameObject("/Type")] = NameObject("/Annot") + # The flags were NOT added to the constructor on purpose: + # We expect that most users don't want to change the default. + # If they do, they can use the property. The default is 0. + + @property + def flags(self) -> AnnotationFlag: + return AnnotationFlag(self.get(NameObject("/F"), 0)) + + @flags.setter + def flags(self, value: AnnotationFlag) -> None: + self[NameObject("/F")] = NumberObject(value) + + +NO_FLAGS = AnnotationFlag(0) diff --git a/app/vendor/pypdf/annotations/_markup_annotations.py b/app/vendor/pypdf/annotations/_markup_annotations.py new file mode 100644 index 0000000..7f7d8dd --- /dev/null +++ b/app/vendor/pypdf/annotations/_markup_annotations.py @@ -0,0 +1,351 @@ +import sys +import uuid +from abc import ABC +from typing import Any, Literal, Optional, Union + +from ..constants import AnnotationFlag +from ..generic import ArrayObject, DictionaryObject +from ..generic._base import ( + BooleanObject, + FloatObject, + IndirectObject, + NameObject, + NumberObject, + TextStringObject, +) +from ..generic._rectangle import RectangleObject +from ..generic._utils import hex_to_rgb +from ._base import NO_FLAGS, AnnotationDictionary + +if sys.version_info[:2] >= (3, 10): + from typing import TypeAlias +else: + # PEP 613 introduced typing.TypeAlias with Python 3.10 + # For older Python versions, the backport typing_extensions is necessary: + from typing_extensions import TypeAlias + + +Vertex: TypeAlias = tuple[float, float] + + +def _get_bounding_rectangle(vertices: list[Vertex]) -> RectangleObject: + x_min, y_min = vertices[0][0], vertices[0][1] + x_max, y_max = vertices[0][0], vertices[0][1] + for x, y in vertices: + x_min = min(x_min, x) + y_min = min(y_min, y) + x_max = max(x_max, x) + y_max = max(y_max, y) + return RectangleObject((x_min, y_min, x_max, y_max)) + + +class MarkupAnnotation(AnnotationDictionary, ABC): + """ + Base class for all markup annotations. + + Args: + title_bar: Text to be displayed in the title bar of the annotation; + by convention this is the name of the author + in_reply_to: The annotation that this annotation is "in reply to" + (PDF 1.5). Can be either an annotation (previously added using + :meth:`~pypdf.PdfWriter.add_annotation`) or a reference to the + target annotation. + reply_type: The relationship between this annotation and the one + specified by ``in_reply_to``. Either ``"R"`` (a reply, default) + or ``"Group"`` (grouped with the parent annotation). Raises + ``ValueError`` if a non-default value is provided without + ``in_reply_to``. + annotation_name: A text string uniquely identifying this annotation + among all annotations on its page. Automatically generated when + ``in_reply_to`` is set and no name is provided. Raises + ``ValueError`` if provided without ``in_reply_to``. + + """ + + def __init__( + self, + *, + title_bar: Optional[str] = None, + in_reply_to: Optional[Union[DictionaryObject, IndirectObject]] = None, + reply_type: Literal["R", "Group"] = "R", + annotation_name: Optional[str] = None, + ) -> None: + if title_bar is not None: + self[NameObject("/T")] = TextStringObject(title_bar) + if annotation_name is not None and in_reply_to is None: + raise ValueError( + "annotation_name is only supported when in_reply_to is set" + ) + if reply_type != "R" and in_reply_to is None: + raise ValueError( + "reply_type is only meaningful when in_reply_to is set" + ) + if in_reply_to is not None: + if isinstance(in_reply_to, IndirectObject): + ref: IndirectObject = in_reply_to + else: + indirect_ref = getattr(in_reply_to, "indirect_reference", None) + if not isinstance(indirect_ref, IndirectObject): + raise ValueError( + "in_reply_to must be a registered annotation " + "(added via writer.add_annotation() first)" + ) + ref = indirect_ref + self[NameObject("/IRT")] = ref + self[NameObject("/RT")] = NameObject(f"/{reply_type}") + if annotation_name is None: + annotation_name = str(uuid.uuid4()) + self[NameObject("/NM")] = TextStringObject(annotation_name) + + +class Text(MarkupAnnotation): + """ + A text annotation. + + Args: + rect: array of four integers ``[xLL, yLL, xUR, yUR]`` + specifying the clickable rectangular area + text: The text that is added to the document + open: + flags: + + """ + + def __init__( + self, + *, + rect: Union[RectangleObject, tuple[float, float, float, float]], + text: str, + open: bool = False, + flags: int = NO_FLAGS, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self[NameObject("/Subtype")] = NameObject("/Text") + self[NameObject("/Rect")] = RectangleObject(rect) + self[NameObject("/Contents")] = TextStringObject(text) + self[NameObject("/Open")] = BooleanObject(open) + self[NameObject("/Flags")] = NumberObject(flags) + + +class FreeText(MarkupAnnotation): + """A FreeText annotation""" + + def __init__( + self, + *, + text: str, + rect: Union[RectangleObject, tuple[float, float, float, float]], + font: str = "Helvetica", + bold: bool = False, + italic: bool = False, + font_size: str = "14pt", + font_color: str = "000000", + border_color: Optional[str] = "000000", + background_color: Optional[str] = "ffffff", + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self[NameObject("/Subtype")] = NameObject("/FreeText") + self[NameObject("/Rect")] = RectangleObject(rect) + + # Table 225 of the 1.7 reference ("CSS2 style attributes used in rich text strings") + font_str = "font: " + if italic: + font_str = f"{font_str}italic " + else: + font_str = f"{font_str}normal " + if bold: + font_str = f"{font_str}bold " + else: + font_str = f"{font_str}normal " + font_str = f"{font_str}{font_size} {font}" + font_str = f"{font_str};text-align:left;color:#{font_color}" + + default_appearance_string = "" + if font_color: + for st in hex_to_rgb(font_color): + default_appearance_string = f"{default_appearance_string}{st} " + default_appearance_string = f"{default_appearance_string}rg" + + self.update( + { + NameObject("/Subtype"): NameObject("/FreeText"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Contents"): TextStringObject(text), + # font size color + NameObject("/DS"): TextStringObject(font_str), + NameObject("/DA"): TextStringObject(default_appearance_string), + } + ) + if border_color is None: + # Border Style + self[NameObject("/BS")] = DictionaryObject( + { + # width of 0 means no border + NameObject("/W"): NumberObject(0) + } + ) + if background_color is not None: + self[NameObject("/C")] = ArrayObject( + [FloatObject(n) for n in hex_to_rgb(background_color)] + ) + + +class Line(MarkupAnnotation): + def __init__( + self, + p1: Vertex, + p2: Vertex, + rect: Union[RectangleObject, tuple[float, float, float, float]], + text: str = "", + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.update( + { + NameObject("/Subtype"): NameObject("/Line"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/L"): ArrayObject( + [ + FloatObject(p1[0]), + FloatObject(p1[1]), + FloatObject(p2[0]), + FloatObject(p2[1]), + ] + ), + NameObject("/LE"): ArrayObject( + [ + NameObject("/None"), + NameObject("/None"), + ] + ), + NameObject("/IC"): ArrayObject( + [ + FloatObject(0.5), + FloatObject(0.5), + FloatObject(0.5), + ] + ), + NameObject("/Contents"): TextStringObject(text), + } + ) + + +class PolyLine(MarkupAnnotation): + def __init__( + self, + vertices: list[Vertex], + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + if len(vertices) == 0: + raise ValueError("A polyline needs at least 1 vertex with two coordinates") + coord_list = [] + for x, y in vertices: + coord_list.append(NumberObject(x)) + coord_list.append(NumberObject(y)) + self.update( + { + NameObject("/Subtype"): NameObject("/PolyLine"), + NameObject("/Vertices"): ArrayObject(coord_list), + NameObject("/Rect"): RectangleObject(_get_bounding_rectangle(vertices)), + } + ) + + +class Rectangle(MarkupAnnotation): + def __init__( + self, + rect: Union[RectangleObject, tuple[float, float, float, float]], + *, + interior_color: Optional[str] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.update( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Square"), + NameObject("/Rect"): RectangleObject(rect), + } + ) + + if interior_color: + self[NameObject("/IC")] = ArrayObject( + [FloatObject(n) for n in hex_to_rgb(interior_color)] + ) + + +class Highlight(MarkupAnnotation): + def __init__( + self, + *, + rect: Union[RectangleObject, tuple[float, float, float, float]], + quad_points: ArrayObject, + highlight_color: str = "ff0000", + printing: bool = False, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.update( + { + NameObject("/Subtype"): NameObject("/Highlight"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/QuadPoints"): quad_points, + NameObject("/C"): ArrayObject( + [FloatObject(n) for n in hex_to_rgb(highlight_color)] + ), + } + ) + if printing: + self.flags = AnnotationFlag.PRINT + + +class Ellipse(MarkupAnnotation): + def __init__( + self, + rect: Union[RectangleObject, tuple[float, float, float, float]], + *, + interior_color: Optional[str] = None, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + + self.update( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Circle"), + NameObject("/Rect"): RectangleObject(rect), + } + ) + + if interior_color: + self[NameObject("/IC")] = ArrayObject( + [FloatObject(n) for n in hex_to_rgb(interior_color)] + ) + + +class Polygon(MarkupAnnotation): + def __init__( + self, + vertices: list[tuple[float, float]], + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + if len(vertices) == 0: + raise ValueError("A polygon needs at least 1 vertex with two coordinates") + + coord_list = [] + for x, y in vertices: + coord_list.append(NumberObject(x)) + coord_list.append(NumberObject(y)) + self.update( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Polygon"), + NameObject("/Vertices"): ArrayObject(coord_list), + NameObject("/IT"): NameObject("/PolygonCloud"), + NameObject("/Rect"): RectangleObject(_get_bounding_rectangle(vertices)), + } + ) diff --git a/app/vendor/pypdf/annotations/_non_markup_annotations.py b/app/vendor/pypdf/annotations/_non_markup_annotations.py new file mode 100644 index 0000000..d6a8700 --- /dev/null +++ b/app/vendor/pypdf/annotations/_non_markup_annotations.py @@ -0,0 +1,107 @@ +from collections.abc import Sequence +from typing import TYPE_CHECKING, Any, Optional, Union + +from ..generic._base import ( + BooleanObject, + NameObject, + NumberObject, + TextStringObject, +) +from ..generic._data_structures import ArrayObject, DictionaryObject +from ..generic._fit import DEFAULT_FIT, Fit +from ..generic._rectangle import RectangleObject +from ._base import AnnotationDictionary + + +class Link(AnnotationDictionary): + def __init__( + self, + *, + rect: Union[RectangleObject, tuple[float, float, float, float]], + border: Optional[Sequence[Any]] = None, + url: Optional[str] = None, + target_page_index: Optional[int] = None, + fit: Fit = DEFAULT_FIT, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + if TYPE_CHECKING: + from ..types import BorderArrayType # noqa: PLC0415 + + is_external = url is not None + is_internal = target_page_index is not None + if not is_external and not is_internal: + raise ValueError( + "Either 'url' or 'target_page_index' have to be provided. Both were None." + ) + if is_external and is_internal: + raise ValueError( + "Either 'url' or 'target_page_index' have to be provided. " + f"{url=}, {target_page_index=}" + ) + + border_arr: BorderArrayType + if border is not None: + border_arr = [NumberObject(n) for n in border[:3]] + if len(border) == 4: + dash_pattern = ArrayObject([NumberObject(n) for n in border[3]]) + border_arr.append(dash_pattern) + else: + border_arr = [NumberObject(0)] * 3 + + self.update( + { + NameObject("/Type"): NameObject("/Annot"), + NameObject("/Subtype"): NameObject("/Link"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Border"): ArrayObject(border_arr), + } + ) + if is_external: + self[NameObject("/A")] = DictionaryObject( + { + NameObject("/S"): NameObject("/URI"), + NameObject("/Type"): NameObject("/Action"), + NameObject("/URI"): TextStringObject(url), + } + ) + if is_internal: + # This needs to be updated later! + dest_deferred = DictionaryObject( + { + "target_page_index": NumberObject(target_page_index), + "fit": NameObject(fit.fit_type), + "fit_args": fit.fit_args, + } + ) + self[NameObject("/Dest")] = dest_deferred + + +class Popup(AnnotationDictionary): + def __init__( + self, + *, + rect: Union[RectangleObject, tuple[float, float, float, float]], + parent: Optional[DictionaryObject] = None, + open: bool = False, + **kwargs: Any, + ) -> None: + super().__init__(**kwargs) + self.update( + { + NameObject("/Subtype"): NameObject("/Popup"), + NameObject("/Rect"): RectangleObject(rect), + NameObject("/Open"): BooleanObject(open), + } + ) + if parent: + # This needs to be an indirect object + try: + self[NameObject("/Parent")] = parent.indirect_reference + except AttributeError: + from .._utils import logger_warning # noqa: PLC0415 + + logger_warning( + "Unregistered Parent object : No Parent field set", + source=__name__, + ) diff --git a/app/vendor/pypdf/constants.py b/app/vendor/pypdf/constants.py new file mode 100644 index 0000000..3c2bd7c --- /dev/null +++ b/app/vendor/pypdf/constants.py @@ -0,0 +1,828 @@ +"""Various constants, enums, and flags to aid readability.""" + +import sys +from enum import Enum, IntFlag, auto, unique +from typing import Any + +from ._utils import deprecate_with_replacement + +if sys.version_info >= (3, 11): + from enum import StrEnum +else: + class StrEnum(str, Enum): + def __str__(self) -> str: + return str(self.value) + + +class Core: + """Keywords that don't quite belong anywhere else.""" + + OUTLINES = "/Outlines" + THREADS = "/Threads" + PAGE = "/Page" + PAGES = "/Pages" + CATALOG = "/Catalog" + + +class TrailerKeys: + SIZE = "/Size" + PREV = "/Prev" + ROOT = "/Root" + ENCRYPT = "/Encrypt" + INFO = "/Info" + ID = "/ID" + + +class CatalogAttributes: + """§7.7.2 of the 1.7 and 2.0 references.""" + TYPE = "/Type" # name, required; must be /Catalog + VERSION = "/Version" # name + EXTENSIONS = "/Extensions" # dictionary, optional; ISO 32000-1 + PAGES = "/Pages" # dictionary, required + PAGE_LABELS = "/PageLabels" # number tree, optional + NAMES = "/Names" # dictionary, optional + DESTS = "/Dests" # dictionary, optional + VIEWER_PREFERENCES = "/ViewerPreferences" # dictionary, optional + PAGE_LAYOUT = "/PageLayout" # name, optional + PAGE_MODE = "/PageMode" # name, optional + OUTLINES = "/Outlines" # dictionary, optional + THREADS = "/Threads" # array, optional + OPEN_ACTION = "/OpenAction" # array or dictionary or name, optional + AA = "/AA" # dictionary, optional + URI = "/URI" # dictionary, optional + ACRO_FORM = "/AcroForm" # dictionary, optional + METADATA = "/Metadata" # stream, optional + STRUCT_TREE_ROOT = "/StructTreeRoot" # dictionary, optional + MARK_INFO = "/MarkInfo" # dictionary, optional + LANG = "/Lang" # text string, optional + SPIDER_INFO = "/SpiderInfo" # dictionary, optional + OUTPUT_INTENTS = "/OutputIntents" # array, optional + PIECE_INFO = "/PieceInfo" # dictionary, optional + OC_PROPERTIES = "/OCProperties" # dictionary, optional + PERMS = "/Perms" # dictionary, optional + LEGAL = "/Legal" # dictionary, optional + REQUIREMENTS = "/Requirements" # array, optional + COLLECTION = "/Collection" # dictionary, optional + NEEDS_RENDERING = "/NeedsRendering" # boolean, optional + DSS = "/DSS" # dictionary, optional + AF = "/AF" # array of dictionaries, optional + D_PART_ROOT = "/DPartRoot" # dictionary, optional + + +class EncryptionDictAttributes: + """ + Additional encryption dictionary entries for the standard security handler. + + Table 3.19, Page 122. + Table 21 of the 2.0 manual. + """ + + R = "/R" # number, required; revision of the standard security handler + O = "/O" # 32-byte string, required # noqa: E741 + U = "/U" # 32-byte string, required + P = "/P" # integer flag, required; permitted operations + ENCRYPT_METADATA = "/EncryptMetadata" # boolean flag, optional + + +class UserAccessPermissions(IntFlag): + """ + Table 3.20 User access permissions. + Table 22 of the 2.0 manual. + """ + + R1 = 1 + R2 = 2 + PRINT = 4 + MODIFY = 8 + EXTRACT = 16 + ADD_OR_MODIFY = 32 + R7 = 64 + R8 = 128 + FILL_FORM_FIELDS = 256 + EXTRACT_TEXT_AND_GRAPHICS = 512 + ASSEMBLE_DOC = 1024 + PRINT_TO_REPRESENTATION = 2048 + R13 = 2**12 + R14 = 2**13 + R15 = 2**14 + R16 = 2**15 + R17 = 2**16 + R18 = 2**17 + R19 = 2**18 + R20 = 2**19 + R21 = 2**20 + R22 = 2**21 + R23 = 2**22 + R24 = 2**23 + R25 = 2**24 + R26 = 2**25 + R27 = 2**26 + R28 = 2**27 + R29 = 2**28 + R30 = 2**29 + R31 = 2**30 + R32 = 2**31 + + @classmethod + def _is_reserved(cls, name: str) -> bool: + """Check if the given name corresponds to a reserved flag entry.""" + return name.startswith("R") and name[1:].isdigit() + + @classmethod + def _is_active(cls, name: str) -> bool: + """Check if the given reserved name defaults to 1 = active.""" + return name not in {"R1", "R2"} + + def to_dict(self) -> dict[str, bool]: + """Convert the given flag value to a corresponding verbose name mapping.""" + result: dict[str, bool] = {} + for name, flag in UserAccessPermissions.__members__.items(): + if UserAccessPermissions._is_reserved(name): + continue + result[name.lower()] = (self & flag) == flag + return result + + @classmethod + def from_dict(cls, value: dict[str, bool]) -> "UserAccessPermissions": + """Convert the verbose name mapping to the corresponding flag value.""" + value_copy = value.copy() + result = cls(0) + for name, flag in cls.__members__.items(): + if cls._is_reserved(name): + # Reserved names have a required value. Use it. + if cls._is_active(name): + result |= flag + continue + is_active = value_copy.pop(name.lower(), False) + if is_active: + result |= flag + if value_copy: + raise ValueError(f"Unknown dictionary keys: {value_copy!r}") + return result + + @classmethod + def all(cls) -> "UserAccessPermissions": + return cls((2**32 - 1) - cls.R1 - cls.R2) + + +class Resources: + """ + Table 3.30 Entries in a resource dictionary. + Table 34 in the 2.0 reference. + """ + + EXT_G_STATE = "/ExtGState" # dictionary, optional + COLOR_SPACE = "/ColorSpace" # dictionary, optional + PATTERN = "/Pattern" # dictionary, optional + SHADING = "/Shading" # dictionary, optional + XOBJECT = "/XObject" # dictionary, optional + FONT = "/Font" # dictionary, optional + PROC_SET = "/ProcSet" # array, optional + PROPERTIES = "/Properties" # dictionary, optional + + +class PagesAttributes: + """§7.7.3.2 of the 1.7 and 2.0 reference.""" + + TYPE = "/Type" # name, required; must be /Pages + PARENT = "/Parent" # dictionary, required; indirect reference to pages object + KIDS = "/Kids" # array, required; List of indirect references + COUNT = "/Count" + # integer, required; the number of leaf nodes (page objects) + # that are descendants of this node within the page tree + + +class PageAttributes: + """§7.7.3.3 of the 1.7 and 2.0 reference.""" + + TYPE = "/Type" # name, required; must be /Page + PARENT = "/Parent" # dictionary, required; a pages object + LAST_MODIFIED = ( + "/LastModified" # date, optional; date and time of last modification + ) + RESOURCES = "/Resources" # dictionary, required if there are any + MEDIABOX = "/MediaBox" # rectangle, required; rectangle specifying page size + CROPBOX = "/CropBox" # rectangle, optional + BLEEDBOX = "/BleedBox" # rectangle, optional + TRIMBOX = "/TrimBox" # rectangle, optional + ARTBOX = "/ArtBox" # rectangle, optional + BOX_COLOR_INFO = "/BoxColorInfo" # dictionary, optional + CONTENTS = "/Contents" # stream or array, optional + ROTATE = "/Rotate" # integer, optional; page rotation in degrees + GROUP = "/Group" # dictionary, optional; page group + THUMB = "/Thumb" # stream, optional; indirect reference to image of the page + B = "/B" # array, optional + DUR = "/Dur" # number, optional + TRANS = "/Trans" # dictionary, optional + ANNOTS = "/Annots" # array, optional; an array of annotations + AA = "/AA" # dictionary, optional + METADATA = "/Metadata" # stream, optional + PIECE_INFO = "/PieceInfo" # dictionary, optional + STRUCT_PARENTS = "/StructParents" # integer, optional + ID = "/ID" # byte string, optional + PZ = "/PZ" # number, optional + SEPARATION_INFO = "/SeparationInfo" # dictionary, optional + TABS = "/Tabs" # name, optional + TEMPLATE_INSTANTIATED = "/TemplateInstantiated" # name, optional + PRES_STEPS = "/PresSteps" # dictionary, optional + USER_UNIT = "/UserUnit" # number, optional + VP = "/VP" # dictionary, optional + AF = "/AF" # array of dictionaries, optional + OUTPUT_INTENTS = "/OutputIntents" # array, optional + D_PART = "/DPart" # dictionary, required, if this page is within the range of a DPart, not permitted otherwise + + +class FileSpecificationDictionaryEntries: + """Table 3.41 Entries in a file specification dictionary.""" + + Type = "/Type" + FS = "/FS" # The name of the file system to be used to interpret this file specification + F = "/F" # A file specification string of the form described in §3.10.1 + UF = "/UF" # A Unicode string of the file as described in §3.10.1 + DOS = "/DOS" + Mac = "/Mac" + Unix = "/Unix" + ID = "/ID" + V = "/V" + EF = "/EF" # dictionary, containing a subset of the keys F, UF, DOS, Mac, and Unix + RF = "/RF" # dictionary, containing arrays of /EmbeddedFile + DESC = "/Desc" # description of the file + Cl = "/Cl" + + +class StreamAttributes: + """ + Table 4.2. + Table 5 in the 2.0 reference. + """ + + LENGTH = "/Length" # integer, required + FILTER = "/Filter" # name or array of names, optional + DECODE_PARMS = "/DecodeParms" # variable, optional; /DecodeParams is wrong + + +@unique +class FilterTypes(StrEnum): + """§7.4 of the 1.7 and 2.0 references.""" + + ASCII_HEX_DECODE = "/ASCIIHexDecode" # abbreviation: AHx + ASCII_85_DECODE = "/ASCII85Decode" # abbreviation: A85 + LZW_DECODE = "/LZWDecode" # abbreviation: LZW + FLATE_DECODE = "/FlateDecode" # abbreviation: Fl + RUN_LENGTH_DECODE = "/RunLengthDecode" # abbreviation: RL + CCITT_FAX_DECODE = "/CCITTFaxDecode" # abbreviation: CCF + DCT_DECODE = "/DCTDecode" # abbreviation: DCT + JPX_DECODE = "/JPXDecode" + JBIG2_DECODE = "/JBIG2Decode" + + +class FilterTypeAbbreviations: + """§8.9.7 of the 1.7 and 2.0 references.""" + + AHx = "/AHx" + A85 = "/A85" + LZW = "/LZW" + FL = "/Fl" + RL = "/RL" + CCF = "/CCF" + DCT = "/DCT" + + +class LzwFilterParameters: + """ + Table 4.4. + Table 8 in the 2.0 reference. + """ + + PREDICTOR = "/Predictor" # integer + COLORS = "/Colors" # integer + BITS_PER_COMPONENT = "/BitsPerComponent" # integer + COLUMNS = "/Columns" # integer + EARLY_CHANGE = "/EarlyChange" # integer + + +class CcittFaxDecodeParameters: + """ + Table 4.5. + Table 11 in the 2.0 reference. + """ + + K = "/K" # integer + END_OF_LINE = "/EndOfLine" # boolean + ENCODED_BYTE_ALIGN = "/EncodedByteAlign" # boolean + COLUMNS = "/Columns" # integer + ROWS = "/Rows" # integer + END_OF_BLOCK = "/EndOfBlock" # boolean + BLACK_IS_1 = "/BlackIs1" # boolean + DAMAGED_ROWS_BEFORE_ERROR = "/DamagedRowsBeforeError" # integer + + +class ImageAttributes: + """§11.6.5 of the 1.7 and 2.0 references.""" + + TYPE = "/Type" # name, required; must be /XObject + SUBTYPE = "/Subtype" # name, required; must be /Image + NAME = "/Name" # name, required + WIDTH = "/Width" # integer, required + HEIGHT = "/Height" # integer, required + BITS_PER_COMPONENT = "/BitsPerComponent" # integer, required + COLOR_SPACE = "/ColorSpace" # name, required + DECODE = "/Decode" # array, optional + INTENT = "/Intent" # string, optional + INTERPOLATE = "/Interpolate" # boolean, optional + IMAGE_MASK = "/ImageMask" # boolean, optional + MASK = "/Mask" # 1-bit image mask stream + S_MASK = "/SMask" # dictionary or name, optional + + +class ColorSpaces: + DEVICE_RGB = "/DeviceRGB" + DEVICE_CMYK = "/DeviceCMYK" + DEVICE_GRAY = "/DeviceGray" + + +class TypArguments: + """Table 8.2 of the PDF 1.7 reference.""" + + LEFT = "/Left" + RIGHT = "/Right" + BOTTOM = "/Bottom" + TOP = "/Top" + + +class TypFitArguments: + """Table 8.2 of the PDF 1.7 reference.""" + + XYZ = "/XYZ" + FIT = "/Fit" + FIT_H = "/FitH" + FIT_V = "/FitV" + FIT_R = "/FitR" + FIT_B = "/FitB" + FIT_BH = "/FitBH" + FIT_BV = "/FitBV" + + +class GoToActionArguments: + S = "/S" # name, required: type of action + D = "/D" # name, byte string, or array, required: destination to jump to + SD = "/SD" # array, optional: structure destination to jump to + + +class AnnotationDictionaryAttributes: + """Table 8.15 Entries common to all annotation dictionaries.""" + + Type = "/Type" + Subtype = "/Subtype" + Rect = "/Rect" + Contents = "/Contents" + P = "/P" + NM = "/NM" + M = "/M" + F = "/F" + AP = "/AP" + AS = "/AS" + DA = "/DA" + Border = "/Border" + C = "/C" + StructParent = "/StructParent" + OC = "/OC" + + +class InteractiveFormDictEntries: + Fields = "/Fields" + NeedAppearances = "/NeedAppearances" + SigFlags = "/SigFlags" + CO = "/CO" + DR = "/DR" + DA = "/DA" + Q = "/Q" + XFA = "/XFA" + + +class FieldDictionaryAttributes: + """ + Entries common to all field dictionaries (Table 8.69 PDF 1.7 reference) + (*very partially documented here*). + + FFBits provides the constants used for `/Ff` from Table 8.70/8.75/8.77/8.79 + """ + + FT = "/FT" # name, required for terminal fields + Parent = "/Parent" # dictionary, required for children + Kids = "/Kids" # array, sometimes required + T = "/T" # text string, optional + TU = "/TU" # text string, optional + TM = "/TM" # text string, optional + Ff = "/Ff" # integer, optional + V = "/V" # text string or array, optional + DV = "/DV" # text string, optional + AA = "/AA" # dictionary, optional + Opt = "/Opt" # array, optional + + class FfBits(IntFlag): + """ + Ease building /Ff flags + Some entries may be specific to: + + * Text (Tx) (Table 8.75 PDF 1.7 reference) + * Buttons (Btn) (Table 8.77 PDF 1.7 reference) + * Choice (Ch) (Table 8.79 PDF 1.7 reference) + """ + + ReadOnly = 1 << 0 + """common to Tx/Btn/Ch in Table 8.70""" + Required = 1 << 1 + """common to Tx/Btn/Ch in Table 8.70""" + NoExport = 1 << 2 + """common to Tx/Btn/Ch in Table 8.70""" + + Multiline = 1 << 12 + """Tx""" + Password = 1 << 13 + """Tx""" + + NoToggleToOff = 1 << 14 + """Btn""" + Radio = 1 << 15 + """Btn""" + Pushbutton = 1 << 16 + """Btn""" + + Combo = 1 << 17 + """Ch""" + Edit = 1 << 18 + """Ch""" + Sort = 1 << 19 + """Ch""" + + FileSelect = 1 << 20 + """Tx""" + + MultiSelect = 1 << 21 + """Tx""" + + DoNotSpellCheck = 1 << 22 + """Tx/Ch""" + DoNotScroll = 1 << 23 + """Tx""" + Comb = 1 << 24 + """Tx""" + + RadiosInUnison = 1 << 25 + """Btn""" + + RichText = 1 << 25 + """Tx""" + + CommitOnSelChange = 1 << 26 + """Ch""" + + @classmethod + def attributes(cls) -> tuple[str, ...]: + """ + Get a tuple of all the attributes present in a Field Dictionary. + + This method returns a tuple of all the attribute constants defined in + the FieldDictionaryAttributes class. These attributes correspond to the + entries that are common to all field dictionaries as specified in the + PDF 1.7 reference. + + Returns: + A tuple containing all the attribute constants. + + """ + return ( + cls.TM, + cls.T, + cls.FT, + cls.Parent, + cls.TU, + cls.Ff, + cls.V, + cls.DV, + cls.Kids, + cls.AA, + ) + + @classmethod + def attributes_dict(cls) -> dict[str, str]: + """ + Get a dictionary of attribute keys and their human-readable names. + + This method returns a dictionary where the keys are the attribute + constants defined in the FieldDictionaryAttributes class and the values + are their corresponding human-readable names. These attributes + correspond to the entries that are common to all field dictionaries as + specified in the PDF 1.7 reference. + + Returns: + A dictionary containing attribute keys and their names. + + """ + return { + cls.FT: "Field Type", + cls.Parent: "Parent", + cls.T: "Field Name", + cls.TU: "Alternate Field Name", + cls.TM: "Mapping Name", + cls.Ff: "Field Flags", + cls.V: "Value", + cls.DV: "Default Value", + } + + +class CheckboxRadioButtonAttributes: + """Table 8.76 Field flags common to all field types.""" + + Opt = "/Opt" # Options, Optional + + @classmethod + def attributes(cls) -> tuple[str, ...]: + """ + Get a tuple of all the attributes present in a Field Dictionary. + + This method returns a tuple of all the attribute constants defined in + the CheckboxRadioButtonAttributes class. These attributes correspond to + the entries that are common to all field dictionaries as specified in + the PDF 1.7 reference. + + Returns: + A tuple containing all the attribute constants. + + """ + return (cls.Opt,) + + @classmethod + def attributes_dict(cls) -> dict[str, str]: + """ + Get a dictionary of attribute keys and their human-readable names. + + This method returns a dictionary where the keys are the attribute + constants defined in the CheckboxRadioButtonAttributes class and the + values are their corresponding human-readable names. These attributes + correspond to the entries that are common to all field dictionaries as + specified in the PDF 1.7 reference. + + Returns: + A dictionary containing attribute keys and their names. + + """ + return { + cls.Opt: "Options", + } + + +class FieldFlag(IntFlag): + """Table 8.70 Field flags common to all field types.""" + + READ_ONLY = 1 + REQUIRED = 2 + NO_EXPORT = 4 + + +class DocumentInformationAttributes: + """Table 10.2 Entries in the document information dictionary.""" + + TITLE = "/Title" # text string, optional + AUTHOR = "/Author" # text string, optional + SUBJECT = "/Subject" # text string, optional + KEYWORDS = "/Keywords" # text string, optional + CREATOR = "/Creator" # text string, optional + PRODUCER = "/Producer" # text string, optional + CREATION_DATE = "/CreationDate" # date, optional + MOD_DATE = "/ModDate" # date, optional + TRAPPED = "/Trapped" # name, optional + + +class PageLayouts: + """ + Page 84, PDF 1.4 reference. + Page 115, PDF 2.0 reference. + """ + + SINGLE_PAGE = "/SinglePage" + ONE_COLUMN = "/OneColumn" + TWO_COLUMN_LEFT = "/TwoColumnLeft" + TWO_COLUMN_RIGHT = "/TwoColumnRight" + TWO_PAGE_LEFT = "/TwoPageLeft" # (PDF 1.5) + TWO_PAGE_RIGHT = "/TwoPageRight" # (PDF 1.5) + + +class GraphicsStateParameters: + """Table 58 – Entries in a Graphics State Parameter Dictionary""" + + TYPE = "/Type" # name, optional + LW = "/LW" # number, optional + LC = "/LC" # integer, optional + LJ = "/LJ" # integer, optional + ML = "/ML" # number, optional + D = "/D" # array, optional + RI = "/RI" # name, optional + OP = "/OP" + op = "/op" + OPM = "/OPM" + FONT = "/Font" # array, optional + BG = "/BG" + BG2 = "/BG2" + UCR = "/UCR" + UCR2 = "/UCR2" + TR = "/TR" + TR2 = "/TR2" + HT = "/HT" + FL = "/FL" + SM = "/SM" + SA = "/SA" + BM = "/BM" + S_MASK = "/SMask" # dictionary or name, optional + CA = "/CA" + ca = "/ca" + AIS = "/AIS" + TK = "/TK" + + +class _CatalogDictionaryMeta(type): + def __getattribute__(cls, name: str) -> Any: + value = super().__getattribute__(name) + + if not name.startswith("__"): + deprecate_with_replacement("CatalogDictionary", "CatalogAttributes", "7.0.0") + return value + + +class CatalogDictionary(CatalogAttributes, metaclass=_CatalogDictionaryMeta): + def __init__(self) -> None: + deprecate_with_replacement("CatalogDictionary", "CatalogAttributes", "7.0.0") + + +class OutlineFontFlag(IntFlag): + """A class used as an enumerable flag for formatting an outline font.""" + + italic = 1 + bold = 2 + + +class PageLabelStyle(StrEnum): + """ + Table 8.10 in the 1.7 reference. + Table 161 in the 2.0 reference. + """ + + DECIMAL = "/D" # Decimal Arabic numerals + UPPERCASE_ROMAN = "/R" # Uppercase Roman numerals + LOWERCASE_ROMAN = "/r" # Lowercase Roman numerals + UPPERCASE_LETTER = "/A" # Uppercase letters + LOWERCASE_LETTER = "/a" # Lowercase letters + + +class AnnotationFlag(IntFlag): + """See §12.5.3 "Annotation Flags".""" + + INVISIBLE = 1 + HIDDEN = 2 + PRINT = 4 + NO_ZOOM = 8 + NO_ROTATE = 16 + NO_VIEW = 32 + READ_ONLY = 64 + LOCKED = 128 + TOGGLE_NO_VIEW = 256 + LOCKED_CONTENTS = 512 + + +PDF_KEYS = ( + AnnotationDictionaryAttributes, + CatalogAttributes, + CatalogDictionary, + CcittFaxDecodeParameters, + CheckboxRadioButtonAttributes, + ColorSpaces, + Core, + DocumentInformationAttributes, + EncryptionDictAttributes, + FieldDictionaryAttributes, + FileSpecificationDictionaryEntries, + FilterTypeAbbreviations, + FilterTypes, + GoToActionArguments, + GraphicsStateParameters, + ImageAttributes, + InteractiveFormDictEntries, + LzwFilterParameters, + PageAttributes, + PageLayouts, + PagesAttributes, + Resources, + StreamAttributes, + TrailerKeys, + TypArguments, + TypFitArguments, +) + + +class ImageType(IntFlag): + NONE = 0 + XOBJECT_IMAGES = auto() + INLINE_IMAGES = auto() + DRAWING_IMAGES = auto() + ALL = XOBJECT_IMAGES | INLINE_IMAGES | DRAWING_IMAGES + IMAGES = ALL # for consistency with ObjectDeletionFlag + + +_INLINE_IMAGE_VALUE_MAPPING = { + "/G": "/DeviceGray", + "/RGB": "/DeviceRGB", + "/CMYK": "/DeviceCMYK", + "/I": "/Indexed", + "/AHx": "/ASCIIHexDecode", + "/A85": "/ASCII85Decode", + "/LZW": "/LZWDecode", + "/Fl": "/FlateDecode", + "/RL": "/RunLengthDecode", + "/CCF": "/CCITTFaxDecode", + "/DCT": "/DCTDecode", + "/DeviceGray": "/DeviceGray", + "/DeviceRGB": "/DeviceRGB", + "/DeviceCMYK": "/DeviceCMYK", + "/Indexed": "/Indexed", + "/ASCIIHexDecode": "/ASCIIHexDecode", + "/ASCII85Decode": "/ASCII85Decode", + "/LZWDecode": "/LZWDecode", + "/FlateDecode": "/FlateDecode", + "/RunLengthDecode": "/RunLengthDecode", + "/CCITTFaxDecode": "/CCITTFaxDecode", + "/DCTDecode": "/DCTDecode", + "/RelativeColorimetric": "/RelativeColorimetric", +} + +_INLINE_IMAGE_KEY_MAPPING = { + "/BPC": "/BitsPerComponent", + "/CS": "/ColorSpace", + "/D": "/Decode", + "/DP": "/DecodeParms", + "/F": "/Filter", + "/H": "/Height", + "/W": "/Width", + "/I": "/Interpolate", + "/Intent": "/Intent", + "/IM": "/ImageMask", + "/BitsPerComponent": "/BitsPerComponent", + "/ColorSpace": "/ColorSpace", + "/Decode": "/Decode", + "/DecodeParms": "/DecodeParms", + "/Filter": "/Filter", + "/Height": "/Height", + "/Width": "/Width", + "/Interpolate": "/Interpolate", + "/ImageMask": "/ImageMask", +} + + +class AFRelationship: + """ + Associated file relationship types, defining the relationship between + the PDF component and the associated file. + + Defined in table 43 of the PDF 2.0 reference. + """ + + SOURCE = "/Source" # Original content source + DATA = "/Data" # Base data for visual presentation + ALTERNATIVE = "/Alternative" # Alternative content representation + SUPPLEMENT = "/Supplement" # Supplemental representation of original source/data + ENCRYPTED_PAYLOAD = "/EncryptedPayload" # Encrypted payload document + FORM_DATA = "/FormData" # Data associated with AcroForm of this PDF + SCHEMA = "/Schema" # Schema definition for associated object + UNSPECIFIED = "/Unspecified" # Not known or cannot be described with values + + +class BorderStyles: + """ + A class defining border styles used in PDF documents. + + Defined in table 168 of the PDF 2.0 reference. + """ + + BEVELED = "/B" + DASHED = "/D" + INSET = "/I" + SOLID = "/S" + UNDERLINED = "/U" + + +class FontFlags(IntFlag): + """ + A class defining font flags in PDF document font descriptor resources. + + Defined in table 121 of the PDF 2.0 reference. + """ + + FIXED_PITCH = 1 << 0 + SERIF = 1 << 1 + SYMBOLIC = 1 << 2 + SCRIPT = 1 << 3 + NONSYMBOLIC = 1 << 5 + ITALIC = 1 << 6 + ALL_CAP = 1 << 16 + SMALL_CAP = 1 << 17 + FORCE_BOLD = 1 << 18 diff --git a/app/vendor/pypdf/errors.py b/app/vendor/pypdf/errors.py new file mode 100644 index 0000000..8fec6e1 --- /dev/null +++ b/app/vendor/pypdf/errors.py @@ -0,0 +1,74 @@ +""" +All errors/exceptions pypdf raises and all of the warnings it uses. + +Please note that broken PDF files might cause other Exceptions. +""" + + +class DeprecationError(Exception): + """Raised when a deprecated feature is used.""" + + +class DependencyError(Exception): + """ + Raised when a required dependency (a library or module that pypdf depends on) + is not available or cannot be imported. + """ + + +class PyPdfError(Exception): + """Base class for all exceptions raised by pypdf.""" + + +class PdfReadError(PyPdfError): + """Raised when there is an issue reading a PDF file.""" + + +class PageSizeNotDefinedError(PyPdfError): + """Raised when the page size of a PDF document is not defined.""" + + +class PdfReadWarning(UserWarning): + """Issued when there is a potential issue reading a PDF file, but it can still be read.""" + + +class PdfStreamError(PdfReadError): + """Raised when there is an issue reading the stream of data in a PDF file.""" + + +class ParseError(PyPdfError): + """ + Raised when there is an issue parsing (analyzing and understanding the + structure and meaning of) a PDF file. + """ + + +class FileNotDecryptedError(PdfReadError): + """ + Raised when a PDF file that has been encrypted + (meaning it requires a password to be accessed) has not been successfully + decrypted. + """ + + +class WrongPasswordError(FileNotDecryptedError): + """Raised when the wrong password is used to try to decrypt an encrypted PDF file.""" + + +class EmptyFileError(PdfReadError): + """Raised when a PDF file is empty or has no content.""" + + +class EmptyImageDataError(PyPdfError): + """Raised when trying to process an image that has no data.""" + + +STREAM_TRUNCATED_PREMATURELY = "Stream has ended unexpectedly" + + +class LimitReachedError(PyPdfError): + """Raised when a limit is reached.""" + + +class XmpDocumentError(PyPdfError, RuntimeError): + """Raised when the XMP XML document context is invalid or missing.""" diff --git a/app/vendor/pypdf/filters.py b/app/vendor/pypdf/filters.py new file mode 100644 index 0000000..eb75b9c --- /dev/null +++ b/app/vendor/pypdf/filters.py @@ -0,0 +1,892 @@ +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +""" +Implementation of stream filters; §7.4 Filters of the PDF 2.0 specification. + +§8.9.7 Inline images of the PDF 2.0 specification has abbreviations that can be +used for the names of filters in an inline image object. +""" +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + +import binascii +import math +import os +import shutil +import struct +import subprocess +import zlib +from base64 import a85decode +from dataclasses import dataclass +from pathlib import Path +from tempfile import TemporaryDirectory +from typing import Any, NoReturn, Optional, Union, cast + +from ._codecs._codecs import LzwCodec as _LzwCodec +from ._configuration import get_configuration +from ._utils import ( + WHITESPACES_AS_BYTES, + deprecate, + deprecation_with_replacement, + logger_warning, +) +from .constants import CcittFaxDecodeParameters as CCITT +from .constants import FilterTypeAbbreviations as FTA +from .constants import FilterTypes as FT +from .constants import ImageAttributes, StreamAttributes +from .constants import LzwFilterParameters as LZW +from .errors import DependencyError, LimitReachedError, PdfReadError, PdfStreamError +from .generic import ( + ArrayObject, + DictionaryObject, + IndirectObject, + NullObject, + NumberObject, + PdfObject, + StreamObject, + is_null_or_none, +) + +MAX_DECLARED_STREAM_LENGTH = 75_000_000 # DEPRECATED: Use pypdf.Configuration. +MAX_ARRAY_BASED_STREAM_OUTPUT_LENGTH = 75_000_000 # DEPRECATED: Use pypdf.Configuration. + +JBIG2_MAX_OUTPUT_LENGTH = 75_000_000 # DEPRECATED: Use pypdf.Configuration. +LZW_MAX_OUTPUT_LENGTH = 75_000_000 # DEPRECATED: Use pypdf.Configuration. +RUN_LENGTH_MAX_OUTPUT_LENGTH = 75_000_000 # DEPRECATED: Use pypdf.Configuration. +ZLIB_MAX_OUTPUT_LENGTH = 75_000_000 # DEPRECATED: Use pypdf.Configuration. +ZLIB_MAX_RECOVERY_INPUT_LENGTH = 5_000_000 # DEPRECATED: Use pypdf.Configuration. +FLATE_MAX_COLUMNS = 250_000 # DEPRECATED: Use pypdf.Configuration. +FLATE_MAX_ROW_LENGTH = 4_000_000 # DEPRECATED: Use pypdf.Configuration. +FLATE_MAX_BUFFER_SIZE = 75_000_000 # DEPRECATED: Use pypdf.Configuration. + +# Reuse cached 1-byte values in the fallback loop to avoid per-byte allocations. +_SINGLE_BYTES = tuple(bytes((i,)) for i in range(256)) + + +def _decompress_with_limit(data: bytes) -> bytes: + decompressor = zlib.decompressobj() + configuration = get_configuration() + result = decompressor.decompress(data, max_length=configuration.zlib_maximum_output_length) + if decompressor.unconsumed_tail: + raise LimitReachedError( + f"Limit reached while decompressing. {len(decompressor.unconsumed_tail)} bytes remaining." + ) + return result + + +def decompress(data: bytes) -> bytes: + """ + Decompress the given data using zlib. + + Attempts to decompress the input data using zlib. + If the decompression fails due to a zlib error, it falls back + to using a decompression object with a larger window size. + + Please note that the output length is limited to avoid memory + issues. If you need to process larger content streams, consider + adapting ``pypdf.Configuration.zlib_maximum_output_length``. In case you + are only dealing with trusted inputs and/or want to disable these + limits, set the value to `0`. + + Args: + data: The input data to be decompressed. + + Returns: + The decompressed data. + + """ + try: + return _decompress_with_limit(data) + except zlib.error: + # First quick approach: There are known issues with faulty added bytes to the + # tail of the encoded stream from early Adobe Distiller or Pitstop versions + # with CR char as the default line separator (assumed by reverse engineering) + # that breaks the decoding process in the end. + # + # Try first to cut off some of the tail byte by byte, but limited to not + # iterate through too many loops and kill the performance for large streams, + # to then allow the final fallback to run. Added this intermediate attempt, + # because starting from the head of the stream byte by byte kills completely + # the performance for large streams (e.g., 6 MB) with the tail-byte-issue + # and takes ages. This solution is really fast: + max_tail_cut_off_bytes: int = 8 + for i in range(1, min(max_tail_cut_off_bytes + 1, len(data))): + try: + return _decompress_with_limit(data[:-i]) + except zlib.error: + pass + + # If still failing, then try with increased window size. + decompressor = zlib.decompressobj(zlib.MAX_WBITS | 32) + result = bytearray() + configuration = get_configuration() + remaining_limit = configuration.zlib_maximum_output_length + data_length = len(data) + known_errors = set() + for index in range(data_length): + if index >= configuration.zlib_maximum_recovery_input_length: + raise LimitReachedError( + f"Recovery limit reached while decompressing. {data_length - index} bytes remaining." + ) + + chunk = _SINGLE_BYTES[data[index]] + try: + decompressed = decompressor.decompress(chunk, max_length=remaining_limit) + result += decompressed + remaining_limit -= len(decompressed) + if remaining_limit <= 0: + raise LimitReachedError( + f"Limit reached while decompressing. {data_length - index} bytes remaining." + ) + except zlib.error as error: + error_str = str(error) + if error_str in known_errors: + continue + logger_warning(error_str, source=__name__) + known_errors.add(error_str) + return bytes(result) + + +class FlateDecode: + @staticmethod + def decode( + data: bytes, + decode_parms: Optional[Union[DictionaryObject, IndirectObject]] = None, + **kwargs: Any, + ) -> bytes: + """ + Decode data which is flate-encoded. + + Args: + data: Flate-encoded data. + decode_parms: Additional decoding parameters. + + Returns: + The flate-decoded data. + + Raises: + PdfReadError: Unsupported parameters have been found. + + """ + str_data = decompress(data) + + processed_parms: Optional[PdfObject] = decode_parms + if isinstance(decode_parms, IndirectObject) and processed_parms is not None: + processed_parms = processed_parms.get_object() + if isinstance(processed_parms, dict): + parameters = processed_parms + else: + if not is_null_or_none(processed_parms): + logger_warning( + "Detected invalid /DecodeParms, results might be incorrect: %(value)s (type %(type_name)s)", + source=__name__, value=decode_parms, type_name=decode_parms.__class__.__name__, + ) + parameters = DictionaryObject() + + predictor = parameters.get("/Predictor", 1) + + # predictor 1 == no predictor + if predictor != 1: + columns, colors, bits_per_component = FlateDecode._get_parameters(parameters) + configuration = get_configuration() + + # PNG predictor can vary by row and so is the lead byte on each row + row_length = ( + math.ceil(columns * colors * bits_per_component / 8) + 1 + ) # number of bytes + if row_length > configuration.flate_maximum_row_length: + raise LimitReachedError( + f"Row length of {row_length} exceeds defined limit of {configuration.flate_maximum_row_length}." + ) + + # TIFF prediction: + if predictor == 2: + row_length -= 1 # remove the predictor byte + bpp = row_length // columns + str_data_mut = bytearray(str_data) + for i in range(len(str_data_mut)): + if i % row_length >= bpp: + str_data_mut[i] = (str_data_mut[i] + str_data_mut[i - bpp]) % 256 + str_data = bytes(str_data_mut) + # PNG prediction: + elif 10 <= predictor <= 15: + str_data = FlateDecode._decode_png_prediction( + str_data, columns, row_length + ) + else: + raise PdfReadError(f"Unsupported flatedecode predictor {predictor!r}") + return str_data + + @staticmethod + def _get_parameters(parameters: DictionaryObject) -> tuple[int, int, int]: + # For details, see table 8 of ISO 32000-2:2020. + def get(key: str, default: int) -> int: + _value = parameters.get(key, NumberObject(default)).get_object() + if not isinstance(_value, int) or _value < 1: + raise PdfReadError(f"Expected positive number for {key}, got {_value}!") + return _value + + configuration = get_configuration() + columns = get(key=LZW.COLUMNS, default=1) + if columns > configuration.flate_maximum_columns: + raise LimitReachedError( + f"Number of columns {columns} exceeds defined limit of {configuration.flate_maximum_columns}." + ) + + colors = get(key=LZW.COLORS, default=1) + if colors > 16: + raise LimitReachedError( + f"Color value {colors} exceeds limit of 16. " + f"Please open an issue if this limits valid use cases." + ) + + bits_per_component = get(key=LZW.BITS_PER_COMPONENT, default=8) + if bits_per_component > 16: + raise PdfReadError(f"More than 16 bits per component are not allowed: {bits_per_component}") + + return columns, colors, bits_per_component + + @staticmethod + def _decode_png_prediction(data: bytes, columns: int, row_length: int) -> bytes: + # PNG prediction can vary from row to row + if (remainder := len(data) % row_length) != 0: + logger_warning("Image data is not rectangular. Adding padding.", source=__name__) + data += b"\x00" * (row_length - remainder) + assert len(data) % row_length == 0 + output = bytearray() + previous_row_data = bytes(row_length) + bpp = (row_length - 1) // columns # recomputed locally to not change params + for row in range(0, len(data), row_length): + row_data = bytearray(data[row : row + row_length]) + filter_byte = row_data[0] + + if filter_byte == 0: + # PNG None Predictor + pass + elif filter_byte == 1: + # PNG Sub Predictor + for i in range(bpp + 1, row_length): + row_data[i] = (row_data[i] + row_data[i - bpp]) % 256 + elif filter_byte == 2: + # PNG Up Predictor + for i in range(1, row_length): + row_data[i] = (row_data[i] + previous_row_data[i]) % 256 + elif filter_byte == 3: + # PNG Average Predictor + for i in range(1, bpp + 1): + floor = previous_row_data[i] // 2 + row_data[i] = (row_data[i] + floor) % 256 + for i in range(bpp + 1, row_length): + left = row_data[i - bpp] + floor = (left + previous_row_data[i]) // 2 + row_data[i] = (row_data[i] + floor) % 256 + elif filter_byte == 4: + # PNG Paeth Predictor + for i in range(1, bpp + 1): + row_data[i] = (row_data[i] + previous_row_data[i]) % 256 + for i in range(bpp + 1, row_length): + left = row_data[i - bpp] + up = previous_row_data[i] + up_left = previous_row_data[i - bpp] + + p = left + up - up_left + dist_left = abs(p - left) + dist_up = abs(p - up) + dist_up_left = abs(p - up_left) + + if dist_left <= dist_up and dist_left <= dist_up_left: + paeth = left + elif dist_up <= dist_up_left: + paeth = up + else: + paeth = up_left + + row_data[i] = (row_data[i] + paeth) % 256 + else: + raise PdfReadError( + f"Unsupported PNG filter {filter_byte!r}" + ) # pragma: no cover + previous_row_data = bytes(row_data) + output += row_data[1:] + return bytes(output) + + @staticmethod + def encode(data: bytes, level: int = -1) -> bytes: + """ + Compress the input data using zlib. + + Args: + data: The data to be compressed. + level: See https://docs.python.org/3/library/zlib.html#zlib.compress + + Returns: + The compressed data. + + """ + return zlib.compress(data, level) + + +class ASCIIHexDecode: + """ + The ASCIIHexDecode filter decodes data that has been encoded in ASCII + hexadecimal form into a base-7 ASCII format. + """ + + @staticmethod + def decode( + data: Union[str, bytes], + decode_parms: Optional[DictionaryObject] = None, + **kwargs: Any, + ) -> bytes: + """ + Decode an ASCII-Hex encoded data stream. + + Args: + data: a str sequence of hexadecimal-encoded values to be + converted into a base-7 ASCII string + decode_parms: this filter does not use parameters. + + Returns: + A string conversion in base-7 ASCII, where each of its values + v is such that 0 <= ord(v) <= 127. + + Raises: + PdfStreamError: + + """ + if isinstance(data, str): + data = data.encode() + + # Stop at EOD + eod = data.find(b">") + if eod == -1: + logger_warning( + "missing EOD in ASCIIHexDecode, check if output is OK", + source=__name__, + ) + hex_data = data + else: + hex_data = data[:eod] + + # Remove whitespace + hex_data = b"".join(hex_data.split()) + + # Pad if odd length + if len(hex_data) % 2 == 1: + hex_data += b"0" + + # The spec permits only 0-9, A-F, a-f and whitespace inside the stream. + # Stray bytes used to surface as an uncaught binascii.Error; turn them + # into a proper PdfStreamError instead. + try: + return binascii.unhexlify(hex_data) + except binascii.Error as error: + raise PdfStreamError(f"Invalid hexadecimal character in ASCIIHexDecode stream: {error}") + + +class RunLengthDecode: + """ + The RunLengthDecode filter decodes data that has been encoded in a + simple byte-oriented format based on run length. + The encoded data is a sequence of runs, where each run consists of + a length byte followed by 1 to 128 bytes of data. If the length byte is + in the range 0 to 127, + the following length + 1 (1 to 128) bytes are copied literally during + decompression. + If length is in the range 129 to 255, the following single byte is to be + copied 257 − length (2 to 128) times during decompression. A length value + of 128 denotes EOD. + """ + + @staticmethod + def decode( + data: bytes, + decode_parms: Optional[DictionaryObject] = None, + **kwargs: Any, + ) -> bytes: + """ + Decode a run length encoded data stream. + + Args: + data: a bytes sequence of length/data + decode_parms: this filter does not use parameters. + + Returns: + A bytes decompressed sequence. + + Raises: + PdfStreamError: + + """ + lst = [] + index = 0 + data_length = len(data) + total_length = 0 + configuration = get_configuration() + while True: + if index >= data_length: + logger_warning( + "missing EOD in RunLengthDecode, check if output is OK", source=__name__ + ) + break # Reached end of string without an EOD + length = data[index] + index += 1 + if length == 128: + if index < data_length: + # We should first check, if we have an inner stream from a multi-encoded + # stream with a faulty trailing newline that we can decode properly. + # We will just ignore the last byte and raise a warning ... + if (index == data_length - 1) and (data[index : index + 1] == b"\n"): + logger_warning( + "Found trailing newline in stream data, check if output is OK", source=__name__ + ) + break + # Raising an exception here breaks all image extraction for this file, which might + # not be desirable. For this reason, indicate that the output is most likely wrong, + # as processing stopped after the first EOD marker. See issue #3517. + logger_warning( + "Early EOD in RunLengthDecode, check if output is OK", source=__name__ + ) + break + if length < 128: + length += 1 + lst.append(data[index : (index + length)]) + index += length + else: # >128 + if index >= data_length: + logger_warning( + "Missing EOD in RunLengthDecode, check if output is OK", source=__name__ + ) + break # Reached end of string without the replicated byte + length = 257 - length + lst.append(bytes((data[index],)) * length) + index += 1 + total_length += length + if total_length > configuration.run_length_maximum_output_length: + raise LimitReachedError("Limit reached while decompressing.") + return b"".join(lst) + + +class LZWDecode: + class Decoder: + STOP = 257 + CLEARDICT = 256 + + def __init__(self, data: bytes) -> None: + self.data = data + + def decode(self) -> bytes: + configuration = get_configuration() + return _LzwCodec(max_output_length=configuration.lzw_maximum_output_length).decode(self.data) + + @staticmethod + def decode( + data: bytes, + decode_parms: Optional[DictionaryObject] = None, + **kwargs: Any, + ) -> bytes: + """ + Decode an LZW encoded data stream. + + Args: + data: ``bytes`` or ``str`` text to decode. + decode_parms: a dictionary of parameter values. + + Returns: + decoded data. + + """ + # decode_parms is unused here + return LZWDecode.Decoder(data).decode() + + +class ASCII85Decode: + """Decodes string ASCII85-encoded data into a byte format.""" + + @staticmethod + def decode( + data: Union[str, bytes], + decode_parms: Optional[DictionaryObject] = None, + **kwargs: Any, + ) -> bytes: + """ + Decode an Ascii85 encoded data stream. + + Args: + data: ``bytes`` or ``str`` text to decode. + decode_parms: this filter does not use parameters. + + Returns: + decoded data. + + """ + if isinstance(data, str): + data = data.encode() + data = data.strip(WHITESPACES_AS_BYTES) + if len(data) > 2 and data.endswith(b">"): + data = data[:-1].rstrip(WHITESPACES_AS_BYTES) + data[-1:] + try: + return a85decode(data, adobe=True, ignorechars=WHITESPACES_AS_BYTES) + except ValueError as error: + if error.args[0] == "Ascii85 encoded byte sequences must end with b'~>'": + logger_warning("Ignoring missing Ascii85 end marker.", source=__name__) + return a85decode(data, adobe=False, ignorechars=WHITESPACES_AS_BYTES) + raise + + +class DCTDecode: + @staticmethod + def decode( + data: bytes, + decode_parms: Optional[DictionaryObject] = None, + **kwargs: Any, + ) -> bytes: + """ + Decompresses data encoded using a DCT (discrete cosine transform) + technique based on the JPEG standard (IS0/IEC 10918), + reproducing image sample data that approximates the original data. + + Args: + data: text to decode. + decode_parms: this filter does not use parameters. + + Returns: + decoded data. + + """ + return data + + +class JPXDecode: + @staticmethod + def decode( + data: bytes, + decode_parms: Optional[DictionaryObject] = None, + **kwargs: Any, + ) -> bytes: + """ + Decompresses data encoded using the wavelet-based JPEG 2000 standard, + reproducing the original image data. + + Args: + data: text to decode. + decode_parms: this filter does not use parameters. + + Returns: + decoded data. + + """ + return data + + +@dataclass +class CCITTParameters: + """§7.4.6, optional parameters for the CCITTFaxDecode filter.""" + + K: int = 0 + columns: int = 1728 + rows: int = 0 + EndOfLine: Union[bool, None] = False + EncodedByteAlign: Union[bool, None] = False + EndOfBlock: Union[bool, None] = True + BlackIs1: bool = False + DamagedRowsBeforeError: Union[int, None] = 0 + + @property + def group(self) -> int: + if self.K < 0: + # Pure two-dimensional encoding (Group 4) + ccitt_group = 4 + else: + # K == 0: Pure one-dimensional encoding (Group 3, 1-D) + # K > 0: Mixed one- and two-dimensional encoding (Group 3, 2-D) + ccitt_group = 3 + return ccitt_group + + +def __create_old_class_instance( + K: int = 0, + columns: int = 0, + rows: int = 0 +) -> NoReturn: + deprecation_with_replacement("CCITParameters", "CCITTParameters", "6.0.0") + + +# Create an alias for the old class name +CCITParameters = __create_old_class_instance + + +class CCITTFaxDecode: + """ + §7.4.6, CCITTFaxDecode filter (ISO 32000). + + Either Group 3 or Group 4 CCITT facsimile (fax) encoding. + CCITT encoding is bit-oriented, not byte-oriented. + + §7.4.6, optional parameters for the CCITTFaxDecode filter. + """ + + @staticmethod + def _get_parameters( + parameters: Union[ArrayObject, DictionaryObject, IndirectObject, None], + rows: Union[int, IndirectObject], + ) -> CCITTParameters: + ccitt_parameters = CCITTParameters(rows=int(rows)) + if parameters: + parameters_unwrapped = cast( + Union[ArrayObject, DictionaryObject], parameters.get_object() + ) + if isinstance(parameters_unwrapped, ArrayObject): + for decode_parm in parameters_unwrapped: + if CCITT.K in decode_parm: + ccitt_parameters.K = decode_parm[CCITT.K].get_object() + if CCITT.COLUMNS in decode_parm: + ccitt_parameters.columns = decode_parm[CCITT.COLUMNS].get_object() + if CCITT.BLACK_IS_1 in decode_parm: + ccitt_parameters.BlackIs1 = decode_parm[CCITT.BLACK_IS_1].get_object().value + else: + if CCITT.K in parameters_unwrapped: + ccitt_parameters.K = parameters_unwrapped[CCITT.K].get_object() # type: ignore[assignment] + if CCITT.COLUMNS in parameters_unwrapped: + ccitt_parameters.columns = parameters_unwrapped[CCITT.COLUMNS].get_object() # type: ignore[assignment] + if CCITT.BLACK_IS_1 in parameters_unwrapped: + ccitt_parameters.BlackIs1 = parameters_unwrapped[CCITT.BLACK_IS_1].get_object().value # type: ignore[union-attr] + return ccitt_parameters + + @staticmethod + def decode( + data: bytes, + decode_parms: Optional[DictionaryObject] = None, + height: int = 0, + **kwargs: Any, + ) -> bytes: + params = CCITTFaxDecode._get_parameters(decode_parms, height) + + img_size = len(data) + # TIFF SHORT and LONG fields are unsigned (TIFF 6.0, §2); pack them with + # unsigned codes so dimensions with the high bit set do not overflow. + tiff_header_struct = "<2sHLH" + "HHLL" * 8 + "H" + tiff_header = struct.pack( + tiff_header_struct, + b"II", # Byte order indication: Little endian + 42, # Version number (always 42) + 8, # Offset to the first image file directory (IFD) + 8, # Number of tags in IFD + 256, # ImageWidth, LONG, 1, width + 4, + 1, + params.columns, + 257, # ImageLength, LONG, 1, length + 4, + 1, + params.rows, + 258, # BitsPerSample, SHORT, 1, 1 + 3, + 1, + 1, + 259, # Compression, SHORT, 1, compression Type + 3, + 1, + params.group, + 262, # Thresholding, SHORT, 1, 0 = BlackIs1 + 3, + 1, + int(params.BlackIs1), + 273, # StripOffsets, LONG, 1, length of header + 4, + 1, + struct.calcsize( + tiff_header_struct + ), + 278, # RowsPerStrip, LONG, 1, length + 4, + 1, + params.rows, + 279, # StripByteCounts, LONG, 1, size of image + 4, + 1, + img_size, + 0, # last IFD + ) + + return tiff_header + data + + +JBIG2DEC_BINARY = shutil.which("jbig2dec") # DEPRECATED: Use pypdf.Configuration. + + +class JBIG2Decode: + @staticmethod + def decode( + data: bytes, + decode_parms: Optional[DictionaryObject] = None, + **kwargs: Any, + ) -> bytes: + configuration = get_configuration() + if configuration.jbig2dec_binary is None: + raise DependencyError("jbig2dec binary is not available.") + + with TemporaryDirectory() as tempdir: + directory = Path(tempdir) + paths: list[Path] = [] + + if decode_parms and "/JBIG2Globals" in decode_parms: + jbig2_globals = decode_parms["/JBIG2Globals"] + if not is_null_or_none(jbig2_globals) and not is_null_or_none(pointer := jbig2_globals.get_object()): + assert pointer is not None, "mypy" + if isinstance(pointer, StreamObject): + path = directory.joinpath("globals.jbig2") + path.write_bytes(pointer.get_data()) + paths.append(path) + + path = directory.joinpath("image.jbig2") + path.write_bytes(data) + paths.append(path) + + environment = os.environ.copy() + environment["LC_ALL"] = "C" + result = subprocess.run( # noqa: S603 + [ + configuration.jbig2dec_binary, + "--embedded", + "--format", "png", + "--output", "-", + "-M", str(configuration.jbig2_maximum_output_length), + *paths + ], + capture_output=True, + env=environment, + ) + if b"unrecognized option '--embedded'" in result.stderr or b"unrecognized option '-M'" in result.stderr: + raise DependencyError("jbig2dec>=0.19 is required.") + if b"FATAL ERROR failed to allocate image data buffer" in result.stderr: + raise LimitReachedError( + f"Memory limit reached while reading JBIG2 data:\n{result.stderr.decode('utf-8')}" + ) + if result.stderr: + for line in result.stderr.decode("utf-8").splitlines(): + logger_warning(line, source=__name__) + if result.returncode != 0: + raise PdfStreamError(f"Unable to decode JBIG2 data. Exit code: {result.returncode}") + return result.stdout + + @staticmethod + def _is_binary_compatible() -> bool: + configuration = get_configuration() + if not configuration.jbig2dec_binary: # pragma: no cover + return False + result = subprocess.run( # noqa: S603 + [configuration.jbig2dec_binary, "--version"], + capture_output=True, + text=True, + ) + version = result.stdout.split(" ", maxsplit=1)[1] + + from ._utils import Version # noqa: PLC0415 + return Version(version) >= Version("0.19") + + +def _deprecate_inline_image_filters(filter_name: str, old_name: str, new_name: str) -> None: + if filter_name != old_name: + return + deprecate( + f"The filter name {old_name} is deprecated and will be removed in pypdf 7.0.0. Use {new_name} instead.", + 4, + ) + + +def decode_stream_data(stream: StreamObject) -> bytes: + """ + Decode the stream data based on the specified filters. + + This function decodes the stream data using the filters provided in the + stream. + + Args: + stream: The input stream object containing the data and filters. + + Returns: + The decoded stream data. + + Raises: + NotImplementedError: If an unsupported filter type is encountered. + + """ + filters = stream.get(StreamAttributes.FILTER, ()) + if isinstance(filters, IndirectObject): + filters = cast(ArrayObject, filters.get_object()) + if not isinstance(filters, ArrayObject): + # We have a single filter instance + filters = (filters,) + decode_parms = stream.get(StreamAttributes.DECODE_PARMS, (DictionaryObject(),) * len(filters)) + if not isinstance(decode_parms, (list, tuple)): + decode_parms = (decode_parms,) + data: bytes = stream._data + # If there is no data to decode, we should not try to decode it. + if not data: + return data + for filter_name, params in zip(filters, decode_parms): + if isinstance(params, NullObject): + # The decoders are typed for a DictionaryObject; a plain {} is not + # one, so a null /DecodeParms entry would hand them the wrong type. + params = DictionaryObject() + if filter_name in (FT.ASCII_HEX_DECODE, FTA.AHx): + _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.AHx, new_name=FT.ASCII_HEX_DECODE) + data = ASCIIHexDecode.decode(data) + elif filter_name in (FT.ASCII_85_DECODE, FTA.A85): + _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.A85, new_name=FT.ASCII_85_DECODE) + data = ASCII85Decode.decode(data) + elif filter_name in (FT.LZW_DECODE, FTA.LZW): + _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.LZW, new_name=FT.LZW_DECODE) + data = LZWDecode.decode(data, params) + elif filter_name in (FT.FLATE_DECODE, FTA.FL): + _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.FL, new_name=FT.FLATE_DECODE) + data = FlateDecode.decode(data, params) + elif filter_name in (FT.RUN_LENGTH_DECODE, FTA.RL): + _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.RL, new_name=FT.RUN_LENGTH_DECODE) + data = RunLengthDecode.decode(data) + elif filter_name in (FT.CCITT_FAX_DECODE, FTA.CCF): + _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.CCF, new_name=FT.CCITT_FAX_DECODE) + height = stream.get(ImageAttributes.HEIGHT, ()) + data = CCITTFaxDecode.decode(data, params, height) + elif filter_name in (FT.DCT_DECODE, FTA.DCT): + _deprecate_inline_image_filters(filter_name=filter_name, old_name=FTA.DCT, new_name=FT.DCT_DECODE) + data = DCTDecode.decode(data) + elif filter_name == FT.JPX_DECODE: + data = JPXDecode.decode(data) + elif filter_name == FT.JBIG2_DECODE: + data = JBIG2Decode.decode(data, params) + elif filter_name == "/Crypt": + if "/Name" in params or "/Type" in params: + raise NotImplementedError( + "/Crypt filter with /Name or /Type not supported yet" + ) + else: + raise NotImplementedError(f"Unsupported filter {filter_name}") + return data diff --git a/app/vendor/pypdf/generic/__init__.py b/app/vendor/pypdf/generic/__init__.py new file mode 100644 index 0000000..bde1628 --- /dev/null +++ b/app/vendor/pypdf/generic/__init__.py @@ -0,0 +1,115 @@ +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +"""Implementation of generic PDF objects (dictionary, number, string, ...).""" +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + +from ..constants import OutlineFontFlag +from ._base import ( + BooleanObject, + ByteStringObject, + FloatObject, + IndirectObject, + NameObject, + NullObject, + NumberObject, + PdfObject, + TextStringObject, + encode_pdfdocencoding, + is_null_or_none, +) +from ._data_structures import ( + ArrayObject, + ContentStream, + DecodedStreamObject, + Destination, + DictionaryObject, + EncodedStreamObject, + Field, + StreamObject, + TreeObject, + read_object, +) +from ._files import EmbeddedFile +from ._fit import Fit +from ._link import DirectReferenceLink, NamedReferenceLink, ReferenceLink, extract_links +from ._outline import OutlineItem +from ._rectangle import RectangleObject +from ._utils import ( + create_string_object, + decode_pdfdocencoding, + hex_to_rgb, + read_hex_string_from_stream, + read_string_from_stream, +) +from ._viewerpref import ViewerPreferences + +PAGE_FIT = Fit.fit() + + +__all__ = [ + "PAGE_FIT", + "ArrayObject", + "BooleanObject", + "ByteStringObject", + "ContentStream", + "DecodedStreamObject", + "Destination", + "DictionaryObject", + "DirectReferenceLink", + "EmbeddedFile", + "EncodedStreamObject", + "Field", + "Fit", + "FloatObject", + "IndirectObject", + "NameObject", + "NamedReferenceLink", + "NullObject", + "NumberObject", + "OutlineFontFlag", + "OutlineItem", + "PdfObject", + "RectangleObject", + "ReferenceLink", + "StreamObject", + "TextStringObject", + "TreeObject", + "ViewerPreferences", + # Utility functions + "create_string_object", + "decode_pdfdocencoding", + "encode_pdfdocencoding", + "extract_links", + "hex_to_rgb", + "is_null_or_none", + "read_hex_string_from_stream", + # Data structures core functions + "read_object", + "read_string_from_stream", +] diff --git a/app/vendor/pypdf/generic/_appearance_stream.py b/app/vendor/pypdf/generic/_appearance_stream.py new file mode 100644 index 0000000..0951c05 --- /dev/null +++ b/app/vendor/pypdf/generic/_appearance_stream.py @@ -0,0 +1,894 @@ +from __future__ import annotations + +import copy +import re +from dataclasses import dataclass, field +from enum import IntEnum +from io import BytesIO +from operator import attrgetter +from typing import TYPE_CHECKING, Any, NamedTuple, cast + +from .._codecs import encoding_dict_from_named_encoding +from .._codecs.core_font_metrics import CORE_FONT_METRICS +from .._font import Font +from .._page import Transformation +from .._utils import is_char_rtl, logger_warning +from ..constants import AnnotationDictionaryAttributes, BorderStyles, FieldDictionaryAttributes, PageAttributes +from ..errors import PdfReadError +from ..generic import ( + ArrayObject, + DecodedStreamObject, + DictionaryObject, + FloatObject, + IndirectObject, + NameObject, + NumberObject, + RectangleObject, + StreamObject, +) +from ..generic._base import ByteStringObject, TextStringObject +from ._color import Color, DeviceGray + +if TYPE_CHECKING: + from pypdf._writer import PdfWriter + + from .._page import PageObject + +try: + import arabic_reshaper + from bidi.algorithm import get_display + HAS_RTL_SUPPORT = True +except ImportError: + HAS_RTL_SUPPORT = False + +DEFAULT_FONT_SIZE_IN_MULTILINE = 12 + +# "The glyph widths shall be measured in units in which 1000 units correspond to 1 unit in text space" +# (Table 111, PDF Specification 2.0) +TEXT_SPACE_TO_GLYPH_SPACE_FACTOR = 1000 + + +@dataclass +class BaseStreamConfig: + """A container representing the basic layout of an appearance stream.""" + rectangle: RectangleObject = field(default_factory=lambda: RectangleObject((0.0, 0.0, 0.0, 0.0))) + border_width: int = 1 # The width of the border in points + border_style: str = BorderStyles.SOLID + border_color: Color | None = None + background_color: Color | None = None + rotation: int = 0 + + +class BaseStreamAppearance(DecodedStreamObject): + """A class representing the very base of an appearance stream, that is, a rectangle and a border.""" + + def _add_matrix(self, rotation: int) -> None: + # We need to rotate our rectangle while keeping its origin to (0, 0). + # Rotation goes counterclockwise. We want to know the furthest points to which we rotated left and down. + # These will serve as our X and Y offsets to translate the entire object origin back to (0, 0). + # If a corner rotates into negative space, that is our offset. If none does, the minimum is 0.0. + matrix = Transformation().rotate(rotation) + bottom_right_corner = (self._layout.rectangle.width, 0.0) + top_left_corner = (0.0, self._layout.rectangle.height) + top_right_corner = (self._layout.rectangle.width, self._layout.rectangle.height) + rotated_bottom_right_corner = matrix.apply_on(bottom_right_corner) + rotated_top_left_corner = matrix.apply_on(top_left_corner) + rotated_top_right_corner = matrix.apply_on(top_right_corner) + translation_x_offset = -min( + 0.0, rotated_bottom_right_corner[0], rotated_top_left_corner[0], rotated_top_right_corner[0] + ) + translation_y_offset = -min( + 0.0, rotated_bottom_right_corner[1], rotated_top_left_corner[1], rotated_top_right_corner[1] + ) + matrix = matrix.translate(translation_x_offset, translation_y_offset) + self[NameObject("/Matrix")] = ArrayObject([FloatObject(round(i, 3)) for i in matrix.ctm]) + + def __init__(self, layout: BaseStreamConfig | None) -> None: + """ + Takes the appearance stream layout as an argument. + + Args: + layout: The basic layout parameters. + """ + super().__init__() + self._layout = layout or BaseStreamConfig() + self[NameObject("/Type")] = NameObject("/XObject") + self[NameObject("/Subtype")] = NameObject("/Form") + self[NameObject("/BBox")] = self._layout.rectangle + + # Define the rotation matrix + rotation = self._layout.rotation % 360 + if rotation: + self._add_matrix(rotation) + + ap_stream_parts = [] + # Only add a background color when color is defined and not transparent + if isinstance(self._layout.background_color, Color): + ap_stream_parts.append( + f"0 0 {self._layout.rectangle.width} {self._layout.rectangle.height} re\n" + f"{self._layout.background_color.as_operator()}\n" + "f\n" + ) + # Only add a border when border width is larger than 0, border + # color is defined, and border color is not set to transparent. + if self._layout.border_width > 0: + if isinstance(self._layout.border_color, Color): + border_width_string = f"{self._layout.border_width} w\n" if self._layout.border_width != 1 else "" + ap_stream_parts.append( + f"{self._layout.border_width} " + f"{self._layout.border_width} " + f"{self._layout.rectangle.width - 2 * self._layout.border_width} " + f"{self._layout.rectangle.height - 2 * self._layout.border_width} re\n" + f"{border_width_string}" + f"{self._layout.border_color.as_operator(stroke=True)}\n" + "s\n" + ) + else: + # If we aren't drawing a border, then set border_width to 0. This means less margin to + # take into account and results in larger font sizes. + self._layout.border_width = 0 + if ap_stream_parts: + ap_stream_parts.insert(0, "q\n") + ap_stream_parts.append("Q\n") + self._ap_stream_data = "".join(ap_stream_parts).encode() + + +class TextAlignment(IntEnum): + """Defines the alignment options for text within a form field's appearance stream.""" + + LEFT = 0 + CENTER = 1 + RIGHT = 2 + + +class WidthWordGlyphs(NamedTuple): + """A tuple of the unscaled width of a word (unencoded text) and the font-encoded glyphs that represent it.""" + + width: float + word: str + glyphs: str + + +class TextStreamAppearance(BaseStreamAppearance): + """ + A class representing the appearance stream for a text-based form field. + + This class generates the content stream (the `ap_stream_data`) that dictates + how text is rendered within a form field's bounding box. It handles properties + like font, font size, color, multiline text, and text selection highlighting. + """ + + def _scale_text( + self, + font: Font, + font_size: float, + leading_factor: float, + field_width: float, + field_height: float, + paragraphs: list[list[WidthWordGlyphs]], + min_font_size: float, + font_size_step: float = 0.2 + ) -> tuple[list[WidthWordGlyphs], float]: + """ + Takes a piece of text and scales it to field_width or field_height, given font_name + and font_size. Wraps text where necessary. + + Args: + font: The font to be used. + font_size: The font size in points. + leading_factor: The line distance. + field_width: The width of the field in which to fit the text. + field_height: The height of the field in which to fit the text. + paragraphs: The list of text paragraphs to fit with the field, where each paragraph is a list + of tuples of unscaled width, unencoded text, and glyphs (font-encoded text). + min_font_size: The minimum font size at which to scale the text. + font_size_step: The amount by which to decrement font size per step while scaling. + + Returns: + The text in the form of list of tuples, each tuple containing the length of a line + and its contents, and the font_size for these lines and lengths. + """ + wrapped_lines = [] + current_line_words: list[WidthWordGlyphs] = [] + current_line_width: float = 0 + space_width = font.space_width * font_size / TEXT_SPACE_TO_GLYPH_SPACE_FACTOR + for paragraph in paragraphs: + for i, width_word_glyphs in enumerate(paragraph): + word_width = width_word_glyphs.width * font_size + test_width = current_line_width + word_width + (space_width if i else 0) + if test_width > field_width and current_line_words: + wrapped_lines.append( + WidthWordGlyphs( + width=current_line_width, + word=" ".join(map(attrgetter("word"), current_line_words)), + glyphs=font.space_char.join(map(attrgetter("glyphs"), current_line_words)) + ) + ) + current_line_words = [width_word_glyphs] + current_line_width = word_width + elif not current_line_words and word_width > field_width: + wrapped_lines.append( + WidthWordGlyphs( + width=word_width, + word=width_word_glyphs.word, + glyphs=width_word_glyphs.glyphs + ) + ) + current_line_words = [] + current_line_width = 0 + else: + if current_line_words: + current_line_width += space_width + current_line_words.append(width_word_glyphs) + current_line_width += word_width + if current_line_words: + wrapped_lines.append(WidthWordGlyphs( + width=current_line_width, + word=" ".join(map(attrgetter("word"), current_line_words)), + glyphs=font.space_char.join(map(attrgetter("glyphs"), current_line_words)) + )) + current_line_words = [] + current_line_width = 0 + # Estimate total height. + estimated_total_height = font_size + (len(wrapped_lines) - 1) * leading_factor * font_size + if estimated_total_height > field_height: + # Text overflows height; Retry with smaller font size. + new_font_size = font_size - font_size_step + if new_font_size >= min_font_size: + return self._scale_text( + font, + new_font_size, + leading_factor, + field_width, + field_height, + paragraphs, + min_font_size, + font_size_step + ) + return wrapped_lines, round(font_size, 1) + + def _generate_appearance_stream_data( + self, + text: str, + selection: list[str] | None , + font: Font, + font_name: str = "/Helv", + font_size: float = 0.0, + font_color: Color | None = None, + is_multiline: bool = False, + alignment: TextAlignment = TextAlignment.LEFT, + is_comb: bool = False, + max_length: int | None = None + ) -> bytes: + """ + Generates the raw bytes of the PDF appearance stream for a text field. + + This private method assembles the PDF content stream operators to draw + the provided text within the specified rectangle. It handles text positioning, + font application, color, and special formatting like selected text. + + Args: + text: The text to be rendered in the form field. + selection: An optional list of strings that should be highlighted as selected. + font: The font to use. + font_name: The name of the font resource to use (e.g., "/Helv"). + font_size: The font size. If 0, it is automatically calculated + based on whether the field is multiline or not. + font_color: The color to apply to the font, represented as a class Color + is_multiline: A boolean indicating if the text field is multiline. + alignment: Text alignment, can be TextAlignment.LEFT, .RIGHT, or .CENTER. + is_comb: Boolean that designates fixed-length fields, where every character + fills one "cell", such as in a postcode. + max_length: Used if is_comb is set. The maximum number of characters for a fixed- + length field. + + Returns: + A byte string containing the PDF content stream data. + + """ + font_color = font_color or DeviceGray() + rectangle = self._layout.rectangle + leading_factor = ( + (font.font_descriptor.bbox[3] - font.font_descriptor.bbox[1]) / TEXT_SPACE_TO_GLYPH_SPACE_FACTOR + ) + + # Set margins based on border width and style + factor = 2 if self._layout.border_style in {"/B", "/I"} else 1 + margin = self._layout.border_width * factor + field_height = rectangle.height - 2 * margin + field_width = rectangle.width - 4 * max(margin, 1) + + reverse_cmap, encoding_cmap = font._get_typographic_maps() + + def _unicode_to_glyph_id(text: str, reverse_cmap: dict[str, str]) -> str: + if HAS_RTL_SUPPORT: + # Use arabic-reshaper and python-bidi to rearrange and shape text for the PDF engine + reshaped_text = arabic_reshaper.reshape(text) + visual_text = get_display(reshaped_text, base_dir="L") + return "".join(reverse_cmap.get(char, char) for char in visual_text) + + return "".join(reverse_cmap.get(char, char) for char in text) + + + def _glyph_id_to_bytes(glyphs: str, encoding_cmap: dict[str, bytes]) -> list[bytes]: + return [encoding_cmap.get( + glyph_id, bytes((ord(glyph_id),)) if ord(glyph_id) < 256 else b"?" + ) for glyph_id in glyphs] + + # If font_size is 0, apply the logic for multiline or large-as-possible font + if font_size == 0: + min_font_size = 4.0 # The minimum font size + if selection: # Don't wrap text when dealing with a /Ch field, in order to prevent problems + is_multiline = False # with matching "selection" with "line" later on. + if is_multiline: + font_size = DEFAULT_FONT_SIZE_IN_MULTILINE + # We create a list of paragraphs, here each paragraph is a list of tuples, where each WidthWordGlyphs + # tuple signifies an unscaled word width, the word itself, and the glyphs that encode it. + paragraphs: list[list[WidthWordGlyphs]] = [] + for line in text.splitlines(): + if not line.strip(): + paragraphs.append([WidthWordGlyphs(width=0.0, word="", glyphs="")]) + continue + line_by_widths_words_glyphs: list[WidthWordGlyphs] = [] + words = line.split(" ") + for word in words: + glyph_word = _unicode_to_glyph_id(word, reverse_cmap) + line_by_widths_words_glyphs.append( + WidthWordGlyphs( + width=font.get_text_width(word) / TEXT_SPACE_TO_GLYPH_SPACE_FACTOR, + word=word, + glyphs=glyph_word + ) + ) + paragraphs.append(line_by_widths_words_glyphs) + lines, font_size = self._scale_text( + font, + font_size, + leading_factor, + field_width, + field_height, + paragraphs, + min_font_size + ) + else: + max_vertical_size = field_height / leading_factor + glyphs = _unicode_to_glyph_id(text, reverse_cmap) + text_width_unscaled = font.get_text_width(glyphs) / TEXT_SPACE_TO_GLYPH_SPACE_FACTOR + max_horizontal_size = field_width / (text_width_unscaled or 1) + font_size = round(max(min(max_vertical_size, max_horizontal_size), min_font_size), 1) + lines = [WidthWordGlyphs(width=text_width_unscaled * font_size, word=text, glyphs=glyphs)] + elif is_comb: + if max_length and len(text) > max_length: + logger_warning( + ( + "Length of text %(text)s exceeds maximum length (%(max_length)d) " + "of field, input truncated." + ), + source=__name__, + text=text, + max_length=max_length, + ) + # We act as if each character is one line, because we draw it separately later on + lines = [] + for index, char in enumerate(text): + if index < (max_length or len(text)): + glyphs = _unicode_to_glyph_id(char, reverse_cmap) + lines.append( + WidthWordGlyphs( + width=font.get_text_width(glyphs) * font_size / TEXT_SPACE_TO_GLYPH_SPACE_FACTOR, + word=char, + glyphs=glyphs + ) + ) + else: + lines = [] + for line in text.splitlines(): + glyphs = _unicode_to_glyph_id(line, reverse_cmap) + lines.append( + WidthWordGlyphs( + width=font.get_text_width(glyphs) * font_size / TEXT_SPACE_TO_GLYPH_SPACE_FACTOR, + word=line, + glyphs=glyphs + ) + ) + + # Set the vertical offset + if is_multiline: + y_offset = ( + field_height + margin - font.font_descriptor.bbox[3] * font_size / TEXT_SPACE_TO_GLYPH_SPACE_FACTOR + ) + else: + y_offset = margin + ( + (field_height - font.font_descriptor.ascent * font_size / TEXT_SPACE_TO_GLYPH_SPACE_FACTOR) / 2 + ) + default_appearance = f"{font_name} {font_size} Tf {font_color.as_operator()}" + + ap_stream = bytearray() + ap_stream += ( + f"q\n/Tx BMC \nq\n" + f"{2 * max(margin, 1)} {margin} " + f"{round(field_width - 2 * max(margin, 1), 3)} {round(field_height - margin, 3)} re\n" + f"W\nBT\n{default_appearance}\n" + ).encode() + current_x_pos: float = 0 # Initial virtual position within the text object. + + selection_glyphs = _unicode_to_glyph_id("".join(selection), reverse_cmap) if selection else "" + for line_number, (line_width, original_text, line) in enumerate(lines): + if selection_glyphs and line in selection_glyphs: + # Might be improved, but cannot find how to get fill working => replaced with lined box + ap_stream += ( + f"1 {round(y_offset - (line_number * font_size * leading_factor) - 1, 3)} " + f"{rectangle.width - 2} {font_size + 2} re\n" + f"0.5 0.5 0.5 rg s\n{default_appearance}\n" + ).encode() + + # Calculate the desired absolute starting X for the current line + desired_abs_x_start: float = 0 + if is_comb and max_length: + # Calculate the width of a cell for one character + cell_width = rectangle.width / max_length + # Space from the left edge of the cell to the character's baseline start + # line_width here is the *actual* character width in points for the single character 'line' + centering_offset_in_cell = (cell_width - line_width) / 2 + # Absolute start X = (Cell Index, i.e., line_number * Cell Width) + Centering Offset + desired_abs_x_start = (line_number * cell_width) + centering_offset_in_cell + elif alignment == TextAlignment.RIGHT: + desired_abs_x_start = rectangle.width - max(margin, 1) * 2 - line_width + elif alignment == TextAlignment.CENTER: + desired_abs_x_start = (rectangle.width - line_width) / 2 + else: # Left aligned; default + desired_abs_x_start = max(margin, 1) * 2 + # Calculate x_rel_offset: how much to move from the current_x_pos + # to reach the desired_abs_x_start. + x_rel_offset = desired_abs_x_start - current_x_pos + + # Y-offset: + y_rel_offset: float = 0 + if line_number == 0: + y_rel_offset = y_offset # Initial vertical position + elif is_comb: + y_rel_offset = 0.0 # DO NOT move vertically for subsequent characters + else: + y_rel_offset = - font_size * leading_factor # Move down by line height + + # Td is a relative translation (Tx and Ty). + # It updates the current text position. + ap_stream += f"{round(x_rel_offset, 3)} {round(y_rel_offset, 3)} Td\n".encode() + # Update current_x_pos based on the Td operation for the next iteration. + # This is the X position where the *current line* will start. + current_x_pos = desired_abs_x_start + + is_rtl = any(is_char_rtl(char) for char in line) + + encoded_line = _glyph_id_to_bytes(line, encoding_cmap) + if is_rtl: + # Encode input text as UTF-16BE with a BOM, so that the input text is returned + # in the right direction when copying text from the resulting PDF + bom_text = b"\xfe\xff" + original_text.encode("utf-16-be") + hex_original_text = bom_text.hex().upper() + ap_stream += f"/Span << /ActualText <{hex_original_text}> >> BDC\n".encode() + if font.sub_type == "Type0": # 16-bit font + ap_stream += b"<" + (b"".join(encoded_line)).hex().encode() + b"> Tj\n" + else: # Simple font, 8-bit encoded. + # Escape parentheses (PDF 1.7 reference, table 3.2, Literal Strings) + line_as_bytes = ( + b"".join(encoded_line) + .replace(b"\\", b"\\\\") + .replace(b"(", br"\(") + .replace(b")", br"\)") + ) + ap_stream += b"(" + line_as_bytes + b") Tj\n" + if is_rtl: + ap_stream += b"EMC\n" + ap_stream += b"ET\nQ\nEMC\nQ\n" + + return bytes(ap_stream) + + def __init__( + self, + layout: BaseStreamConfig | None = None, + text: str = "", + selection: list[str] | None = None, + font: Font | None = None, + font_resource: DictionaryObject | IndirectObject | None = None, + font_name: str = "/Helv", + font_size: float = 0.0, + font_color: Color | None = None, + is_multiline: bool = False, + alignment: TextAlignment = TextAlignment.LEFT, + is_comb: bool = False, + max_length: int | None = None + ) -> None: + """ + Initializes a TextStreamAppearance object. + + This constructor creates a new PDF stream object configured as an XObject + of subtype Form. It uses the `_generate_appearance_stream_data` method to + generate the content for the stream. + + Args: + layout: The basic layout parameters. + text: The text to be rendered in the form field. + selection: An optional list of strings that should be highlighted as selected. + font: A Font object. Falls back to Type 1 Helvetica if not given. + font_resource: An optional variable that represents a PDF font dictionary. Falls back + to Type 1 Helvetica if not given. + font_name: The name of the font resource, e.g., "/Helv". + font_size: The font size. If 0, it's auto-calculated. + font_color: The font color string. + is_multiline: A boolean indicating if the text field is multiline. + alignment: Text alignment, can be TextAlignment.LEFT, .RIGHT, or .CENTER. + is_comb: Boolean that designates fixed-length fields, where every character + fills one "cell", such as in a postcode. + max_length: Used if is_comb is set. The maximum number of characters for a fixed- + length field. + + """ + super().__init__(layout) + + if not font or not font_resource: + font_name = "/Helv" + font = Font.from_core_font_name() + font_resource = font.as_font_resource() + + self._ap_stream_data += self._generate_appearance_stream_data( + text, + selection, + font, + font_name=font_name, + font_size=font_size, + font_color=font_color or DeviceGray(), + is_multiline=is_multiline, + alignment=alignment, + is_comb=is_comb, + max_length=max_length + ) + + self.set_data(ByteStringObject(self._ap_stream_data)) + self[NameObject("/Length")] = NumberObject(len(self._ap_stream_data)) + # Update Resources with font information + self[NameObject("/Resources")] = DictionaryObject({ + NameObject("/Font"): DictionaryObject({ + NameObject(font_name): getattr(font_resource, "indirect_reference", font_resource) + }) + }) + + @staticmethod + def _find_annotation_font_resource( + font_name: str, + annotation: DictionaryObject, + acro_form: DictionaryObject, + text: str + ) -> tuple[str, Font]: + # Try to find a resource dictionary for the font by examining the annotation and, if that fails, + # the AcroForm resources dictionary + acro_form_resources: Any = cast( + DictionaryObject, + annotation.get_inherited( + "/DR", + acro_form.get("/DR", DictionaryObject()), + ), + ) + acro_form_font_resources = acro_form_resources.get("/Font", DictionaryObject()) + font_resource = acro_form_font_resources.get(font_name, None) + if font_resource: + font = Font.from_font_resource(font_resource.get_object()) + else: + # Normally, we should have found a font resource by now. However, when a user has provided a specific + # font name, we may not have found the associated font resource among the AcroForm resources. Also, in + # case of the 14 Adobe Core fonts, we may be expected to construct a font resource ourselves. + if font_name.removeprefix("/") not in CORE_FONT_METRICS: + # Default to Helvetica if we haven't found a font resource and cannot construct one ourselves. + logger_warning( + "Font dictionary for %(font_name)s not found; defaulting to Helvetica.", + source=__name__, + font_name=font_name, + ) + font_name = "/Helvetica" + font = Font.from_core_font_name(font_name) + + # If we have found a font resource, it still might not be able to encode the text value we received. + encodable = font.can_encode(text) + + if not encodable: + # If we have a font file, we can try to produce a new font resource with an encoding + # that does include the necessary characters. We only try this for a TrueType font, meaning + # that, in PDF terms, it is a simple, 8-bit encoded font. + if font.font_descriptor.font_file and font.sub_type == "TrueType": + try: + font = font.from_truetype_font_file(BytesIO(font.font_descriptor.font_file.get_data())) + font_name = "/PYPDF1" # This means we most probably do not clash with an existing font name + encodable = font.can_encode(text) + except (ImportError, PdfReadError) as e: + logger_warning("Unable to use embedded font for encoding: %(e)s", source=__name__, e=e) + + # If it's one of the unembedded 14 Adobe Core Fonts, we can test other supported encodings + elif font.sub_type == "Type1" and font.name in CORE_FONT_METRICS: + core_font_metrics = CORE_FONT_METRICS[font.name] + test_encodings = { + "cp1250", # Central / Eastern European + "cp1252", # Western European + "cp1254", # Turkish + "cp1257", # Baltic Rim + "iso8859_15" # Western European ISO Alternate + } + for encoding in test_encodings: + test_font = copy.copy(font) + test_font.encoding = encoding_dict_from_named_encoding(encoding) + encodable = test_font.can_encode(text) + if encodable: + font = test_font + font.character_widths.clear() + for code, character in test_font.encoding.items(): + # Look up the width using the glyph name from the encoding + if character in core_font_metrics.character_widths: + font.character_widths[chr(code)] = core_font_metrics.character_widths[character] + font.character_widths["default"] = core_font_metrics.character_widths["default"] + font_name = "/PYPDF1" + encoding + break + + if not encodable: + logger_warning( + ( + "Text string '%(text)s' contains characters not supported by font encoding. " + "This may result in text corruption. " + "Consider calling writer.update_page_form_field_values with auto_regenerate=True." + ), + source=__name__, + text=text, + ) + + return font_name, font + + @staticmethod + def _sync_appearance_stream_font_resources( + writer: PdfWriter, + font_name: str, + font: Font, + target_resource_dict: DictionaryObject, + page: PageObject | None = None + ) -> IndirectObject: + """ + Unified helper to sync fonts from an AP stream to a target resource dictionary (e.g., AcroForm /DR). + Will sync to page resources as well when page is added to the arguments. + """ + target_fonts = target_resource_dict.setdefault(NameObject("/Font"), DictionaryObject()).get_object() + if font_name not in target_fonts: + font_resource_reference = font._add_to_writer( + writer, + target_fonts, + NameObject(font_name) + ) + else: + font_resource_reference = target_fonts[font_name] + + if page: + page_fonts_resource = cast(DictionaryObject, page[PageAttributes.RESOURCES]).setdefault( + NameObject("/Font"), DictionaryObject() + ).get_object() + if font_name not in page_fonts_resource: + page_fonts_resource[NameObject(font_name)] = getattr( + font_resource_reference, "indirect_reference", font_resource_reference + ) + + return font_resource_reference + + @classmethod + def from_text_annotation( + cls, + writer: PdfWriter, + page: PageObject, + flatten: bool, + acro_form: DictionaryObject, # _root_object[CatalogAttributes.ACRO_FORM]) + field: DictionaryObject, + annotation: DictionaryObject, + user_font_name: str = "", + user_font_size: float = -1, + ) -> TextStreamAppearance: + """ + Creates a TextStreamAppearance object from a text field annotation. + + This class method is a factory for creating a `TextStreamAppearance` + instance by extracting all necessary information (bounding box, font, + text content, etc.) from the PDF field and annotation dictionaries. + It respects inheritance for properties like default appearance (`/DA`). + + Args: + writer: The PdfWriter instance that we are creating text stream appearances for. + page: The page that we are processing annotations for. + flatten: Whether we flatten text annotations or not. If true, add new font resource + to the page font resources. Otherwise, add them to the AcroForm resources. + acro_form: The root AcroForm dictionary from the PDF catalog. + field: The field dictionary object. + annotation: The widget annotation dictionary object associated with the field. + user_font_name: An optional user-provided font name to override the + default. Defaults to an empty string. + user_font_size: An optional user-provided font size to override the + default. A value of -1 indicates no override. + + Returns: + A new `TextStreamAppearance` instance configured for the given field. + + """ + # Calculate rectangle dimensions + _rectangle = cast(RectangleObject, annotation[AnnotationDictionaryAttributes.Rect]) + # Normalize the rectangle, apply page rotation if applicable + if page.get_inherited("/Rotate") in {90, 270}: + rectangle = RectangleObject((0, 0, abs(_rectangle[3] - _rectangle[1]), abs(_rectangle[2] - _rectangle[0]))) + else: + rectangle = RectangleObject((0, 0, abs(_rectangle[2] - _rectangle[0]), abs(_rectangle[3] - _rectangle[1]))) + + # Get default appearance dictionary from annotation + default_appearance = annotation.get_inherited( + AnnotationDictionaryAttributes.DA, + acro_form.get(AnnotationDictionaryAttributes.DA, None), + ) + if not default_appearance: + # Create a default appearance if none was found in the annotation + default_appearance = TextStringObject("/Helv 0 Tf 0 g") + else: + default_appearance = default_appearance.get_object() + + # Retrieve field text and selected values + field_flags = field.get(FieldDictionaryAttributes.Ff, 0) + if ( + field.get(FieldDictionaryAttributes.FT, "/Tx") == "/Ch" and + field_flags & FieldDictionaryAttributes.FfBits.Combo == 0 + ): + text = "\n".join(annotation.get_inherited(FieldDictionaryAttributes.Opt, [])) + selection = field.get("/V", []) + if not isinstance(selection, list): + selection = [selection] + else: # /Tx + text = field.get("/V", "") + selection = [] + + # Derive font name, size and color from the default appearance. Also set + # user-provided font name and font size in the default appearance, if given. + # For a font name, this presumes that we can find an associated font resource + # dictionary. Uses the variable font_properties as an intermediate. + # As per the PDF spec: + # "At a minimum, the string [that is, default_appearance] shall include a Tf (text + # font) operator along with its two operands, font and size" (Section 12.7.4.3 + # "Variable text" of the PDF 2.0 specification). + font_properties = [prop for prop in re.split(r"\s", default_appearance) if prop] + da_font_name = font_properties.pop(font_properties.index("Tf") - 2) + font_size = float(font_properties.pop(font_properties.index("Tf") - 1)) + font_properties.remove("Tf") + font_color = Color.from_normalized_values(tuple(float(val) for val in font_properties[:-1])) + # Determine the font name to use, prioritizing the user's input + if user_font_name: + font_name = user_font_name + else: + font_name = da_font_name + # Determine the font size to use, prioritizing the user's input + if user_font_size > 0: + font_size = user_font_size + + font_name, font = cls._find_annotation_font_resource(font_name, annotation, acro_form, text) + + # Change the /DA information if we changed the font name + if font_name != da_font_name: + annotation[NameObject("/DA")] = TextStringObject(default_appearance.replace(da_font_name, font_name)) + + # Synchronise font resources + font_resource_reference = cls._sync_appearance_stream_font_resources( + writer, + font_name, + font, + acro_form.setdefault(NameObject("/DR"), DictionaryObject()), + page if flatten else None, + ) + + # Retrieve formatting information + is_comb = False + max_length = None + if field_flags & FieldDictionaryAttributes.FfBits.Comb: + is_comb = True + max_length = annotation.get("/MaxLen") + is_multiline = False + if field_flags & FieldDictionaryAttributes.FfBits.Multiline: + is_multiline = True + alignment = field.get("/Q", TextAlignment.LEFT) + border_width = 1 + border_style = BorderStyles.SOLID + if "/BS" in field: + border_width = cast(DictionaryObject, field["/BS"]).get("/W", border_width) + border_style = cast(DictionaryObject, field["/BS"]).get("/S", border_style) + + rotation = 0 + border_color: Color | None = None + background_color: Color | None = None + appearance_characteristics = field.get_inherited("/MK", None) + if isinstance(appearance_characteristics, DictionaryObject): + rotation = int(appearance_characteristics.get("/R", 0)) + # Color.from_normalized_values([]) results in None for a "/BC" or "/BG" value of [] + border_color = Color.from_normalized_values(appearance_characteristics.get("/BC")) + background_color = Color.from_normalized_values(appearance_characteristics.get("/BG")) + + # Create the TextStreamAppearance instance + layout = BaseStreamConfig( + rectangle=rectangle, + border_width=border_width, + border_style=border_style, + border_color=border_color, + background_color=background_color, + rotation=rotation + ) + + new_appearance_stream = cls( + layout, + text, + selection, + font, + font_resource_reference, + font_name=font_name, + font_size=font_size, + font_color=font_color, + is_multiline=is_multiline, + alignment=alignment, + is_comb=is_comb, + max_length=max_length + ) + + if AnnotationDictionaryAttributes.AP in annotation: + for key, value in ( + cast(DictionaryObject, annotation[AnnotationDictionaryAttributes.AP]).get("/N", {}).items() + ): + if key in {"/BBox", "/Length", "/Subtype", "/Type", "/Filter"}: + continue + # Don't overwrite font resources added by TextAppearanceStream.__init__ + if key == "/Resources": + if "/Font" not in value: + value.get_object()[NameObject("/Font")] = DictionaryObject() + value["/Font"].get_object()[NameObject(font_name)] = getattr( + font_resource_reference, "indirect_reference", font_resource_reference + ) + else: + new_appearance_stream[key] = value + + return new_appearance_stream + + +def transform_annotation_appearance(annotation_obj: DictionaryObject, transformation: Transformation) -> None: + """ + Compose `transformation` into an annotation's /AP /N appearance stream(s), in place. + + Repositioning/resizing an annotation's /Rect alone is not enough: per + the appearance-stream algorithm (PDF 2.0, 12.5.5), a viewer fits the + appearance's /BBox (as mapped by its own /Matrix) into /Rect using an + axis-aligned scale, never a rotation. Left alone, rotating or shearing + a page therefore stretches/skews the untouched appearance content into + the new, differently-shaped /Rect instead of rotating it. Composing the + transform into /Matrix (rather than overwriting it) keeps the rendered + content consistent with the rest of the transformed page while + preserving whatever the annotation already had. Handles both a single + appearance stream and the multi-state /AP /N dict used by + checkbox/radio-button widgets. + """ + if "/AP" not in annotation_obj: + return + ap = cast(DictionaryObject, annotation_obj["/AP"]) + if "/N" not in ap: + return + # __getitem__ already resolves indirect references, so this is never None. + normal_ap = ap["/N"] + # /N is a single appearance stream for most annotations, but for + # widgets with multiple states (checkboxes, radio buttons) it is + # instead a dict of named sub-streams, one per state. Only a stream + # carries its own /BBox/Matrix. + state_aps = ( + normal_ap.values() + if isinstance(normal_ap, DictionaryObject) and not isinstance(normal_ap, StreamObject) + else [normal_ap] + ) + for state_ap in state_aps: + # get_object() is always safe to call: PdfObject.get_object() just + # returns self when the object is already resolved. + state_obj = state_ap.get_object() + if not isinstance(state_obj, StreamObject): + continue + old_matrix = tuple(state_obj.get("/Matrix", (1, 0, 0, 1, 0, 0))) + state_obj[NameObject("/Matrix")] = ArrayObject( + FloatObject(x) for x in Transformation(old_matrix).transform(transformation).ctm + ) diff --git a/app/vendor/pypdf/generic/_base.py b/app/vendor/pypdf/generic/_base.py new file mode 100644 index 0000000..cc5e6d7 --- /dev/null +++ b/app/vendor/pypdf/generic/_base.py @@ -0,0 +1,968 @@ +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +import binascii +import codecs +import hashlib +import re +import sys +from collections.abc import Sequence +from math import log10 +from struct import iter_unpack +from typing import Any, Callable, ClassVar, Optional, Union, cast + +if sys.version_info[:2] >= (3, 10): + from typing import TypeGuard +else: + from typing_extensions import TypeGuard # PEP 647 + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +from .._codecs import _pdfdoc_encoding_rev +from .._protocols import PdfObjectProtocol, PdfWriterProtocol +from .._utils import ( + WHITESPACES, + StreamType, + classproperty, + deprecation_no_replacement, + deprecation_with_replacement, + logger_warning, + read_non_whitespace, + read_until_regex, +) +from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfReadError, PdfStreamError + +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + + +class PdfObject(PdfObjectProtocol): + # function for calculating a hash value + hash_func: Callable[..., "hashlib._Hash"] = hashlib.sha1 + indirect_reference: Optional["IndirectObject"] + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement .hash_bin() so far" + ) + + def hash_value_data(self) -> bytes: + return f"{self}".encode() + + def hash_value(self) -> bytes: + return ( + f"{self.__class__.__name__}:" + f"{self.hash_func(self.hash_value_data()).hexdigest()}" + ).encode() + + def replicate( + self, + pdf_dest: PdfWriterProtocol, + ) -> "PdfObject": + """ + Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter) + without ensuring links. This is used in clone_document_from_root with incremental = True. + + Args: + pdf_dest: Target to clone to. + + Returns: + The cloned PdfObject + + """ + return self.clone(pdf_dest) + + def clone( + self, + pdf_dest: PdfWriterProtocol, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "PdfObject": + """ + Clone object into pdf_dest (PdfWriterProtocol which is an interface for PdfWriter). + + By default, this method will call ``_reference_clone`` (see ``_reference``). + + + Args: + pdf_dest: Target to clone to. + force_duplicate: By default, if the object has already been cloned and referenced, + the copy will be returned; when ``True``, a new copy will be created. + (Default value = ``False``) + ignore_fields: List/tuple of field names (for dictionaries) that will be ignored + during cloning (applies to children duplication as well). If fields are to be + considered for a limited number of levels, you have to add it as integer, for + example ``[1,"/B","/TOTO"]`` means that ``"/B"`` will be ignored at the first + level only but ``"/TOTO"`` on all levels. + + Returns: + The cloned PdfObject + + """ + raise NotImplementedError( + f"{self.__class__.__name__} does not implement .clone so far" + ) + + def _reference_clone( + self, clone: "PdfObject", pdf_dest: PdfWriterProtocol, force_duplicate: bool = False + ) -> "PdfObject": + """ + Reference the object within the _objects of pdf_dest only if + indirect_reference attribute exists (which means the objects was + already identified in xref/xobjstm) if object has been already + referenced do nothing. + + Args: + clone: + pdf_dest: + + Returns: + The clone + + """ + try: + if ( + not force_duplicate + and clone.indirect_reference is not None + and clone.indirect_reference.pdf == pdf_dest + ): + return clone + except Exception: + pass + # if hasattr(clone, "indirect_reference"): + try: + ind = self.indirect_reference + except AttributeError: + return clone + if ( + pdf_dest.incremental + and ind is not None + and ind.pdf == pdf_dest._reader + and ind.idnum <= len(pdf_dest._objects) + ): + i = ind.idnum + else: + i = len(pdf_dest._objects) + 1 + if ind is not None: + if id(ind.pdf) not in pdf_dest._id_translated: + pdf_dest._id_translated[id(ind.pdf)] = {} + pdf_dest._id_translated[id(ind.pdf)]["PreventGC"] = ind.pdf + if ( + not force_duplicate + and ind.idnum in pdf_dest._id_translated[id(ind.pdf)] + ): + obj = pdf_dest.get_object( + pdf_dest._id_translated[id(ind.pdf)][ind.idnum] + ) + assert isinstance(obj, PdfObject), "mypy" + return obj + pdf_dest._id_translated[id(ind.pdf)][ind.idnum] = i + try: + pdf_dest._objects[i - 1] = clone + except IndexError: + pdf_dest._objects.append(clone) + i = len(pdf_dest._objects) + clone.indirect_reference = IndirectObject(i, 0, pdf_dest) + return clone + + def get_object(self) -> Optional["PdfObject"]: + """Resolve indirect references.""" + return self + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + raise NotImplementedError + + +class NullObject(PdfObject): + def clone( + self, + pdf_dest: PdfWriterProtocol, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "NullObject": + """Clone object into pdf_dest.""" + return cast( + "NullObject", self._reference_clone(NullObject(), pdf_dest, force_duplicate) + ) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__,)) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(b"null") + + @staticmethod + def read_from_stream(stream: StreamType) -> "NullObject": + nulltxt = stream.read(4) + if nulltxt != b"null": + raise PdfReadError("Could not read Null object") + return NullObject() + + def __repr__(self) -> str: + return "NullObject" + + def __eq__(self, other: object) -> bool: + return isinstance(other, NullObject) + + def __hash__(self) -> int: + return self.hash_bin() + + +class BooleanObject(PdfObject): + value: bool + + def __init__(self, value: Any) -> None: + self.value = value + + def clone( + self, + pdf_dest: PdfWriterProtocol, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "BooleanObject": + """Clone object into pdf_dest.""" + return cast( + "BooleanObject", + self._reference_clone(BooleanObject(self.value), pdf_dest, force_duplicate), + ) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__, self.value)) + + def __eq__(self, o: object, /) -> bool: + if isinstance(o, BooleanObject): + return self.value == o.value + if isinstance(o, bool): + return self.value == o + return False + + def __hash__(self) -> int: + return self.hash_bin() + + def __repr__(self) -> str: + return "True" if self.value else "False" + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + if self.value: + stream.write(b"true") + else: + stream.write(b"false") + + @staticmethod + def read_from_stream(stream: StreamType) -> "BooleanObject": + word = stream.read(4) + if word == b"true": + return BooleanObject(True) + if word == b"fals": + stream.read(1) + return BooleanObject(False) + raise PdfReadError("Could not read Boolean object") + + +class IndirectObject(PdfObject): + _MAXIMUM_PART_LENGTH = 64 + + def __init__(self, idnum: int, generation: int, pdf: Any) -> None: # PdfReader + self.idnum = idnum + self.generation = generation + self.pdf = pdf + + def __hash__(self) -> int: + return hash((self.idnum, self.generation, id(self.pdf))) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__, self.idnum, self.generation, id(self.pdf))) + + def replicate( + self, + pdf_dest: PdfWriterProtocol, + ) -> "PdfObject": + return IndirectObject(self.idnum, self.generation, pdf_dest) + + def clone( + self, + pdf_dest: PdfWriterProtocol, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "IndirectObject": + """Clone object into pdf_dest.""" + if self.pdf == pdf_dest and not force_duplicate: + # Already duplicated and no extra duplication required + return self + if id(self.pdf) not in pdf_dest._id_translated: + pdf_dest._id_translated[id(self.pdf)] = {} + pdf_dest._id_translated[id(self.pdf)]["PreventGC"] = self.pdf + + if self.idnum in pdf_dest._id_translated[id(self.pdf)]: + dup = pdf_dest.get_object(pdf_dest._id_translated[id(self.pdf)][self.idnum]) + if force_duplicate: + assert dup is not None + assert dup.indirect_reference is not None + idref = dup.indirect_reference + return IndirectObject(idref.idnum, idref.generation, idref.pdf) + else: + obj = self.get_object() + # case observed : a pointed object can not be found + if obj is None: + # this normally + obj = NullObject() + assert isinstance(self, (IndirectObject,)) + obj.indirect_reference = self + dup = pdf_dest._add_object( + obj.clone(pdf_dest, force_duplicate, ignore_fields) + ) + assert isinstance(dup, PdfObject), "mypy" + assert dup.indirect_reference is not None, "mypy" + return dup.indirect_reference + + @property + def indirect_reference(self) -> "IndirectObject": # type: ignore[override] + return self + + def get_object(self) -> Optional["PdfObject"]: + obj: Optional[PdfObject] = self.pdf.get_object(self) + return obj + + def __deepcopy__(self, memo: Any) -> "IndirectObject": + return IndirectObject(self.idnum, self.generation, self.pdf) + + def _get_object_with_check(self) -> Optional["PdfObject"]: + o = self.get_object() + # the check is done here to not slow down get_object() + if isinstance(o, IndirectObject): + raise PdfStreamError( + f"{self.__repr__()} references an IndirectObject {o.__repr__()}" + ) + return o + + def __getattr__(self, name: str) -> Any: + # Attribute not found in object: look in pointed object + try: + return getattr(self._get_object_with_check(), name) + except AttributeError: + raise AttributeError( + f"No attribute {name} found in IndirectObject or pointed object" + ) + + def __getitem__(self, key: Any) -> Any: + # items should be extracted from pointed Object + return self._get_object_with_check()[key] # type: ignore[index] + + def __contains__(self, key: Any) -> bool: + return key in self._get_object_with_check() # type: ignore[operator] + + def __iter__(self) -> Any: + return self._get_object_with_check().__iter__() # type: ignore[union-attr] + + def __float__(self) -> str: + # in this case we are looking for the pointed data + return self.get_object().__float__() # type: ignore[union-attr, no-any-return] + + def __int__(self) -> int: + # in this case we are looking for the pointed data + return self.get_object().__int__() # type: ignore[union-attr, no-any-return] + + def __str__(self) -> str: + # in this case we are looking for the pointed data + return self.get_object().__str__() + + def __repr__(self) -> str: + return f"IndirectObject({self.idnum!r}, {self.generation!r}, {id(self.pdf)})" + + def __eq__(self, other: object) -> bool: + return ( + other is not None + and isinstance(other, IndirectObject) + and self.idnum == other.idnum + and self.generation == other.generation + and self.pdf is other.pdf + ) + + def __ne__(self, other: object) -> bool: + return not self.__eq__(other) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(f"{self.idnum} {self.generation} R".encode()) + + @staticmethod + def read_from_stream(stream: StreamType, pdf: Any) -> "IndirectObject": # PdfReader + def read_part(name: str, skip_leading_whitespace: bool) -> bytes: + result = bytearray() + while True: + tok = stream.read(1) + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + if tok.isspace() or tok in WHITESPACES: + if skip_leading_whitespace and not result: + continue + break + if len(result) >= IndirectObject._MAXIMUM_PART_LENGTH: + raise PdfReadError(f"{name} exceeds maximum length limit of {IndirectObject._MAXIMUM_PART_LENGTH}.") + result += tok + return bytes(result) + + idnum = read_part("Object ID", skip_leading_whitespace=False) + generation = read_part("Generation number", skip_leading_whitespace=True) + + r = read_non_whitespace(stream) + if r != b"R": + raise PdfReadError( + f"Error reading indirect object reference at byte {hex(stream.tell())}" + ) + try: + return IndirectObject(int(idnum), int(generation), pdf) + except (ValueError, OverflowError) as e: + raise PdfReadError( + f"Invalid indirect object reference ({idnum!r} {generation!r} R): {e}" + ) from e + + +FLOAT_WRITE_PRECISION = 8 # shall be min 5 digits max, allow user adj + + +class FloatObject(float, PdfObject): + def __new__( + cls, value: Any = "0.0", context: Optional[Any] = None + ) -> Self: + try: + value = float(value) + return float.__new__(cls, value) + except Exception as e: + # If this isn't a valid decimal (happens in malformed PDFs) + # fallback to 0 + logger_warning( + "%(error)s : FloatObject (%(value)s) invalid; use 0.0 instead", + source=__name__, + error=e, + value=value, + ) + return float.__new__(cls, 0.0) + + def clone( + self, + pdf_dest: Any, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "FloatObject": + """Clone object into pdf_dest.""" + return cast( + "FloatObject", + self._reference_clone(FloatObject(self), pdf_dest, force_duplicate), + ) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__, self.as_numeric)) + + def myrepr(self) -> str: + if self == 0: # type: ignore[comparison-overlap] + return "0.0" + nb = FLOAT_WRITE_PRECISION - int(log10(abs(self))) + return f"{self:.{max(1, nb)}f}".rstrip("0").rstrip(".") + + def __repr__(self) -> str: + return self.myrepr() # repr(float(self)) + + def as_numeric(self) -> float: + return float(self) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(self.myrepr().encode("utf8")) + + +class NumberObject(int, PdfObject): + NumberPattern = re.compile(b"[^+-.0-9]") + _LENGTH_LIMIT = 64 + + def __new__(cls, value: Any) -> Self: + try: + return int.__new__(cls, int(value)) + except ValueError: + logger_warning("NumberObject(%(value)s) invalid; use 0 instead", source=__name__, value=value) + return int.__new__(cls, 0) + + def clone( + self, + pdf_dest: Any, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "NumberObject": + """Clone object into pdf_dest.""" + return cast( + "NumberObject", + self._reference_clone(NumberObject(self), pdf_dest, force_duplicate), + ) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__, self.as_numeric())) + + def as_numeric(self) -> int: + return int(repr(self).encode("utf8")) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(repr(self).encode("utf8")) + + @staticmethod + def read_from_stream(stream: StreamType) -> Union["NumberObject", "FloatObject"]: + num = read_until_regex(stream=stream, regex=NumberObject.NumberPattern, length=NumberObject._LENGTH_LIMIT) + if b"." in num: + return FloatObject(num) + return NumberObject(num) + + +class ByteStringObject(bytes, PdfObject): + """ + Represents a string object where the text encoding could not be determined. + + This occurs quite often, as the PDF spec doesn't provide an alternate way to + represent strings -- for example, the encryption data stored in files (like + /O) is clearly not text, but is still stored in a "String" object. + """ + + def clone( + self, + pdf_dest: Any, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "ByteStringObject": + """Clone object into pdf_dest.""" + return cast( + "ByteStringObject", + self._reference_clone( + ByteStringObject(bytes(self)), pdf_dest, force_duplicate + ), + ) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__, bytes(self))) + + @property + def original_bytes(self) -> bytes: + """For compatibility with TextStringObject.original_bytes.""" + return self + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(b"<") + stream.write(binascii.hexlify(self)) + stream.write(b">") + + def __str__(self) -> str: + charset_to_try = ["utf-16", *list(NameObject.CHARSETS)] + for enc in charset_to_try: + try: + return self.decode(enc) + except UnicodeDecodeError: + pass + raise PdfReadError("Cannot decode ByteStringObject.") + + +class TextStringObject(str, PdfObject): # noqa: SLOT000 + """ + A string object that has been decoded into a real unicode string. + + If read from a PDF document, this string appeared to match the + PDFDocEncoding, or contained a UTF-16BE BOM mark to cause UTF-16 decoding + to occur. + """ + + autodetect_pdfdocencoding: bool + autodetect_utf16: bool + utf16_bom: bytes + _original_bytes: Optional[bytes] = None + + def __new__(cls, value: Any) -> Self: + original_bytes = None + if isinstance(value, bytes): + original_bytes = value + value = value.decode("charmap") + text_string_object = str.__new__(cls, value) + text_string_object._original_bytes = original_bytes + text_string_object.autodetect_utf16 = False + text_string_object.autodetect_pdfdocencoding = False + text_string_object.utf16_bom = b"" + if original_bytes is not None and original_bytes[:2] in {codecs.BOM_UTF16_LE, codecs.BOM_UTF16_BE}: + # The value of `original_bytes` is only set for inputs being `bytes`. + # If this is UTF-16 data according to the BOM (first two characters), + # perform special handling. All other cases should not need any special conversion + # due to already being a string. + try: + text_string_object = str.__new__(cls, original_bytes.decode("utf-16")) + except UnicodeDecodeError as exception: + logger_warning( + "%(exception)s; initial string: %(initial_string)r", + source=__name__, + exception=exception, + initial_string=exception.object, + ) + text_string_object = str.__new__(cls, exception.object[: exception.start].decode("utf-16")) + text_string_object._original_bytes = original_bytes + text_string_object.autodetect_utf16 = True + text_string_object.utf16_bom = original_bytes[:2] + else: + try: + encode_pdfdocencoding(text_string_object) + text_string_object.autodetect_pdfdocencoding = True + except UnicodeEncodeError: + text_string_object.autodetect_utf16 = True + text_string_object.utf16_bom = codecs.BOM_UTF16_BE + return text_string_object + + def clone( + self, + pdf_dest: Any, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "TextStringObject": + """Clone object into pdf_dest.""" + obj = TextStringObject(self) + obj._original_bytes = self._original_bytes + obj.autodetect_pdfdocencoding = self.autodetect_pdfdocencoding + obj.autodetect_utf16 = self.autodetect_utf16 + obj.utf16_bom = self.utf16_bom + return cast( + "TextStringObject", self._reference_clone(obj, pdf_dest, force_duplicate) + ) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__, self.original_bytes)) + + @property + def original_bytes(self) -> bytes: + """ + It is occasionally possible that a text string object gets created where + a byte string object was expected due to the autodetection mechanism -- + if that occurs, this "original_bytes" property can be used to + back-calculate what the original encoded bytes were. + """ + if self._original_bytes is not None: + return self._original_bytes + return self.get_original_bytes() + + def get_original_bytes(self) -> bytes: + # We're a text string object, but the library is trying to get our raw + # bytes. This can happen if we auto-detected this string as text, but + # we were wrong. It's pretty common. Return the original bytes that + # would have been used to create this object, based upon the autodetect + # method. + if self.autodetect_utf16: + if self.utf16_bom == codecs.BOM_UTF16_LE: + return codecs.BOM_UTF16_LE + self.encode("utf-16le") + if self.utf16_bom == codecs.BOM_UTF16_BE: + return codecs.BOM_UTF16_BE + self.encode("utf-16be") + return self.encode("utf-16be") + if self.autodetect_pdfdocencoding: + return encode_pdfdocencoding(self) + raise Exception("no information about original bytes") # pragma: no cover + + def get_encoded_bytes(self) -> bytes: + # Try to write the string out as a PDFDocEncoding encoded string. It's + # nicer to look at in the PDF file. Sadly, we take a performance hit + # here for trying... + try: + if self._original_bytes is not None: + return self._original_bytes + if self.autodetect_utf16: + raise UnicodeEncodeError("", "forced", -1, -1, "") + bytearr = encode_pdfdocencoding(self) + except UnicodeEncodeError: + if self.utf16_bom == codecs.BOM_UTF16_LE: + bytearr = codecs.BOM_UTF16_LE + self.encode("utf-16le") + elif self.utf16_bom == codecs.BOM_UTF16_BE: + bytearr = codecs.BOM_UTF16_BE + self.encode("utf-16be") + else: + bytearr = self.encode("utf-16be") + return bytearr + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + bytearr = self.get_encoded_bytes() + stream.write(b"(") + for c_ in iter_unpack("c", bytearr): + c = cast(bytes, c_[0]) + if not c.isalnum() and c != b" ": + # This: + # stream.write(rf"\{c:0>3o}".encode()) + # gives + # https://github.com/davidhalter/parso/issues/207 + stream.write(b"\\%03o" % ord(c)) + else: + stream.write(c) + stream.write(b")") + + +class NameObject(str, PdfObject): # noqa: SLOT000 + delimiter_pattern = re.compile(rb"\s+|[\(\)<>\[\]{}/%]") + prefix = b"/" + renumber_table: ClassVar[dict[str, bytes]] = { + **{chr(i): f"#{i:02X}".encode() for i in b"#()<>[]{}/%"}, + **{chr(i): f"#{i:02X}".encode() for i in range(33)}, + } + _LENGTH_LIMIT = 4096 + + def clone( + self, + pdf_dest: Any, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "NameObject": + """Clone object into pdf_dest.""" + return cast( + "NameObject", + self._reference_clone(NameObject(self), pdf_dest, force_duplicate), + ) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__, self)) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(self.renumber()) + + def renumber(self) -> bytes: + out = self[0].encode("utf-8") + if out != b"/": + deprecation_no_replacement( + f"Incorrect first char in NameObject, should start with '/': ({self})", + "5.0.0", + ) + parts = [out] + for c in self[1:]: + if c > "~": + parts.extend(f"#{x:02X}".encode() for x in c.encode("utf-8")) + else: + try: + parts.append(self.renumber_table[c]) + except KeyError: + parts.append(c.encode("utf-8")) + return b"".join(parts) + + def _sanitize(self) -> "NameObject": + """ + Sanitize the NameObject's name to be a valid PDF name part + (alphanumeric, underscore, hyphen). The _sanitize method replaces + spaces and any non-alphanumeric/non-underscore/non-hyphen with + underscores. + + Returns: + NameObject with sanitized name. + """ + name = str(self).removeprefix("/") + name = re.sub(r" ", "_", name) + name = re.sub(r"[^a-zA-Z0-9_-]", "_", name) + return NameObject("/" + name) + + @classproperty + def surfix(cls) -> bytes: # noqa: N805 + deprecation_with_replacement("surfix", "prefix", "5.0.0") + return b"/" + + @staticmethod + def unnumber(sin: bytes) -> bytes: + result = bytearray() + i = 0 + while i < len(sin): + if sin[i:i + 1] == b"#": + try: + result.append(int(sin[i + 1 : i + 3], 16)) + i += 3 + continue + except (ValueError, IndexError): + # if the 2 characters after # can not be converted to hex + # we change nothing and carry on + pass + result.append(sin[i]) + i += 1 + return bytes(result) + + CHARSETS = ("utf-8", "gbk", "latin1") + + @staticmethod + def read_from_stream(stream: StreamType, pdf: Any) -> "NameObject": # PdfReader + name = stream.read(1) + if name != NameObject.prefix: + raise PdfReadError("Name read error") + name += read_until_regex(stream=stream, regex=NameObject.delimiter_pattern, length=NameObject._LENGTH_LIMIT) + try: + # Name objects should represent irregular characters + # with a '#' followed by the symbol's hex number + name = NameObject.unnumber(name) + for enc in NameObject.CHARSETS: + try: + ret = name.decode(enc) + return NameObject(ret) + except Exception: + pass + raise UnicodeDecodeError("", name, 0, 0, "Code Not Found") + except (UnicodeEncodeError, UnicodeDecodeError) as e: + if not pdf.strict: + logger_warning( + "Illegal character in NameObject (%(name)r), you may need to adjust NameObject.CHARSETS", + source=__name__, + name=name, + ) + return NameObject(name.decode("charmap")) + raise PdfReadError( + f"Illegal character in NameObject ({name!r}). " + "You may need to adjust NameObject.CHARSETS.", + ) from e + + +def encode_pdfdocencoding(unicode_string: str) -> bytes: + try: + return bytes([_pdfdoc_encoding_rev[k] for k in unicode_string]) + except KeyError: + raise UnicodeEncodeError( + "pdfdocencoding", + unicode_string, + -1, + -1, + "does not exist in translation table", + ) + + +def is_null_or_none(x: Any) -> TypeGuard[Union[NullObject, IndirectObject, None]]: + """ + Returns: + True if x is None or NullObject. + + """ + return x is None or ( + isinstance(x, PdfObject) + and (x.get_object() is None or isinstance(x.get_object(), NullObject)) + ) diff --git a/app/vendor/pypdf/generic/_color.py b/app/vendor/pypdf/generic/_color.py new file mode 100644 index 0000000..053fa83 --- /dev/null +++ b/app/vendor/pypdf/generic/_color.py @@ -0,0 +1,85 @@ +from collections.abc import Sequence +from dataclasses import dataclass, field +from typing import ClassVar, Union + + +@dataclass +class Color: + """ + A factory class to generate one of class GrayscaleColor, RGBColor or CMYKColor. Call with + Color.from_normalized_values() on a tuple of length 1 for grayscale, 3 for RGB, or 4 for CMYK. + """ + color_operator: str = field(init=False) + _ordered_fields: ClassVar[tuple[str, ...]] + + @classmethod + def from_normalized_values( + cls, + color: Union[Sequence[float], None] + ) -> Union["Color", None]: + """ + Method to instantiate a color class. Can be called with value of None for cases where an appearance + characteristics dictionary contains an empty "/BG" or "/BC" value, which returns None and signifies + transparent color. See Table 189, "Entries in an appearance characteristics dictionary" of the PDF + specification 1.7. + + Args: + color: A sequence of 1 (for DeviceGray), 3 (for DeviceRGB) or 4 (for DeviceCMYK) float values + in the range of 0.0 to 1.0 (representing normalized color channel values), or None to return None. + """ + color_types: dict[int, type[Color]] = { + 1: DeviceGray, + 3: DeviceRGB, + 4: DeviceCMYK, + } + + if ( + color is not None + and (color_length := len(color)) in color_types + and all(isinstance(val, (int, float)) and 0.0 <= val <= 1.0 for val in color) + ): + # Create instance of the appropriate subclass + color_subclass = color_types[color_length] + kwargs = dict(zip(color_subclass._ordered_fields, color)) + return color_subclass(**kwargs) + + return None + + def as_operator(self, stroke: bool = False) -> str: + """ + Returns the PDF color operator as a string. + + Args: + stroke: Returns stroke (i.e., uppercase) color operator if True + """ + values = [f"{round(getattr(self, field), 3):g}" for field in self._ordered_fields] + return f"{' '.join(values)} {self.color_operator.upper() if stroke else self.color_operator}" + + +@dataclass +class DeviceGray(Color): + gray: float = 0.0 + + color_operator = "g" + _ordered_fields = ("gray",) + + +@dataclass +class DeviceRGB(Color): + red: float = 0.0 + green: float = 0.0 + blue: float = 0.0 + + color_operator = "rg" + _ordered_fields = ("red", "green", "blue") + + +@dataclass +class DeviceCMYK(Color): + cyan: float = 0.0 + magenta: float = 0.0 + yellow: float = 0.0 + black: float = 1.0 + + color_operator = "k" + _ordered_fields = ("cyan", "magenta", "yellow", "black") diff --git a/app/vendor/pypdf/generic/_data_structures.py b/app/vendor/pypdf/generic/_data_structures.py new file mode 100644 index 0000000..bb836ed --- /dev/null +++ b/app/vendor/pypdf/generic/_data_structures.py @@ -0,0 +1,1900 @@ +# Copyright (c) 2006, Mathieu Fenniak +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +__author__ = "Mathieu Fenniak" +__author_email__ = "biziqe@mathieu.fenniak.net" + +import logging +import os +import re +import sys +from collections.abc import Iterable, Sequence +from io import BytesIO +from math import ceil +from typing import ( + Any, + Callable, + Optional, + Union, + cast, +) + +from .._protocols import PdfReaderProtocol, PdfWriterProtocol, XmpInformationProtocol +from .._utils import ( + WHITESPACES, + BinaryStreamType, + StreamType, + deprecation_no_replacement, + logger_warning, + read_non_whitespace, + read_until_regex, + read_until_whitespace, + skip_over_comment, +) +from ..constants import ( + CheckboxRadioButtonAttributes, + FieldDictionaryAttributes, + OutlineFontFlag, + StreamAttributes, +) +from ..constants import FilterTypes as FT +from ..constants import TypArguments as TA +from ..constants import TypFitArguments as TF +from ..errors import STREAM_TRUNCATED_PREMATURELY, LimitReachedError, PdfReadError, PdfStreamError +from ._base import ( + BooleanObject, + ByteStringObject, + FloatObject, + IndirectObject, + NameObject, + NullObject, + NumberObject, + PdfObject, + TextStringObject, + is_null_or_none, +) +from ._fit import Fit +from ._image_inline import ( + extract_inline__ascii85_decode, + extract_inline__ascii_hex_decode, + extract_inline__dct_decode, + extract_inline__run_length_decode, + extract_inline_default, +) +from ._utils import read_hex_string_from_stream, read_string_from_stream + +if sys.version_info >= (3, 11): + from typing import Self +else: + from typing_extensions import Self + +logger = logging.getLogger(__name__) + +IndirectPattern = re.compile(rb"[+-]?(\d+)\s+(\d+)\s+R[^a-zA-Z]") + + +class ArrayObject(list[Any], PdfObject): + def replicate( + self, + pdf_dest: PdfWriterProtocol, + ) -> "ArrayObject": + arr = cast( + "ArrayObject", + self._reference_clone(ArrayObject(), pdf_dest, False), + ) + for data in self: + if hasattr(data, "replicate"): + arr.append(data.replicate(pdf_dest)) + else: + arr.append(data) + return arr + + def clone( + self, + pdf_dest: PdfWriterProtocol, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "ArrayObject": + """Clone object into pdf_dest.""" + try: + if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore[union-attr] + return self + except Exception: + pass + arr = cast( + "ArrayObject", + self._reference_clone(ArrayObject(), pdf_dest, force_duplicate=True), + ) + for data in self: + if isinstance(data, StreamObject): + dup = data._reference_clone( + data.clone(pdf_dest, force_duplicate, ignore_fields), + pdf_dest, + force_duplicate, + ) + arr.append(dup.indirect_reference) + elif isinstance(data, IndirectObject) and isinstance(resolved := data.get_object(), StreamObject): + dup = data._reference_clone( + resolved.clone(pdf_dest, force_duplicate=True, ignore_fields=ignore_fields), + pdf_dest, + force_duplicate, + ) + arr.append(dup.indirect_reference) + elif hasattr(data, "clone"): + arr.append(data.clone(pdf_dest, force_duplicate, ignore_fields)) + else: + arr.append(data) + return arr + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash((self.__class__, tuple(x.hash_bin() for x in self))) + + def items(self) -> Iterable[Any]: + """Emulate DictionaryObject.items for a list (index, object).""" + return enumerate(self) + + def _to_lst(self, lst: Any) -> list[Any]: + # Convert to list, internal + result: list[Any] + if isinstance(lst, (list, tuple, set)): + result = list(lst) + elif isinstance(lst, PdfObject): + result = [lst] + elif isinstance(lst, str): + if lst[0] == "/": + result = [NameObject(lst)] + else: + result = [TextStringObject(lst)] + elif isinstance(lst, bytes): + result = [ByteStringObject(lst)] + else: # for numbers,... + result = [lst] + return result + + def __add__(self, lst: Any) -> "ArrayObject": + """ + Allow extension by adding list or add one element only + + Args: + lst: any list, tuples are extended the list. + other types(numbers,...) will be appended. + if str is passed it will be converted into TextStringObject + or NameObject (if starting with "/") + if bytes is passed it will be converted into ByteStringObject + + Returns: + ArrayObject with all elements + + """ + temp = ArrayObject(self) + temp.extend(self._to_lst(lst)) + return temp + + def __iadd__(self, lst: Any) -> Self: + """ + Allow extension by adding list or add one element only + + Args: + lst: any list, tuples are extended the list. + other types(numbers,...) will be appended. + if str is passed it will be converted into TextStringObject + or NameObject (if starting with "/") + if bytes is passed it will be converted into ByteStringObject + + """ + self.extend(self._to_lst(lst)) + return self + + def __isub__(self, lst: Any) -> Self: + """Allow to remove items""" + for x in self._to_lst(lst): + try: + index = self.index(x) + del self[index] + except ValueError: + pass + return self + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(b"[") + for data in self: + stream.write(b" ") + data.write_to_stream(stream) + stream.write(b" ]") + + @staticmethod + def read_from_stream( + stream: StreamType, + pdf: Optional[PdfReaderProtocol], + forced_encoding: Union[str, list[str], dict[int, str], None] = None, + ) -> "ArrayObject": + arr = ArrayObject() + tmp = stream.read(1) + if tmp != b"[": + raise PdfReadError("Could not read array") + while True: + # skip leading whitespace + tok = stream.read(1) + while tok.isspace(): + tok = stream.read(1) + if tok == b"": + break + if tok == b"%": + stream.seek(-1, 1) + skip_over_comment(stream) + continue + stream.seek(-1, 1) + # check for array ending + peek_ahead = stream.read(1) + if peek_ahead == b"]": + break + stream.seek(-1, 1) + # read and append object + arr.append(read_object(stream, pdf, forced_encoding)) + return arr + + +class DictionaryObject(dict[Any, Any], PdfObject): + def replicate( + self, + pdf_dest: PdfWriterProtocol, + ) -> "DictionaryObject": + d__ = cast( + "DictionaryObject", + self._reference_clone(self.__class__(), pdf_dest, False), + ) + for k, v in self.items(): + d__[k.replicate(pdf_dest)] = ( + v.replicate(pdf_dest) if hasattr(v, "replicate") else v + ) + return d__ + + def clone( + self, + pdf_dest: PdfWriterProtocol, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "DictionaryObject": + """Clone object into pdf_dest.""" + try: + if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore[union-attr] + return self + except Exception: + pass + + visited: set[tuple[int, int]] = set() # (idnum, generation) + d__ = cast( + "DictionaryObject", + self._reference_clone(self.__class__(), pdf_dest, force_duplicate), + ) + if ignore_fields is None: + ignore_fields = [] + if len(d__.keys()) == 0: + d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited) + return d__ + + def _clone( + self, + src: "DictionaryObject", + pdf_dest: PdfWriterProtocol, + force_duplicate: bool, + ignore_fields: Optional[Sequence[Union[str, int]]], + visited: set[tuple[int, int]], # (idnum, generation) + ) -> None: + """ + Update the object from src. + + Args: + src: "DictionaryObject": + pdf_dest: + force_duplicate: + ignore_fields: + + """ + # First we remove the ignore_fields + # that are for a limited number of levels + assert ignore_fields is not None + ignore_fields = list(ignore_fields) + x = 0 + while x < len(ignore_fields): + if isinstance(ignore_fields[x], int): + if cast(int, ignore_fields[x]) <= 0: + del ignore_fields[x] + del ignore_fields[x] + continue + ignore_fields[x] -= 1 # type:ignore + x += 1 + # Check if this is a chain list, we need to loop to prevent recur + if any( + field not in ignore_fields + and field in src + and isinstance(src.raw_get(field), IndirectObject) + and isinstance(src[field], DictionaryObject) + and ( + src.get("/Type", None) is None + or cast(DictionaryObject, src[field]).get("/Type", None) is None + or src.get("/Type", None) + == cast(DictionaryObject, src[field]).get("/Type", None) + ) + for field in ["/Next", "/Prev", "/N", "/V"] + ): + ignore_fields = list(ignore_fields) + for lst in (("/Next", "/Prev"), ("/N", "/V")): + for k in lst: + objs = [] + if ( + k in src + and k not in self + and isinstance(src.raw_get(k), IndirectObject) + and isinstance(src[k], DictionaryObject) + # If need to go further the idea is to check + # that the types are the same + and ( + src.get("/Type", None) is None + or cast(DictionaryObject, src[k]).get("/Type", None) is None + or src.get("/Type", None) + == cast(DictionaryObject, src[k]).get("/Type", None) + ) + ): + cur_obj: Optional[DictionaryObject] = cast( + "DictionaryObject", src[k] + ) + prev_obj: Optional[DictionaryObject] = self + while cur_obj is not None: + clon = cast( + "DictionaryObject", + cur_obj._reference_clone( + cur_obj.__class__(), pdf_dest, force_duplicate + ), + ) + # Check to see if we've previously processed our item + if clon.indirect_reference is not None: + idnum = clon.indirect_reference.idnum + generation = clon.indirect_reference.generation + if (idnum, generation) in visited: + cur_obj = None + break + visited.add((idnum, generation)) + objs.append((cur_obj, clon)) + assert prev_obj is not None + prev_obj[NameObject(k)] = clon.indirect_reference + prev_obj = clon + try: + if cur_obj == src: + cur_obj = None + else: + cur_obj = cast("DictionaryObject", cur_obj[k]) + except Exception: + cur_obj = None + for s, c in objs: + c._clone( + s, pdf_dest, force_duplicate, ignore_fields, visited + ) + + for k, v in src.items(): + if k not in ignore_fields: + if isinstance(v, StreamObject): + if not hasattr(v, "indirect_reference"): + v.indirect_reference = None + vv = v.clone(pdf_dest, force_duplicate, ignore_fields) + assert vv.indirect_reference is not None + self[k.clone(pdf_dest)] = vv.indirect_reference + elif k not in self: + self[NameObject(k)] = ( + v.clone(pdf_dest, force_duplicate, ignore_fields) + if hasattr(v, "clone") + else v + ) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + return hash( + (self.__class__, tuple(((k, v.hash_bin()) for k, v in self.items()))) + ) + + def raw_get(self, key: Any) -> Any: + return dict.__getitem__(self, key) + + def get_inherited(self, key: str, default: Any = None) -> Any: + """ + Returns the value of a key or from the parent if not found. + If not found returns default. + + Args: + key: string identifying the field to return + + default: default value to return + + Returns: + Current key or inherited one, otherwise default value. + + """ + current = self + visited: set[int] = set() + + while True: + # Detect cyclic parent references + obj_id = id(current) + if obj_id in visited: + raise LimitReachedError(f"Detected cycle in /Parent hierarchy when retrieving value for key {key!r}.") + visited.add(obj_id) + + if key in current: + return current[key] + + if "/Parent" not in current: + return default + + # Walk upward + current = cast( + "DictionaryObject", + current["/Parent"].get_object(), + ) + + def __setitem__(self, key: Any, value: Any) -> Any: + if not isinstance(key, PdfObject): + raise ValueError("Key must be a PdfObject") + if not isinstance(value, PdfObject): + raise ValueError("Value must be a PdfObject") + return dict.__setitem__(self, key, value) + + def setdefault(self, key: Any, value: Optional[Any] = None) -> Any: + if not isinstance(key, PdfObject): + raise ValueError("Key must be a PdfObject") + if not isinstance(value, PdfObject): + raise ValueError("Value must be a PdfObject") + return dict.setdefault(self, key, value) + + def __getitem__(self, key: Any) -> PdfObject: + return cast(PdfObject, dict.__getitem__(self, key).get_object()) + + @property + def xmp_metadata(self) -> Optional[XmpInformationProtocol]: + """ + Retrieve XMP (Extensible Metadata Platform) data relevant to this + object, if available. + + See Table 347 — Additional entries in a metadata stream dictionary. + + Returns: + Returns a :class:`~pypdf.xmp.XmpInformation` instance + that can be used to access XMP metadata from the document. Can also + return None if no metadata was found on the document root. + + """ + from ..xmp import XmpInformation # noqa: PLC0415 + + metadata = self.get("/Metadata", None) + if is_null_or_none(metadata): + return None + assert metadata is not None, "mypy" + metadata = metadata.get_object() + return XmpInformation(metadata) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(b"<<\n") + for key, value in self.items(): + if len(key) > 2 and key[1] == "%" and key[-1] == "%": + continue + key.write_to_stream(stream, encryption_key) + stream.write(b" ") + value.write_to_stream(stream) + stream.write(b"\n") + stream.write(b">>") + + @classmethod + def _get_next_object_position( + cls, position_before: int, position_end: int, generations: list[int], pdf: PdfReaderProtocol + ) -> int: + out = position_end + for generation in generations: + for x in pdf.xref[generation].values(): + if position_before < x <= position_end: + out = min(out, x) + return out + + @classmethod + def _read_unsized_from_stream( + cls, *, stream: BinaryStreamType, pdf: PdfReaderProtocol, length: int, + ) -> bytes: + current_position = stream.tell() + + # Determine stream size. + try: + stream.seek(0, os.SEEK_END) + stream_length = stream.tell() + finally: + stream.seek(current_position) + + object_position = cls._get_next_object_position( + position_before=current_position, position_end=stream_length, generations=list(pdf.xref), pdf=pdf + ) + + bytes_to_read = object_position - current_position + if bytes_to_read >= length: + raise LimitReachedError(f"Requested length of {bytes_to_read} exceeds maximum allowed length.") + + # Read until the next object position. + read_value = stream.read(bytes_to_read) + endstream_position = read_value.find(b"endstream") + if endstream_position < 0: + raise PdfReadError( + f"Unable to find 'endstream' marker for obj starting at {current_position}." + ) + # 9 = len(b"endstream") + stream.seek(current_position + endstream_position + 9) + return read_value[: endstream_position - 1] + + @staticmethod + def read_from_stream( + stream: StreamType, + pdf: Optional[PdfReaderProtocol], + forced_encoding: Union[str, list[str], dict[int, str], None] = None, + ) -> "DictionaryObject": + tmp = stream.read(2) + if tmp != b"<<": + raise PdfReadError( + f"Dictionary read error at byte {hex(stream.tell())}: " + "stream must begin with '<<'" + ) + data: dict[Any, Any] = {} + while True: + tok = read_non_whitespace(stream) + if tok == b"\x00": + continue + if tok == b"%": + stream.seek(-1, 1) + skip_over_comment(stream) + continue + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + + if tok == b">": + stream.read(1) + break + stream.seek(-1, 1) + try: + try: + key = read_object(stream, pdf) + if isinstance(key, NullObject): + break + if not isinstance(key, NameObject): + raise PdfReadError( + f"Expecting a NameObject for key but found {key!r}" + ) + except PdfReadError as exc: + if pdf is not None and pdf.strict: + raise + logger_warning("%(exception)r", source=__name__, exception=exc) + continue + tok = read_non_whitespace(stream) + stream.seek(-1, 1) + value = read_object(stream, pdf, forced_encoding) + except (RecursionError, LimitReachedError) as exc: + raise PdfReadError(exc.__repr__()) + except Exception as exc: + if pdf is not None and pdf.strict: + raise PdfReadError(exc.__repr__()) + logger_warning("%(exception)r", source=__name__, exception=exc) + retval = DictionaryObject() + retval.update(data) + return retval # return partial data + + if key not in data: + data[key] = value + else: + # multiple definitions of key not permitted + msg = ( + "Multiple definitions in dictionary at byte " + "%(position)s for key %(key)s" + ) + values = {"position": hex(stream.tell()), "key": key} + if pdf is not None and pdf.strict: + raise PdfReadError(msg % values) + logger_warning(msg, source=__name__, **values) + + pos = stream.tell() + s = read_non_whitespace(stream) + if s == b"s" and stream.read(5) == b"tream": + eol = stream.read(1) + # Occasional PDF file output has spaces after 'stream' keyword but before EOL. + # patch provided by Danial Sandler + while eol == b" ": + eol = stream.read(1) + if eol not in (b"\n", b"\r"): + raise PdfStreamError("Stream data must be followed by a newline") + if eol == b"\r" and stream.read(1) != b"\n": + stream.seek(-1, 1) + # this is a stream object, not a dictionary + if StreamAttributes.LENGTH not in data: + if pdf is not None and pdf.strict: + raise PdfStreamError("Stream length not defined") + logger_warning( + "Stream length not defined @pos=%(position)d", + source=__name__, + position=stream.tell(), + ) + data[NameObject(StreamAttributes.LENGTH)] = NumberObject(-1) + length = data[StreamAttributes.LENGTH] + if isinstance(length, IndirectObject): + t = stream.tell() + assert pdf is not None, "mypy" + length = pdf.get_object(length) + stream.seek(t, 0) + if length is None: # if the PDF is damaged + length = -1 + pstart = stream.tell() + + from .._configuration import get_configuration # noqa: PLC0415 + configuration = get_configuration() + if length >= 0: + if length > configuration.maximum_declared_stream_length: + raise LimitReachedError(f"Declared stream length of {length} exceeds maximum allowed length.") + + data["__streamdata__"] = stream.read(length) + else: + data["__streamdata__"] = read_until_regex( + stream=stream, regex=re.compile(b"endstream"), length=configuration.maximum_declared_stream_length, + ) + e = read_non_whitespace(stream) + ndstream = stream.read(8) + if (e + ndstream) != b"endstream": + # the odd PDF file has a length that is too long, so + # we need to read backwards to find the "endstream" ending. + # ReportLab (unknown version) generates files with this bug, + # and Python users into PDF files tend to be our audience. + # we need to do this to correct the streamdata and chop off + # an extra character. + pos = stream.tell() + stream.seek(-10, 1) + end = stream.read(9) + if end == b"endstream": + # we found it by looking back one character further. + data["__streamdata__"] = data["__streamdata__"][:-1] + elif pdf is not None and not pdf.strict: + stream.seek(pstart, 0) + data["__streamdata__"] = DictionaryObject._read_unsized_from_stream( + stream=stream, pdf=pdf, length=configuration.maximum_declared_stream_length + ) + pos = stream.tell() + else: + stream.seek(pos, 0) + raise PdfReadError( + "Unable to find 'endstream' marker after stream at byte " + f"{hex(stream.tell())} (nd='{ndstream!r}', end='{end!r}')." + ) + else: + stream.seek(pos, 0) + if "__streamdata__" in data: + return StreamObject.initialize_from_dictionary(data) + retval = DictionaryObject() + retval.update(data) + return retval + + +class TreeObject(DictionaryObject): + def __init__(self, dct: Optional[DictionaryObject] = None) -> None: + DictionaryObject.__init__(self) + if dct: + self.update(dct) + + def has_children(self) -> bool: + return "/First" in self + + def __iter__(self) -> Any: + return self.children() + + def children(self) -> Iterable[Any]: + if not self.has_children(): + return + + child_ref = self[NameObject("/First")] + last = self[NameObject("/Last")] + child = child_ref.get_object() + visited: set[int] = set() + while True: + child_id = id(child) + if child_id in visited: + logger_warning("Detected cycle in outline structure for %(child)s", source=__name__, child=child) + return + visited.add(child_id) + + yield child + + if child == last: + return + child_ref = child.get(NameObject("/Next")) # type: ignore[union-attr] + if is_null_or_none(child_ref): + return + child = child_ref.get_object() + + def add_child(self, child: Any, pdf: PdfWriterProtocol) -> None: + self.insert_child(child, None, pdf) + + def inc_parent_counter_default( + self, parent: Union[IndirectObject, "TreeObject", None], n: int + ) -> None: + if is_null_or_none(parent): + return + assert parent is not None, "mypy" + parent = cast("TreeObject", parent.get_object()) + if "/Count" in parent: + parent[NameObject("/Count")] = NumberObject( + max(0, cast(int, parent[NameObject("/Count")]) + n) + ) + self.inc_parent_counter_default(parent.get("/Parent", None), n) + + def inc_parent_counter_outline( + self, parent: Union[IndirectObject, "TreeObject", None], n: int + ) -> None: + if is_null_or_none(parent): + return + assert parent is not None, "mypy" + parent = cast("TreeObject", parent.get_object()) + # BooleanObject requires comparison with == not is + opn = parent.get("/%is_open%", True) == True # noqa: E712 + c = cast(int, parent.get("/Count", 0)) + if c < 0: + c = abs(c) + parent[NameObject("/Count")] = NumberObject((c + n) * (1 if opn else -1)) + if not opn: + return + self.inc_parent_counter_outline(parent.get("/Parent", None), n) + + def insert_child( + self, + child: Any, + before: Any, + pdf: PdfWriterProtocol, + inc_parent_counter: Optional[Callable[..., Any]] = None, + ) -> IndirectObject: + if inc_parent_counter is None: + inc_parent_counter = self.inc_parent_counter_default + child_obj = child.get_object() + assert child.indirect_reference is not None, "mypy" + child_reference: IndirectObject = child.indirect_reference + + prev: Optional[DictionaryObject] + if "/First" not in self: # no child yet + self[NameObject("/First")] = child_reference + self[NameObject("/Count")] = NumberObject(0) + self[NameObject("/Last")] = child_reference + child_obj[NameObject("/Parent")] = self.indirect_reference + inc_parent_counter(self, child_obj.get("/Count", 1)) + if "/Next" in child_obj: + del child_obj["/Next"] + if "/Prev" in child_obj: + del child_obj["/Prev"] + return child_reference + prev = cast("DictionaryObject", self["/Last"]) + + visited: set[int] = set() + while prev.indirect_reference != before: + prev_id = id(prev) + if prev_id in visited: + raise LimitReachedError("Detected cycle in tree structure.") + visited.add(prev_id) + if "/Next" in prev: + prev = cast("TreeObject", prev["/Next"]) + continue + + # append at the end + prev[NameObject("/Next")] = cast("TreeObject", child_reference) + child_obj[NameObject("/Prev")] = prev.indirect_reference + child_obj[NameObject("/Parent")] = self.indirect_reference + if "/Next" in child_obj: + del child_obj["/Next"] + self[NameObject("/Last")] = child_reference + inc_parent_counter(self, child_obj.get("/Count", 1)) + return child_reference + try: # insert as first or in the middle + assert isinstance(prev["/Prev"], DictionaryObject) + prev["/Prev"][NameObject("/Next")] = child_reference + child_obj[NameObject("/Prev")] = prev["/Prev"] + except Exception: # it means we are inserting in first position + child_obj.pop("/Next", None) + child_obj[NameObject("/Next")] = prev + prev[NameObject("/Prev")] = child_reference + child_obj[NameObject("/Parent")] = self.indirect_reference + inc_parent_counter(self, child_obj.get("/Count", 1)) + return child_reference + + def _remove_node_from_tree( + self, prev: Any, prev_ref: Any, cur: Any, last: Any + ) -> None: + """ + Adjust the pointers of the linked list and tree node count. + + Args: + prev: + prev_ref: + cur: + last: + + """ + next_ref = cur.get(NameObject("/Next"), None) + if prev is None: + if next_ref: + # Removing first tree node + next_obj = next_ref.get_object() + del next_obj[NameObject("/Prev")] + self[NameObject("/First")] = next_ref + self[NameObject("/Count")] = NumberObject( + self[NameObject("/Count")] - 1 # type: ignore[operator] + ) + + else: + # Removing only tree node + self[NameObject("/Count")] = NumberObject(0) + del self[NameObject("/First")] + if NameObject("/Last") in self: + del self[NameObject("/Last")] + else: + if next_ref: + # Removing middle tree node + next_obj = next_ref.get_object() + next_obj[NameObject("/Prev")] = prev_ref + prev[NameObject("/Next")] = next_ref + else: + # Removing last tree node + assert cur == last + del prev[NameObject("/Next")] + self[NameObject("/Last")] = prev_ref + self[NameObject("/Count")] = NumberObject(self[NameObject("/Count")] - 1) # type: ignore[operator] + + def remove_child(self, child: Any) -> None: + child_obj = child.get_object() + child = child_obj.indirect_reference + + if NameObject("/Parent") not in child_obj: + raise ValueError("Removed child does not appear to be a tree item") + if child_obj[NameObject("/Parent")] != self: + raise ValueError("Removed child is not a member of this tree") + + found = False + prev_ref = None + prev = None + cur_ref: Optional[Any] = self[NameObject("/First")] + cur: Optional[dict[str, Any]] = cur_ref.get_object() # type: ignore[union-attr] + last_ref = self[NameObject("/Last")] + last = last_ref.get_object() + while cur is not None: + if cur == child_obj: + TreeObject._remove_node_from_tree(self, prev, prev_ref, cur, last) + found = True + break + + # Go to the next node + prev_ref = cur_ref + prev = cur + if NameObject("/Next") in cur: + cur_ref = cur[NameObject("/Next")] + cur = cur_ref.get_object() + else: + cur_ref = None + cur = None + + if not found: + raise ValueError("Removal couldn't find item in tree") + + _reset_node_tree_relationship(child_obj) + + def remove_from_tree(self) -> None: + """Remove the object from the tree it is in.""" + if "/Parent" not in self: + raise ValueError("Removed child does not appear to be a tree item") + cast("TreeObject", self["/Parent"]).remove_child(self) + + def empty_tree(self) -> None: + for child in self: + child_obj = child.get_object() + _reset_node_tree_relationship(child_obj) + + if NameObject("/Count") in self: + del self[NameObject("/Count")] + if NameObject("/First") in self: + del self[NameObject("/First")] + if NameObject("/Last") in self: + del self[NameObject("/Last")] + + +def _reset_node_tree_relationship(child_obj: Any) -> None: + """ + Call this after a node has been removed from a tree. + + This resets the nodes attributes in respect to that tree. + + Args: + child_obj: + + """ + del child_obj[NameObject("/Parent")] + if NameObject("/Next") in child_obj: + del child_obj[NameObject("/Next")] + if NameObject("/Prev") in child_obj: + del child_obj[NameObject("/Prev")] + + +class StreamObject(DictionaryObject): + def __init__(self) -> None: + self._data: bytes = b"" + self.decoded_self: Optional[DecodedStreamObject] = None + + def replicate( + self, + pdf_dest: PdfWriterProtocol, + ) -> "StreamObject": + d__ = cast( + "StreamObject", + self._reference_clone(self.__class__(), pdf_dest, False), + ) + d__._data = self._data + try: + decoded_self = self.decoded_self + if decoded_self is None: + self.decoded_self = None + else: + self.decoded_self = cast( + "DecodedStreamObject", decoded_self.replicate(pdf_dest) + ) + except Exception: + pass + for k, v in self.items(): + d__[k.replicate(pdf_dest)] = ( + v.replicate(pdf_dest) if hasattr(v, "replicate") else v + ) + return d__ + + def _clone( + self, + src: DictionaryObject, + pdf_dest: PdfWriterProtocol, + force_duplicate: bool, + ignore_fields: Optional[Sequence[Union[str, int]]], + visited: set[tuple[int, int]], + ) -> None: + """ + Update the object from src. + + Args: + src: + pdf_dest: + force_duplicate: + ignore_fields: + + """ + self._data = cast("StreamObject", src)._data + try: + decoded_self = cast("StreamObject", src).decoded_self + if decoded_self is None: + self.decoded_self = None + else: + self.decoded_self = cast( + "DecodedStreamObject", + decoded_self.clone(pdf_dest, force_duplicate, ignore_fields), + ) + except Exception: + pass + super()._clone(src, pdf_dest, force_duplicate, ignore_fields, visited) + + def hash_bin(self) -> int: + """ + Used to detect modified object. + + Returns: + Hash considering type and value. + + """ + # Use _data to prevent errors on non-decoded streams. + return hash((super().hash_bin(), self._data)) + + def get_data(self) -> bytes: + return self._data + + def set_data(self, data: bytes) -> None: + self._data = data + + def hash_value_data(self) -> bytes: + data = super().hash_value_data() + data += self.get_data() + return data + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + self[NameObject(StreamAttributes.LENGTH)] = NumberObject(len(self._data)) + DictionaryObject.write_to_stream(self, stream) + del self[StreamAttributes.LENGTH] + stream.write(b"\nstream\n") + stream.write(self._data) + stream.write(b"\nendstream") + + @staticmethod + def initialize_from_dictionary( + data: dict[str, Any] + ) -> Union["EncodedStreamObject", "DecodedStreamObject"]: + retval: Union[EncodedStreamObject, DecodedStreamObject] + if StreamAttributes.FILTER in data: + retval = EncodedStreamObject() + else: + retval = DecodedStreamObject() + retval._data = data["__streamdata__"] + del data["__streamdata__"] + if StreamAttributes.LENGTH in data: + del data[StreamAttributes.LENGTH] + retval.update(data) + return retval + + def flate_encode(self, level: int = -1) -> "EncodedStreamObject": + from ..filters import FlateDecode # noqa: PLC0415 + + if StreamAttributes.FILTER in self: + f = self[StreamAttributes.FILTER] + if isinstance(f, ArrayObject): + f = ArrayObject([NameObject(FT.FLATE_DECODE), *f]) + try: + params = ArrayObject( + [NullObject(), *self.get(StreamAttributes.DECODE_PARMS, ArrayObject())] + ) + except TypeError: + # case of error where the * operator is not working (not an array + params = ArrayObject( + [NullObject(), self.get(StreamAttributes.DECODE_PARMS, ArrayObject())] + ) + else: + f = ArrayObject([NameObject(FT.FLATE_DECODE), f]) + params = ArrayObject( + [NullObject(), self.get(StreamAttributes.DECODE_PARMS, NullObject())] + ) + else: + f = NameObject(FT.FLATE_DECODE) + params = None + retval = EncodedStreamObject() + retval.update(self) + retval[NameObject(StreamAttributes.FILTER)] = f + if params is not None: + retval[NameObject(StreamAttributes.DECODE_PARMS)] = params + retval._data = FlateDecode.encode(self._data, level) + return retval + + def decode_as_image(self, pillow_parameters: Union[dict[str, Any], None] = None) -> Any: + """ + Try to decode the stream object as an image + + Args: + pillow_parameters: parameters provided to Pillow Image.save() method, + cf. + + Returns: + a PIL image if proper decoding has been found + Raises: + Exception: Errors during decoding will be reported. + It is recommended to catch exceptions to prevent + stops in your program. + + """ + from ._image_xobject import _xobj_to_image # noqa: PLC0415 + + if self.get("/Subtype", "") != "/Image": + try: + logger_warning( # pragma: no cover + "%(indirect_reference)s does not seem to be an Image", + source=__name__, + indirect_reference=self.indirect_reference, + ) + except AttributeError: + logger_warning( # pragma: no cover + "%(obj)r object does not seem to be an Image", + source=__name__, + obj=self, + ) + extension, _, img = _xobj_to_image(self, pillow_parameters) + if extension is None: + return None # pragma: no cover + return img + + +class DecodedStreamObject(StreamObject): + pass + + +class EncodedStreamObject(StreamObject): + def __init__(self) -> None: + self.decoded_self: Optional[DecodedStreamObject] = None + + # This overrides the parent method + def get_data(self) -> bytes: + from ..filters import decode_stream_data # noqa: PLC0415 + + if self.decoded_self is not None: + # Cached version of decoded object + return self.decoded_self.get_data() + + # Create decoded object + decoded = DecodedStreamObject() + decoded.set_data(decode_stream_data(self)) + for key, value in self.items(): + if key not in (StreamAttributes.LENGTH, StreamAttributes.FILTER, StreamAttributes.DECODE_PARMS): + decoded[key] = value + self.decoded_self = decoded + return decoded.get_data() + + # This overrides the parent method: + def set_data(self, data: bytes) -> None: + from ..filters import FlateDecode # noqa: PLC0415 + + if self.get(StreamAttributes.FILTER, "") in (FT.FLATE_DECODE, [FT.FLATE_DECODE]): + if not isinstance(data, bytes): + raise TypeError("Data must be bytes") + if self.decoded_self is None: + self.get_data() # to create self.decoded_self + assert self.decoded_self is not None, "mypy" + self.decoded_self.set_data(data) + super().set_data(FlateDecode.encode(data)) + else: + raise PdfReadError( + "Streams encoded with a filter different from FlateDecode are not supported" + ) + + +CONTENT_STREAM_ARRAY_MAX_LENGTH = 10_000 + + +class ContentStream(DecodedStreamObject): + """ + In order to be fast, this data structure can contain either: + + * raw data in ._data + * parsed stream operations in ._operations. + + At any time, ContentStream object can either have both of those fields defined, + or one field defined and the other set to None. + + These fields are "rebuilt" lazily, when accessed: + + * when .get_data() is called, if ._data is None, it is rebuilt from ._operations. + * when .operations is called, if ._operations is None, it is rebuilt from ._data. + + Conversely, these fields can be invalidated: + + * when .set_data() is called, ._operations is set to None. + * when .operations is set, ._data is set to None. + """ + _OPERATOR_LENGTH_LIMIT = 128 + + def __init__( + self, + stream: Any, + pdf: Any, + forced_encoding: Union[str, list[str], dict[int, str], None] = None, + ) -> None: + self.pdf = pdf + self._operations: list[tuple[Any, bytes]] = [] + + # stream may be a StreamObject or an ArrayObject containing + # StreamObjects to be concatenated together. + if stream is None: + super().set_data(b"") + else: + stream = stream.get_object() + if isinstance(stream, ArrayObject): + from pypdf._configuration import get_configuration # noqa: PLC0415 + configuration = get_configuration() + + if (stream_length := len(stream)) > CONTENT_STREAM_ARRAY_MAX_LENGTH: + raise LimitReachedError( + f"Array-based stream has {stream_length} > {CONTENT_STREAM_ARRAY_MAX_LENGTH} elements." + ) + data = bytearray() + length = 0 + for s in stream: + s_resolved = s.get_object() + if isinstance(s_resolved, NullObject): + continue + if not isinstance(s_resolved, StreamObject): + # No need to emit an exception here for now - the PDF structure + # seems to already be broken beforehand in these cases. + logger_warning( + "Expected StreamObject, got %(type_name)s instead. Data might be wrong.", + source=__name__, + type_name=type(s_resolved).__name__, + ) + else: + new_data = s_resolved.get_data() + length += len(new_data) + if length > configuration.array_based_stream_maximum_output_length: + raise LimitReachedError( + f"Array-based stream has at least {length} > " + f"{configuration.array_based_stream_maximum_output_length} output bytes." + ) + data += new_data + if len(data) == 0 or data[-1:] != b"\n": + # There should be no direct need to check for a change of one byte. + length += 1 + data += b"\n" + super().set_data(bytes(data)) + else: + stream_data = stream.get_data() + assert stream_data is not None + super().set_data(stream_data) + self.forced_encoding = forced_encoding + + def replicate( + self, + pdf_dest: PdfWriterProtocol, + ) -> "ContentStream": + d__ = cast( + "ContentStream", + self._reference_clone(self.__class__(None, None), pdf_dest, False), + ) + d__._data = self._data + try: + decoded_self = self.decoded_self + if decoded_self is None: + self.decoded_self = None + else: + self.decoded_self = cast( + "DecodedStreamObject", decoded_self.replicate(pdf_dest) + ) + except Exception: + pass + for k, v in self.items(): + d__[k.replicate(pdf_dest)] = ( + v.replicate(pdf_dest) if hasattr(v, "replicate") else v + ) + return d__ + d__.set_data(self._data) + d__.pdf = pdf_dest + d__._operations = list(self._operations) + d__.forced_encoding = self.forced_encoding + return d__ + + def clone( + self, + pdf_dest: Any, + force_duplicate: bool = False, + ignore_fields: Optional[Sequence[Union[str, int]]] = (), + ) -> "ContentStream": + """ + Clone object into pdf_dest. + + Args: + pdf_dest: + force_duplicate: + ignore_fields: + + Returns: + The cloned ContentStream + + """ + try: + if self.indirect_reference.pdf == pdf_dest and not force_duplicate: # type: ignore[union-attr] + return self + except Exception: + pass + + visited: set[tuple[int, int]] = set() + d__ = cast( + "ContentStream", + self._reference_clone( + self.__class__(None, None), pdf_dest, force_duplicate + ), + ) + if ignore_fields is None: + ignore_fields = [] + d__._clone(self, pdf_dest, force_duplicate, ignore_fields, visited) + return d__ + + def _clone( + self, + src: DictionaryObject, + pdf_dest: PdfWriterProtocol, + force_duplicate: bool, + ignore_fields: Optional[Sequence[Union[str, int]]], + visited: set[tuple[int, int]], + ) -> None: + """ + Update the object from src. + + Args: + src: + pdf_dest: + force_duplicate: + ignore_fields: + + """ + src_cs = cast("ContentStream", src) + super().set_data(src_cs._data) + self.pdf = pdf_dest + self._operations = list(src_cs._operations) + self.forced_encoding = src_cs.forced_encoding + # no need to call DictionaryObjection or anything + # like super(DictionaryObject,self)._clone(src, pdf_dest, force_duplicate, ignore_fields, visited) + + def _parse_content_stream(self, stream: StreamType) -> None: + # 7.8.2 Content Streams + stream.seek(0, 0) + operands: list[Union[int, str, PdfObject]] = [] + while True: + peek = read_non_whitespace(stream) + if peek in (b"", 0): + break + stream.seek(-1, 1) + if peek.isalpha() or peek in (b"'", b'"'): + operator = read_until_regex( + stream=stream, regex=NameObject.delimiter_pattern, length=self._OPERATOR_LENGTH_LIMIT + ) + if operator == b"BI": + # begin inline image - a completely different parsing + # mechanism is required, of course... thanks buddy... + assert operands == [] + ii = self._read_inline_image(stream) + self._operations.append((ii, b"INLINE IMAGE")) + else: + self._operations.append((operands, operator)) + operands = [] + elif peek == b"%": + # If we encounter a comment in the content stream, we have to + # handle it here. Typically, read_object will handle + # encountering a comment -- but read_object assumes that + # following the comment must be the object we're trying to + # read. In this case, it could be an operator instead. + while peek not in (b"\r", b"\n", b""): + peek = stream.read(1) + else: + operands.append(read_object(stream, None, self.forced_encoding)) + + def _read_inline_image(self, stream: StreamType) -> dict[str, Any]: + # begin reading just after the "BI" - begin image + # first read the dictionary of settings. + settings = DictionaryObject() + while True: + tok = read_non_whitespace(stream) + if not tok: + raise PdfReadError("Unexpected end of stream.") + stream.seek(-1, 1) + if tok == b"I": + # "ID" - begin of image data + break + key = read_object(stream, self.pdf) + tok = read_non_whitespace(stream) + stream.seek(-1, 1) + value = read_object(stream, self.pdf) + settings[key] = value + # left at beginning of ID + tmp = stream.read(3) + assert tmp[:2] == b"ID" + filtr = settings.get("/F", settings.get("/Filter", "not set")) + savpos = stream.tell() + if isinstance(filtr, list): + filtr = filtr[0] if filtr else "not set" # used forencoding + if not isinstance(filtr, str): + # A valid filter is a name or an array of names; anything else + # cannot match the abbreviations below, so treat it as unfiltered. + filtr = "not set" + if "AHx" in filtr or "ASCIIHexDecode" in filtr: + data = extract_inline__ascii_hex_decode(stream) + elif "A85" in filtr or "ASCII85Decode" in filtr: + data = extract_inline__ascii85_decode(stream) + elif "RL" in filtr or "RunLengthDecode" in filtr: + data = extract_inline__run_length_decode(stream) + elif "DCT" in filtr or "DCTDecode" in filtr: + data = extract_inline__dct_decode(stream) + elif filtr == "not set": + cs = settings.get("/CS", "") + if isinstance(cs, list): + cs = cs[0] if cs else "" + if not isinstance(cs, str): + cs = "" + if "RGB" in cs: + lcs: float = 3 + elif "CMYK" in cs: + lcs = 4 + else: + bits = settings.get( + "/BPC", + 8 if cs in {"/I", "/G", "/Indexed", "/DeviceGray"} else -1, + ) + if isinstance(bits, (int, float)) and bits > 0: + lcs = bits / 8.0 + else: + data = extract_inline_default(stream) + lcs = -1 + width = settings.get("/W") + height = settings.get("/H") + if lcs > 0 and isinstance(width, int) and isinstance(height, int): + data = stream.read(ceil(width * lcs) * height) + elif lcs > 0: + # Without usable dimensions the raw sample length is unknown; + # fall back to scanning for the `EI` marker. + data = extract_inline_default(stream) + # Move to the `EI` if possible. + ei = read_non_whitespace(stream) + stream.seek(-1, 1) + else: + data = extract_inline_default(stream) + + ei = stream.read(3) + # An `EI` at the very end of the stream yields only two bytes; rewinding + # unconditionally would step back into the marker (#3468). + if len(ei) == 3: + stream.seek(-1, 1) + ei_trailing = ei[2:3] + if ei[:2] != b"EI" or (ei_trailing != b"" and ei_trailing not in WHITESPACES): + # Deal with wrong/missing `EI` tags. Example: Wrong dimensions specified above. + stream.seek(savpos, 0) + data = extract_inline_default(stream) + ei = stream.read(3) + if len(ei) == 3: + stream.seek(-1, 1) + ei_trailing = ei[2:3] + if ei[:2] != b"EI" or ( + ei_trailing != b"" and ei_trailing not in WHITESPACES + ): # pragma: no cover + # Check the same condition again. This should never fail as + # edge cases are covered by `extract_inline_default` above, + # but check this to make sure that we are behind the `EI` afterwards. + raise PdfStreamError( + f"Could not extract inline image, even using fallback. Expected 'EI', got {ei!r}" + ) + return {"settings": settings, "data": data} + + # This overrides the parent method + def get_data(self) -> bytes: + if not self._data: + new_data = BytesIO() + for operands, operator in self._operations: + if operator == b"INLINE IMAGE": + new_data.write(b"BI") + dict_text = BytesIO() + operands["settings"].write_to_stream(dict_text) + new_data.write(dict_text.getvalue()[2:-2]) + new_data.write(b"ID ") + new_data.write(operands["data"]) + new_data.write(b"EI") + else: + for op in operands: + op.write_to_stream(new_data) + new_data.write(b" ") + new_data.write(operator) + new_data.write(b"\n") + self._data = new_data.getvalue() + return self._data + + # This overrides the parent method + def set_data(self, data: bytes) -> None: + super().set_data(data) + self._operations = [] + + @property + def operations(self) -> list[tuple[Any, bytes]]: + if not self._operations and self._data: + self._parse_content_stream(BytesIO(self._data)) + self._data = b"" + return self._operations + + @operations.setter + def operations(self, operations: list[tuple[Any, bytes]]) -> None: + self._operations = operations + self._data = b"" + + def isolate_graphics_state(self) -> None: + if self._operations: + self._operations.insert(0, ([], b"q")) + self._operations.append(([], b"Q")) + elif self._data: + self._data = b"q\n" + self._data + b"\nQ\n" + + # This overrides the parent method + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if not self._data and self._operations: + self.get_data() # this ensures ._data is rebuilt + super().write_to_stream(stream, encryption_key) + + +def read_object( + stream: StreamType, + pdf: Optional[PdfReaderProtocol], + forced_encoding: Union[str, list[str], dict[int, str], None] = None, +) -> PdfObject: + tok = stream.read(1) + stream.seek(-1, 1) # reset to start + if tok == b"/": + return NameObject.read_from_stream(stream, pdf) + if tok == b"<": + # hexadecimal string OR dictionary + peek = stream.read(2) + stream.seek(-2, 1) # reset to start + if peek == b"<<": + return DictionaryObject.read_from_stream(stream, pdf, forced_encoding) + return read_hex_string_from_stream(stream, forced_encoding) + if tok == b"[": + return ArrayObject.read_from_stream(stream, pdf, forced_encoding) + if tok in (b"t", b"f"): + return BooleanObject.read_from_stream(stream) + if tok == b"(": + return read_string_from_stream(stream, forced_encoding) + if tok == b"e" and stream.read(6) == b"endobj": + return NullObject() + if tok == b"n": + return NullObject.read_from_stream(stream) + if tok == b"%": + # comment + skip_over_comment(stream) + tok = read_non_whitespace(stream) + stream.seek(-1, 1) + return read_object(stream, pdf, forced_encoding) + if tok in b"0123456789+-.": + # number object OR indirect reference + peek = stream.read(20) + stream.seek(-len(peek), 1) # reset to start + if IndirectPattern.match(peek) is not None: + assert pdf is not None, "mypy" + return IndirectObject.read_from_stream(stream, pdf) + return NumberObject.read_from_stream(stream) + pos = stream.tell() + stream.seek(-20, 1) + stream_extract = stream.read(80) + stream.seek(pos) + read_until_whitespace(stream) + raise PdfReadError( + f"Invalid Elementary Object starting with {tok!r} @{pos}: {stream_extract!r}" + ) + + +class Field(TreeObject): + """ + A class representing a field dictionary. + + This class is accessed through + :meth:`get_fields()` + """ + + def __init__(self, data: DictionaryObject) -> None: + DictionaryObject.__init__(self) + field_attributes = ( + FieldDictionaryAttributes.attributes() + + CheckboxRadioButtonAttributes.attributes() + ) + self.indirect_reference = data.indirect_reference + for attr in field_attributes: + try: + self[NameObject(attr)] = data[attr] + except KeyError: + pass + if isinstance(self.get("/V"), EncodedStreamObject): + d = cast(EncodedStreamObject, self[NameObject("/V")]).get_data() + if isinstance(d, bytes): + d_str = d.decode() + elif d is None: + d_str = "" + else: + raise Exception("Should never happen") + self[NameObject("/V")] = TextStringObject(d_str) + + # TABLE 8.69 Entries common to all field dictionaries + @property + def field_type(self) -> Optional[NameObject]: + """Read-only property accessing the type of this field.""" + return self.get(FieldDictionaryAttributes.FT) + + @property + def parent(self) -> Optional[DictionaryObject]: + """Read-only property accessing the parent of this field.""" + return self.get(FieldDictionaryAttributes.Parent) + + @property + def kids(self) -> Optional["ArrayObject"]: + """Read-only property accessing the kids of this field.""" + return self.get(FieldDictionaryAttributes.Kids) + + @property + def name(self) -> Optional[str]: + """Read-only property accessing the name of this field.""" + return self.get(FieldDictionaryAttributes.T) + + @property + def alternate_name(self) -> Optional[str]: + """Read-only property accessing the alternate name of this field.""" + return self.get(FieldDictionaryAttributes.TU) + + @property + def mapping_name(self) -> Optional[str]: + """ + Read-only property accessing the mapping name of this field. + + This name is used by pypdf as a key in the dictionary returned by + :meth:`get_fields()` + """ + return self.get(FieldDictionaryAttributes.TM) + + @property + def flags(self) -> Optional[int]: + """ + Read-only property accessing the field flags, specifying various + characteristics of the field (see Table 8.70 of the PDF 1.7 reference). + """ + return self.get(FieldDictionaryAttributes.Ff) + + @property + def value(self) -> Optional[Any]: + """ + Read-only property accessing the value of this field. + + Format varies based on field type. + """ + return self.get(FieldDictionaryAttributes.V) + + @property + def default_value(self) -> Optional[Any]: + """Read-only property accessing the default value of this field.""" + return self.get(FieldDictionaryAttributes.DV) + + @property + def additional_actions(self) -> Optional[DictionaryObject]: + """ + Read-only property accessing the additional actions dictionary. + + This dictionary defines the field's behavior in response to trigger + events. See Section 8.5.2 of the PDF 1.7 reference. + """ + return self.get(FieldDictionaryAttributes.AA) + + +class Destination(TreeObject): + """ + A class representing a destination within a PDF file. + + See section 12.3.2 of the PDF 2.0 reference. + + Args: + title: Title of this destination. + page: Reference to the page of this destination. Should + be an instance of :class:`IndirectObject`. + fit: How the destination is displayed. + + Raises: + PdfReadError: If destination type is invalid. + + """ + + node: Optional[ + DictionaryObject + ] = None # node provide access to the original Object + + def remove_from_tree(self) -> None: + """ + Remove the outline item this destination was built from. + + `reader.outline` and `writer.outline` yield detached copies rather than + the nodes themselves, so the copy never has a `/Parent` and removing it + would be a no-op. `node` is the dictionary in the document. + """ + if self.node is None: + super().remove_from_tree() + elif "/Parent" not in self.node: + raise ValueError("Removed child does not appear to be a tree item") + else: + TreeObject.remove_child( + cast("TreeObject", self.node["/Parent"]), self.node + ) + + def __init__( + self, + title: Union[str, bytes], + page: Union[NumberObject, IndirectObject, NullObject, DictionaryObject], + fit: Fit, + ) -> None: + self._filtered_children: list[Any] = [] # used in PdfWriter + + typ = fit.fit_type + args = fit.fit_args + + DictionaryObject.__init__(self) + self[NameObject("/Title")] = TextStringObject(title) + self[NameObject("/Page")] = page + self[NameObject("/Type")] = typ + + # from table 8.2 of the PDF 1.7 reference. + if typ == "/XYZ": + if len(args) < 1: # left is missing : should never occur + args.append(NumberObject(0.0)) + if len(args) < 2: # top is missing + args.append(NumberObject(0.0)) + if len(args) < 3: # zoom is missing + args.append(NumberObject(0.0)) + # surplus arguments are ignored rather than failing the unpacking + ( + self[NameObject(TA.LEFT)], + self[NameObject(TA.TOP)], + self[NameObject("/Zoom")], + ) = args[:3] + elif len(args) == 0: + pass + elif typ == TF.FIT_R: + if len(args) == 4: # a wrong number of arguments degrades to null + ( + self[NameObject(TA.LEFT)], + self[NameObject(TA.BOTTOM)], + self[NameObject(TA.RIGHT)], + self[NameObject(TA.TOP)], + ) = args + else: + ( + self[NameObject(TA.LEFT)], + self[NameObject(TA.BOTTOM)], + self[NameObject(TA.RIGHT)], + self[NameObject(TA.TOP)], + ) = (NullObject(), NullObject(), NullObject(), NullObject()) + elif typ in [TF.FIT_H, TF.FIT_BH]: + try: # Prefer to be more robust not only to null parameters + (self[NameObject(TA.TOP)],) = args + except Exception: + (self[NameObject(TA.TOP)],) = (NullObject(),) + elif typ in [TF.FIT_V, TF.FIT_BV]: + try: # Prefer to be more robust not only to null parameters + (self[NameObject(TA.LEFT)],) = args + except Exception: + (self[NameObject(TA.LEFT)],) = (NullObject(),) + elif typ in [TF.FIT, TF.FIT_B]: + pass + else: + raise PdfReadError(f"Unknown Destination Type: {typ!r}") + + @property + def dest_array(self) -> "ArrayObject": + return ArrayObject( + [self.raw_get("/Page"), self["/Type"]] + + [ + self[x] + for x in ["/Left", "/Bottom", "/Right", "/Top", "/Zoom"] + if x in self + ] + ) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(b"<<\n") + key = NameObject("/D") + key.write_to_stream(stream) + stream.write(b" ") + value = self.dest_array + value.write_to_stream(stream) + + key = NameObject("/S") + key.write_to_stream(stream) + stream.write(b" ") + value_s = NameObject("/GoTo") + value_s.write_to_stream(stream) + + stream.write(b"\n") + stream.write(b">>") + + @property + def title(self) -> Optional[str]: + """Read-only property accessing the destination title.""" + return self.get("/Title") + + @property + def page(self) -> Optional[IndirectObject]: + """Read-only property accessing the IndirectObject of the destination page.""" + return self.get("/Page") + + @property + def typ(self) -> Optional[str]: + """Read-only property accessing the destination type.""" + return self.get("/Type") + + @property + def zoom(self) -> Optional[int]: + """Read-only property accessing the zoom factor.""" + return self.get("/Zoom", None) + + @property + def left(self) -> Optional[FloatObject]: + """Read-only property accessing the left horizontal coordinate.""" + return self.get("/Left", None) + + @property + def right(self) -> Optional[FloatObject]: + """Read-only property accessing the right horizontal coordinate.""" + return self.get("/Right", None) + + @property + def top(self) -> Optional[FloatObject]: + """Read-only property accessing the top vertical coordinate.""" + return self.get("/Top", None) + + @property + def bottom(self) -> Optional[FloatObject]: + """Read-only property accessing the bottom vertical coordinate.""" + return self.get("/Bottom", None) + + @property + def color(self) -> Optional["ArrayObject"]: + """Read-only property accessing the color in (R, G, B) with values 0.0-1.0.""" + return cast( + "ArrayObject", + self.get("/C", ArrayObject([FloatObject(0), FloatObject(0), FloatObject(0)])), + ) + + @property + def font_format(self) -> Optional[OutlineFontFlag]: + """ + Read-only property accessing the font type. + + 1=italic, 2=bold, 3=both + """ + return OutlineFontFlag(self.get("/F", 0)) + + @property + def outline_count(self) -> Optional[int]: + """ + Read-only property accessing the outline count. + + positive = expanded + negative = collapsed + absolute value = number of visible descendants at all levels + """ + return self.get("/Count", None) diff --git a/app/vendor/pypdf/generic/_files.py b/app/vendor/pypdf/generic/_files.py new file mode 100644 index 0000000..f8b1ae0 --- /dev/null +++ b/app/vendor/pypdf/generic/_files.py @@ -0,0 +1,465 @@ +from __future__ import annotations + +import bisect +from functools import cached_property +from typing import TYPE_CHECKING, cast + +from pypdf._utils import format_iso8824_date, logger_warning, parse_iso8824_date +from pypdf.constants import CatalogAttributes as CA +from pypdf.constants import FileSpecificationDictionaryEntries +from pypdf.constants import PageAttributes as PG +from pypdf.errors import PdfReadError, PyPdfError +from pypdf.generic import ( + ArrayObject, + ByteStringObject, + DecodedStreamObject, + DictionaryObject, + NameObject, + NullObject, + NumberObject, + StreamObject, + TextStringObject, + is_null_or_none, +) + +if TYPE_CHECKING: + import datetime + from collections.abc import Iterator + + from pypdf._writer import PdfWriter + + +class EmbeddedFile: + """ + Container holding the information on an embedded file. + + Attributes are evaluated lazily if possible. + + Further information on embedded files can be found in section 7.11 of the PDF 2.0 specification. + """ + def __init__(self, name: str, pdf_object: DictionaryObject, parent: ArrayObject | None = None) -> None: + """ + Args: + name: The (primary) name as provided in the name tree. + pdf_object: The corresponding PDF object to allow retrieving further data. + parent: The parent list. + """ + self._name = name + self.pdf_object = pdf_object + self._parent = parent + + @property + def name(self) -> str: + """ + The (primary) name of the embedded file as provided in the name tree. + + .. warning:: + + This value can contain arbitrary characters. Please make sure to sanitize it before + using it to write the file content to the disk for example. + """ + return self._name + + @classmethod + def _create_new(cls, writer: PdfWriter, name: str, content: str | bytes) -> EmbeddedFile: + """ + Create a new embedded file and add it to the PdfWriter. + + Args: + writer: The PdfWriter instance to add the embedded file to. + name: The filename to display. + content: The data in the file. + + Returns: + EmbeddedFile instance for the newly created embedded file. + """ + # Convert string content to bytes if needed + if isinstance(content, str): + content = content.encode("latin-1") + + # Create the file entry (the actual embedded file stream) + file_entry = DecodedStreamObject() + file_entry.set_data(content) + file_entry.update({NameObject(PG.TYPE): NameObject("/EmbeddedFile")}) + + # Create the /EF entry + ef_entry = DictionaryObject() + ef_entry.update({NameObject("/F"): writer._add_object(file_entry)}) + + # Create the filespec dictionary + from pypdf.generic import create_string_object # noqa: PLC0415 + filespec = DictionaryObject() + filespec_reference = writer._add_object(filespec) + name_object = cast(TextStringObject, create_string_object(name)) + filespec.update( + { + NameObject(PG.TYPE): NameObject("/Filespec"), + NameObject(FileSpecificationDictionaryEntries.F): name_object, + NameObject(FileSpecificationDictionaryEntries.EF): ef_entry, + } + ) + + # Add the name and filespec to the names array. + # We use the inverse order for insertion, as this allows us to re-use the + # same index. + names_array = cls._get_names_array(writer) + insertion_index = cls._get_insertion_index(names_array, name_object) + names_array.insert(insertion_index, filespec_reference) + names_array.insert(insertion_index, name_object) + + # Return an EmbeddedFile instance + return cls(name=name, pdf_object=filespec, parent=names_array) + + @classmethod + def _get_names_array(cls, writer: PdfWriter) -> ArrayObject: + """Get the names array for embedded files, possibly creating and flattening it.""" + if CA.NAMES not in writer.root_object: + # Add the /Names entry to the catalog. + writer.root_object[NameObject(CA.NAMES)] = writer._add_object(DictionaryObject()) + + names_dict = cast(DictionaryObject, writer.root_object[CA.NAMES]) + if "/EmbeddedFiles" not in names_dict: + # We do not yet have an entry for embedded files. Create and return it. + names = ArrayObject() + embedded_files_names_dictionary = DictionaryObject( + {NameObject(CA.NAMES): names} + ) + names_dict[NameObject("/EmbeddedFiles")] = writer._add_object(embedded_files_names_dictionary) + return names + + # We have an existing embedded files entry. + embedded_files_names_tree = cast(DictionaryObject, names_dict["/EmbeddedFiles"]) + if "/Names" in embedded_files_names_tree: + # Simple case: We already have a flat list. + return cast(ArrayObject, embedded_files_names_tree[NameObject(CA.NAMES)]) + if "/Kids" not in embedded_files_names_tree: + # Invalid case: This is no name tree. + raise PdfReadError("Got neither Names nor Kids in embedded files tree.") + + # Complex case: Convert a /Kids-based name tree to a /Names-based one. + # /Name-based ones are much easier to handle and allow us to simplify the + # actual insertion logic by only having to consider one case. + names = ArrayObject() + kids = cast(ArrayObject, embedded_files_names_tree["/Kids"].get_object()) + embedded_files_names_dictionary = DictionaryObject( + {NameObject(CA.NAMES): names} + ) + names_dict[NameObject("/EmbeddedFiles")] = writer._add_object(embedded_files_names_dictionary) + for kid in kids: + # Write the flattened file entries. As we do not change the actual files, + # this should not have any impact on references to them. + # There might be further (nested) kids here. + # Wait for an example before evaluating an implementation. + for name in kid.get_object().get("/Names", []): + names.append(name) + return names + + @classmethod + def _get_insertion_index(cls, names_array: ArrayObject, name: str) -> int: + keys = [names_array[i].encode("utf-8") for i in range(0, len(names_array), 2)] + name_bytes = name.encode("utf-8") + + start = bisect.bisect_left(keys, name_bytes) + end = bisect.bisect_right(keys, name_bytes) + + if start != end: + return end * 2 + if start == 0: + return 0 + if start == (key_count := len(keys)): + return key_count * 2 + return end * 2 + + @property + def alternative_name(self) -> str | None: + """ + Retrieve the alternative name (as per the file specification dictionary). + + .. warning:: + + This value can contain arbitrary characters. Please make sure to sanitize it before + using it to write the file content to the disk for example. + """ + for key in [FileSpecificationDictionaryEntries.UF, FileSpecificationDictionaryEntries.F]: + # PDF 2.0 reference, table 43: + # > A PDF reader shall use the value of the UF key, when present, instead of the F key. + if key in self.pdf_object: + value = self.pdf_object[key].get_object() + if not is_null_or_none(value): + return cast(str, value) + return None + + @alternative_name.setter + def alternative_name(self, value: TextStringObject | None) -> None: + """Set the alternative name (as per the file specification dictionary).""" + if value is None: + if FileSpecificationDictionaryEntries.UF in self.pdf_object: + self.pdf_object[NameObject(FileSpecificationDictionaryEntries.UF)] = NullObject() + if FileSpecificationDictionaryEntries.F in self.pdf_object: + self.pdf_object[NameObject(FileSpecificationDictionaryEntries.F)] = NullObject() + else: + self.pdf_object[NameObject(FileSpecificationDictionaryEntries.UF)] = value + self.pdf_object[NameObject(FileSpecificationDictionaryEntries.F)] = value + + @property + def description(self) -> str | None: + """Retrieve the description.""" + value = self.pdf_object.get(FileSpecificationDictionaryEntries.DESC) + if is_null_or_none(value): + return None + return value + + @description.setter + def description(self, value: TextStringObject | None) -> None: + """Set the description.""" + if value is None: + self.pdf_object[NameObject(FileSpecificationDictionaryEntries.DESC)] = NullObject() + else: + self.pdf_object[NameObject(FileSpecificationDictionaryEntries.DESC)] = value + + @property + def associated_file_relationship(self) -> str: + """Retrieve the relationship of the referring document to this embedded file.""" + return cast( + NameObject, + self.pdf_object.get("/AFRelationship", NameObject("/Unspecified")), + ) + + @associated_file_relationship.setter + def associated_file_relationship(self, value: NameObject) -> None: + """Set the relationship of the referring document to this embedded file.""" + self.pdf_object[NameObject("/AFRelationship")] = value + + @property + def _embedded_file(self) -> StreamObject: + """Retrieve the actual embedded file stream.""" + if "/EF" not in self.pdf_object: + raise PdfReadError(f"/EF entry not found: {self.pdf_object}") + ef = cast(DictionaryObject, self.pdf_object["/EF"]) + for key in [FileSpecificationDictionaryEntries.UF, FileSpecificationDictionaryEntries.F]: + if key in ef: + return cast(StreamObject, ef[key].get_object()) + raise PdfReadError(f"No /(U)F key found in file dictionary: {ef}") + + @property + def _params(self) -> DictionaryObject: + """Retrieve the file-specific parameters.""" + return cast(DictionaryObject, self._embedded_file.get("/Params", DictionaryObject()).get_object()) + + @cached_property + def _ensure_params(self) -> DictionaryObject: + """Ensure the /Params dictionary exists and return it.""" + embedded_file = self._embedded_file + if "/Params" not in embedded_file: + embedded_file[NameObject("/Params")] = DictionaryObject() + return cast(DictionaryObject, embedded_file["/Params"]) + + @property + def subtype(self) -> str | None: + """Retrieve the subtype. This is a MIME media type, prefixed by a slash.""" + value = self._embedded_file.get("/Subtype") + if is_null_or_none(value): + return None + return value + + @subtype.setter + def subtype(self, value: NameObject | None) -> None: + """Set the subtype. This should be a MIME media type, prefixed by a slash.""" + embedded_file = self._embedded_file + if value is None: + embedded_file[NameObject("/Subtype")] = NullObject() + else: + embedded_file[NameObject("/Subtype")] = value + + @property + def content(self) -> bytes: + """Retrieve the actual file content.""" + return self._embedded_file.get_data() + + @content.setter + def content(self, value: str | bytes) -> None: + """Set the file content.""" + if isinstance(value, str): + value = value.encode("latin-1") + self._embedded_file.set_data(value) + + @property + def size(self) -> int | None: + """Retrieve the size of the uncompressed file in bytes.""" + value = self._params.get("/Size") + if is_null_or_none(value): + return None + return value + + @size.setter + def size(self, value: NumberObject | None) -> None: + """Set the size of the uncompressed file in bytes.""" + params = self._ensure_params + if value is None: + params[NameObject("/Size")] = NullObject() + else: + params[NameObject("/Size")] = value + + @property + def creation_date(self) -> datetime.datetime | None: + """Retrieve the file creation datetime.""" + return parse_iso8824_date(self._params.get("/CreationDate")) + + @creation_date.setter + def creation_date(self, value: datetime.datetime | None) -> None: + """Set the file creation datetime.""" + params = self._ensure_params + if value is None: + params[NameObject("/CreationDate")] = NullObject() + else: + date_str = format_iso8824_date(value) + params[NameObject("/CreationDate")] = TextStringObject(date_str) + + @property + def modification_date(self) -> datetime.datetime | None: + """Retrieve the datetime of the last file modification.""" + return parse_iso8824_date(self._params.get("/ModDate")) + + @modification_date.setter + def modification_date(self, value: datetime.datetime | None) -> None: + """Set the datetime of the last file modification.""" + params = self._ensure_params + if value is None: + params[NameObject("/ModDate")] = NullObject() + else: + date_str = format_iso8824_date(value) + params[NameObject("/ModDate")] = TextStringObject(date_str) + + @property + def checksum(self) -> bytes | None: + """Retrieve the MD5 checksum of the (uncompressed) file.""" + value = self._params.get("/CheckSum") + if is_null_or_none(value): + return None + return value + + @checksum.setter + def checksum(self, value: ByteStringObject | None) -> None: + """Set the MD5 checksum of the (uncompressed) file.""" + params = self._ensure_params + if value is None: + params[NameObject("/CheckSum")] = NullObject() + else: + params[NameObject("/CheckSum")] = value + + @property + def _names(self) -> Iterator[str]: + yield self.name + if self.alternative_name is not None and self.alternative_name != self.name: + yield self.alternative_name + + def delete(self) -> None: + """Delete the file from the document.""" + if not self._parent: + raise PyPdfError("Parent required to delete file from document.") + if self.pdf_object in self._parent: + index = self._parent.index(self.pdf_object) + elif ( + (indirect_reference := getattr(self.pdf_object, "indirect_reference", None)) is not None + and indirect_reference in self._parent + ): + index = self._parent.index(indirect_reference) + else: + raise PyPdfError("File not found in parent object.") + self._parent.pop(index) # Reference. + self._parent.pop(index - 1) # Name. + self.pdf_object = DictionaryObject() # Invalidate. + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} name={self.name!r}>" + + @staticmethod + def _report_malformed(message: str, strict: bool) -> None: + """Raise an exception in strict mode, warn otherwise.""" + if strict: + raise PdfReadError(message) + logger_warning(message, source=__name__) + + @classmethod + def _load_from_names(cls, names: ArrayObject) -> Iterator[EmbeddedFile]: + """ + Convert the given name tree into class instances. + + Args: + names: The name tree to load the data from. + + Returns: + Iterable of class instances for the files found. + """ + # This is a name tree of the format [name_1, reference_1, name_2, reference_2, ...] + for i, name in enumerate(names): + if not isinstance(name, str): + # Skip plain strings and retrieve them as `direct_name` by index. + file_dictionary = name.get_object() + direct_name = names[i - 1].get_object() + yield EmbeddedFile(name=direct_name, pdf_object=file_dictionary, parent=names) + + @classmethod + def _load(cls, catalog: DictionaryObject, strict: bool = False) -> Iterator[EmbeddedFile]: + """ + Load the embedded files for the given document catalog. + + This method and its signature are considered internal API and thus not exposed publicly for now. + + Args: + catalog: The document catalog to load from. + strict: Whether to raise an exception on malformed name trees + instead of issuing a warning and skipping them. + + Returns: + Iterable of class instances for the files found. + """ + if "/Names" not in catalog: + return + names = catalog["/Names"] + if isinstance(names, NullObject): + # A null value is equivalent to an omitted entry. + return + if not isinstance(names, DictionaryObject): + cls._report_malformed(f"Names tree is not a dictionary: {names}", strict=strict) + return + if "/EmbeddedFiles" not in names: + return + container = names["/EmbeddedFiles"] + if isinstance(container, NullObject): + return + if not isinstance(container, DictionaryObject): + cls._report_malformed(f"Embedded files entry is not a dictionary: {container}", strict=strict) + return + + if "/Kids" in container: + kids = container["/Kids"].get_object() + if isinstance(kids, NullObject): + kids = ArrayObject() + elif not isinstance(kids, ArrayObject): + cls._report_malformed(f"Embedded files /Kids is not an array: {kids}", strict=strict) + return + for kid in kids: + # There might be further (nested) kids here. + # Wait for an example before evaluating an implementation. + kid = kid.get_object() + if not isinstance(kid, DictionaryObject): + cls._report_malformed(f"Embedded files kid is not a dictionary: {kid}", strict=strict) + continue + if "/Names" not in kid: + continue + kid_names = kid["/Names"].get_object() + if isinstance(kid_names, NullObject): + continue + if not isinstance(kid_names, ArrayObject): + cls._report_malformed(f"Embedded files name list is not an array: {kid_names}", strict=strict) + continue + yield from cls._load_from_names(kid_names) + if "/Names" in container: + container_names = container["/Names"].get_object() + if isinstance(container_names, NullObject): + return + if not isinstance(container_names, ArrayObject): + cls._report_malformed(f"Embedded files name list is not an array: {container_names}", strict=strict) + return + yield from cls._load_from_names(container_names) diff --git a/app/vendor/pypdf/generic/_fit.py b/app/vendor/pypdf/generic/_fit.py new file mode 100644 index 0000000..596ff27 --- /dev/null +++ b/app/vendor/pypdf/generic/_fit.py @@ -0,0 +1,175 @@ +from collections.abc import Sequence +from typing import Any, Optional, Union + +from ._base import is_null_or_none + + +class Fit: + def __init__( + self, fit_type: str, fit_args: Sequence[Union[float, Any, None]] = () + ) -> None: + from ._base import FloatObject, NameObject, NullObject, NumberObject # noqa: PLC0415 + + self.fit_type = NameObject(fit_type) + self.fit_args: list[Union[NullObject, FloatObject, NumberObject]] = [ + NullObject() if is_null_or_none(a) else FloatObject(a) for a in fit_args + ] + + @classmethod + def xyz( + cls, + left: Optional[float] = None, + top: Optional[float] = None, + zoom: Optional[float] = None, + ) -> "Fit": + """ + Display the page designated by page, with the coordinates (left, top) + positioned at the upper-left corner of the window and the contents + of the page magnified by the factor zoom. + + A null value for any of the parameters left, top, or zoom specifies + that the current value of that parameter is to be retained unchanged. + + A zoom value of 0 has the same meaning as a null value. + + Args: + left: + top: + zoom: + + Returns: + The created fit object. + + """ + return Fit(fit_type="/XYZ", fit_args=(left, top, zoom)) + + @classmethod + def fit(cls) -> "Fit": + """ + Display the page designated by page, with its contents magnified just + enough to fit the entire page within the window both horizontally and + vertically. + + If the required horizontal and vertical magnification factors are + different, use the smaller of the two, centering the page within the + window in the other dimension. + """ + return Fit(fit_type="/Fit") + + @classmethod + def fit_horizontally(cls, top: Optional[float] = None) -> "Fit": + """ + Display the page designated by page, with the vertical coordinate top + positioned at the top edge of the window and the contents of the page + magnified just enough to fit the entire width of the page within the + window. + + A null value for ``top`` specifies that the current value of that + parameter is to be retained unchanged. + + Args: + top: + + Returns: + The created fit object. + + """ + return Fit(fit_type="/FitH", fit_args=(top,)) + + @classmethod + def fit_vertically(cls, left: Optional[float] = None) -> "Fit": + return Fit(fit_type="/FitV", fit_args=(left,)) + + @classmethod + def fit_rectangle( + cls, + left: Optional[float] = None, + bottom: Optional[float] = None, + right: Optional[float] = None, + top: Optional[float] = None, + ) -> "Fit": + """ + Display the page designated by page, with its contents magnified + just enough to fit the rectangle specified by the coordinates + left, bottom, right, and top entirely within the window + both horizontally and vertically. + + If the required horizontal and vertical magnification factors are + different, use the smaller of the two, centering the rectangle within + the window in the other dimension. + + A null value for any of the parameters may result in unpredictable + behavior. + + Args: + left: + bottom: + right: + top: + + Returns: + The created fit object. + + """ + return Fit(fit_type="/FitR", fit_args=(left, bottom, right, top)) + + @classmethod + def fit_box(cls) -> "Fit": + """ + Display the page designated by page, with its contents magnified just + enough to fit its bounding box entirely within the window both + horizontally and vertically. + + If the required horizontal and vertical magnification factors are + different, use the smaller of the two, centering the bounding box + within the window in the other dimension. + """ + return Fit(fit_type="/FitB") + + @classmethod + def fit_box_horizontally(cls, top: Optional[float] = None) -> "Fit": + """ + Display the page designated by page, with the vertical coordinate top + positioned at the top edge of the window and the contents of the page + magnified just enough to fit the entire width of its bounding box + within the window. + + A null value for top specifies that the current value of that parameter + is to be retained unchanged. + + Args: + top: + + Returns: + The created fit object. + + """ + return Fit(fit_type="/FitBH", fit_args=(top,)) + + @classmethod + def fit_box_vertically(cls, left: Optional[float] = None) -> "Fit": + """ + Display the page designated by page, with the horizontal coordinate + left positioned at the left edge of the window and the contents of the + page magnified just enough to fit the entire height of its bounding box + within the window. + + A null value for left specifies that the current value of that + parameter is to be retained unchanged. + + Args: + left: + + Returns: + The created fit object. + + """ + return Fit(fit_type="/FitBV", fit_args=(left,)) + + def __str__(self) -> str: + if not self.fit_args: + return f"Fit({self.fit_type})" + return f"Fit({self.fit_type}, {self.fit_args})" + + +DEFAULT_FIT = Fit.fit() diff --git a/app/vendor/pypdf/generic/_image_inline.py b/app/vendor/pypdf/generic/_image_inline.py new file mode 100644 index 0000000..9ed3404 --- /dev/null +++ b/app/vendor/pypdf/generic/_image_inline.py @@ -0,0 +1,365 @@ +# Copyright (c) 2024, pypdf contributors +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +import logging +from io import BytesIO +from typing import IO + +from .._utils import ( + WHITESPACES, + WHITESPACES_AS_BYTES, + BinaryStreamType, + StreamType, + logger_warning, + read_non_whitespace, +) +from ..errors import PdfReadError + +logger = logging.getLogger(__name__) + +# An inline image should be used only for small images (4096 bytes or less), +# but allow twice this for cases where this has been exceeded. +BUFFER_SIZE = 8192 + + +def _check_end_image_marker(stream: StreamType) -> bool: + ei_tok = read_non_whitespace(stream) + ei_tok += stream.read(2) + stream.seek(-3, 1) + return ei_tok[:2] == b"EI" and (ei_tok[2:3] == b"" or ei_tok[2:3] in WHITESPACES) + + +def extract_inline__ascii_hex_decode(stream: StreamType) -> bytes: + """ + Extract HexEncoded stream from inline image. + The stream will be moved onto the EI. + """ + data_out = bytearray() + # Read data until delimiter > and EI as backup. + while True: + data_buffered = read_non_whitespace(stream) + (read_bytes := stream.read(BUFFER_SIZE)) + if not data_buffered or not read_bytes: + raise PdfReadError("Unexpected end of stream.") + pos_tok = data_buffered.find(b">") + if pos_tok >= 0: # found > + data_out += data_buffered[: pos_tok + 1] + stream.seek(-len(data_buffered) + pos_tok + 1, 1) + break + pos_ei = data_buffered.find(b"EI") + if pos_ei >= 0: # found EI + stream.seek(-len(data_buffered) + pos_ei - 1, 1) + c = stream.read(1) + while c in WHITESPACES: + stream.seek(-2, 1) + c = stream.read(1) + pos_ei -= 1 + data_out += data_buffered[:pos_ei] + break + if len(data_buffered) == 2: + data_out += data_buffered + raise PdfReadError("Unexpected end of stream.") + # Neither > nor EI found + data_out += data_buffered[:-2] + stream.seek(-2, 1) + + if not _check_end_image_marker(stream): + raise PdfReadError("EI stream not found.") + return bytes(data_out) + + +def extract_inline__ascii85_decode(stream: StreamType) -> bytes: + """ + Extract A85 stream from inline image. + The stream will be moved onto the EI. + """ + data_out = bytearray() + # Read data until delimiter ~> + while True: + data_buffered = read_non_whitespace(stream) + (read_bytes := stream.read(BUFFER_SIZE)) + if not data_buffered or not read_bytes: + raise PdfReadError("Unexpected end of stream.") + pos_tok = data_buffered.find(b"~>") + if pos_tok >= 0: # found! + data_out += data_buffered[: pos_tok + 2] + stream.seek(-len(data_buffered) + pos_tok + 2, 1) + break + if len(data_buffered) == 2: # end of buffer + data_out += data_buffered + raise PdfReadError("Unexpected end of stream.") + data_out += data_buffered[ + :-2 + ] # back by one char in case of in the middle of ~> + stream.seek(-2, 1) + + if not _check_end_image_marker(stream): + raise PdfReadError("EI stream not found.") + return bytes(data_out) + + +def extract_inline__run_length_decode(stream: StreamType) -> bytes: + """ + Extract RL (RunLengthDecode) stream from inline image. + The stream will be moved onto the EI. + """ + data_out = bytearray() + # Read data until delimiter 128 + while True: + data_buffered = stream.read(BUFFER_SIZE) + if not data_buffered: + raise PdfReadError("Unexpected end of stream.") + pos_tok = data_buffered.find(b"\x80") + if pos_tok >= 0: # found + # Ideally, we could just use plain run-length decoding here, where 80_16 = 128_10 + # marks the EOD. But there apparently are cases like in issue #3517, where we have + # an inline image with up to 51 EOD markers. In these cases, be resilient here and + # use the default `EI` marker detection instead. Please note that this fallback + # still omits special `EI` handling within the stream, but for now assume that having + # both of these cases occur at the same time is very unlikely (and the image stream + # is broken anyway). + # For now, do not skip over more than one whitespace character. + after_token = data_buffered[pos_tok + 1 : pos_tok + 4] + if after_token.startswith(b"EI") or after_token.endswith(b"EI"): + data_out += data_buffered[: pos_tok + 1] + stream.seek(-len(data_buffered) + pos_tok + 1, 1) + else: + logger_warning("Early EOD in RunLengthDecode of inline image, using fallback.", source=__name__) + ei_marker = data_buffered.find(b"EI") + if ei_marker > 0: + data_out += data_buffered[: ei_marker] + stream.seek(-len(data_buffered) + ei_marker - 1, 1) + break + data_out += data_buffered + + if not _check_end_image_marker(stream): + raise PdfReadError("EI stream not found.") + return bytes(data_out) + + +def extract_inline__dct_decode(stream: BinaryStreamType) -> bytes: + """ + Extract DCT (JPEG) stream from inline image. + The stream will be moved onto the EI. + """ + def read(length: int) -> bytes: + # If 0 bytes are returned, and *size* was not 0, this indicates end of file. + # If the object is in non-blocking mode and no bytes are available, `None` is returned. + _result = stream.read(length) + if _result is None or len(_result) != length: + raise PdfReadError("Unexpected end of stream.") + return _result + + data_out = bytearray() + # Read Blocks of data (ID/Size/data) up to ID=FF/D9 + # https://www.digicamsoft.com/itu/itu-t81-36.html + not_first = False + while True: + c = read(1) + if not_first or (c == b"\xff"): + data_out += c + if c != b"\xff": + continue + not_first = True + c = read(1) + data_out += c + if c == b"\xff": + stream.seek(-1, 1) # pragma: no cover + elif c == b"\x00": # stuffing + pass + elif c == b"\xd9": # end + break + elif c in ( + b"\xc0\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc9\xca\xcb\xcc\xcd\xce\xcf" + b"\xda\xdb\xdc\xdd\xde\xdf" + b"\xe0\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xfe" + ): + c = read(2) + data_out += c + sz = c[0] * 256 + c[1] + data_out += read(sz - 2) + + if not _check_end_image_marker(stream): + raise PdfReadError("EI stream not found.") + return bytes(data_out) + + +def _is_preceded_by_whitespace(stream_out: BytesIO, position: int) -> bool: + """ + Check whether the byte in front of `position` is a whitespace. + + Returns: + Whether the preceding byte is whitespace. If no preceding byte exists, + returns ``False`` because the ``EI`` marker is at the beginning of the + image data and is therefore not delimited by anything. + """ + if position <= 0: + return False + saved_position = stream_out.tell() + # Avoid exporting the BytesIO buffer: PyPy may keep the view alive, + # preventing later writes from resizing the output stream. + stream_out.seek(position - 1) + is_whitespace = stream_out.read(1) in WHITESPACES + stream_out.seek(saved_position) + return is_whitespace + + +def _is_followed_by_closing_operator(stream: StreamType, first_byte: bytes) -> bool: + """ + Check whether `first_byte` starts an exact `Q` or `EMC` operator. + + `first_byte` has already been consumed from the stream, which is restored to its + current position afterwards. Looking at this byte alone is not sufficient, as + binary image data can easily be mistaken for one of these operators. + """ + position = stream.tell() + if first_byte == b"Q": + suffix = stream.read(1) + is_closing_operator = suffix == b"" or suffix in WHITESPACES + elif first_byte == b"E": + suffix = stream.read(3) + is_closing_operator = suffix[:2] == b"MC" and (suffix[2:3] == b"" or suffix[2:3] in WHITESPACES) + else: + is_closing_operator = False + stream.seek(position, 0) + return is_closing_operator + + +def extract_inline_default(stream: StreamType) -> bytes: + """Legacy method, used by default""" + stream_out = BytesIO() + # Read the inline image, while checking for EI (End Image) operator. + while True: + data_buffered = stream.read(BUFFER_SIZE) + if not data_buffered: + raise PdfReadError("Unexpected end of stream.") + pos_ei = data_buffered.find( + b"E" + ) # We can not look straight for "EI" because it may not have been loaded in the buffer + + if pos_ei == -1: + stream_out.write(data_buffered) + else: + # Write out everything including E (the one from EI to be removed) + stream_out.write(data_buffered[0 : pos_ei + 1]) + sav_pos_ei = stream_out.tell() - 1 + # Seek back in the stream to read the E next + stream.seek(pos_ei + 1 - len(data_buffered), 1) + saved_pos = stream.tell() + # Check for End Image + tok2 = stream.read(1) # I of "EI" + if tok2 != b"I": + stream.seek(saved_pos, 0) + continue + tok3 = stream.read(1) # possible space after "EI" + if tok3 == b"": + # The `EI` marker is at the very end of the stream. There can be + # no trailing binary data, so this is unambiguously the end of the + # inline image (#3468). + stream.seek(saved_pos - 1, 0) + stream_out.truncate(sav_pos_ei) + break + if tok3 not in WHITESPACES: + stream.seek(saved_pos, 0) + continue + while tok3 in WHITESPACES: + tok3 = stream.read(1) + # The `EI` marker should be delimited by whitespace on both sides. Preserve the + # legacy tolerance for a missing leading delimiter only when it is followed by an + # exact closing operator, as binary image data would match otherwise. + if not _is_preceded_by_whitespace(stream_out, sav_pos_ei) and not _is_followed_by_closing_operator( + stream, tok3 + ): + stream.seek(saved_pos, 0) + continue + if is_followed_by_binary_data(stream): + # Inline image contains `EI ` sequence usually marking the end of it, but + # is followed by binary data which does not make sense for the actual end. + stream.seek(saved_pos, 0) + continue + # Data contains [\s]EI[\s](Q|EMC): 4 chars are sufficient + # remove E(I) wrongly inserted earlier + stream.seek(saved_pos - 1, 0) + stream_out.truncate(sav_pos_ei) + break + + return stream_out.getvalue() + + +def is_followed_by_binary_data(stream: IO[bytes], length: int = 10) -> bool: + """ + Check if the next bytes of the stream look like binary image data or regular page content. + + This is just some heuristics due to the PDF specification being too imprecise about + inline images containing the `EI` marker which would end an image. Starting with PDF 2.0, + we finally get a mandatory length field, but with (proper) PDF 2.0 support being very limited + everywhere, we should not expect to be able to remove such hacks in the near future - especially + considering legacy documents as well. + + The actual implementation draws some inspiration from + https://github.com/itext/itext-java/blob/9.1.0/kernel/src/main/java/com/itextpdf/kernel/pdf/canvas/parser/util/InlineImageParsingUtils.java + """ + position = stream.tell() + data = stream.read(length) + stream.seek(position) + if not data: + return False + operator_start = None + operator_end = None + + for index, byte in enumerate(data): + if byte < 32 and byte not in WHITESPACES_AS_BYTES: + # This covers all characters not being displayable directly, although omitting whitespace + # to allow for operator detection. + return True + is_whitespace = byte in WHITESPACES_AS_BYTES + if operator_start is None and not is_whitespace: + # Interpret all other non-whitespace characters as the start of an operation. + operator_start = index + if operator_start is not None and is_whitespace: + # A whitespace stops an operation. + # Assume that having an inline image with tons of whitespace is rather unlikely. + operator_end = index + break + + if operator_start is None: + # Inline images should not have tons of whitespaces, which would lead to no operator start. + return False + if operator_end is None: + # We probably are inside an operation. + operator_end = length + operator_length = operator_end - operator_start + operator = data[operator_start:operator_end] + if operator.startswith(b"/") and operator_length > 1: + # Name object. + return False + if operator.replace(b".", b"").isdigit(): + # Graphics operator, for example a move. A number (integer or float). + return False + if operator_length > 3: # noqa: SIM103 + # Usually, the operators inside a content stream should not have more than three characters, + # especially after an inline image. + return True + return False diff --git a/app/vendor/pypdf/generic/_image_xobject.py b/app/vendor/pypdf/generic/_image_xobject.py new file mode 100644 index 0000000..f356230 --- /dev/null +++ b/app/vendor/pypdf/generic/_image_xobject.py @@ -0,0 +1,730 @@ +"""Functions to convert an image XObject to an image""" + +import sys +from io import BytesIO +from typing import Any, Literal, Optional, Union, cast + +from .._configuration import get_configuration +from .._utils import check_if_whitespace_only, logger_warning +from ..constants import ColorSpaces, ImageAttributes, StreamAttributes +from ..constants import FilterTypes as FT +from ..errors import EmptyImageDataError, LimitReachedError, PdfReadError +from ..generic import ( + ArrayObject, + DecodedStreamObject, + EncodedStreamObject, + NullObject, + TextStringObject, + is_null_or_none, +) + +if sys.version_info[:2] >= (3, 10): + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + + +try: + from PIL import Image, UnidentifiedImageError +except ImportError: + raise ImportError( + "pillow is required to do image extraction. " + "It can be installed via 'pip install pypdf[image]'" + ) + +mode_str_type: TypeAlias = Literal[ + "", "1", "RGB", "2bits", "4bits", "P", "L", "RGBA", "CMYK" +] + +MAX_IMAGE_MODE_NESTING_DEPTH: int = 10 + + +def _get_image_mode( + color_space: Union[str, list[Any], Any], + color_components: int, + prev_mode: mode_str_type, + depth: int = 0, +) -> tuple[mode_str_type, bool]: + """ + Returns: + Image mode, not taking into account mask (transparency). + ColorInversion is required (like for some DeviceCMYK). + + """ + if depth > MAX_IMAGE_MODE_NESTING_DEPTH: + raise PdfReadError( + "Color spaces nested too deeply. If required, consider increasing MAX_IMAGE_MODE_NESTING_DEPTH." + ) + if is_null_or_none(color_space): + return "", False + color_space_str: str = "" + if isinstance(color_space, str): + color_space_str = color_space + elif not isinstance(color_space, list): + raise PdfReadError( + "Cannot interpret color space", color_space + ) # pragma: no cover + elif not color_space: + return "", False + elif color_space[0].startswith("/Cal"): # /CalRGB or /CalGray + color_space_str = "/Device" + color_space[0][4:] + elif color_space[0] == "/ICCBased": + icc_profile = color_space[1].get_object() + color_components = cast(int, icc_profile["/N"]) + color_space_str = icc_profile.get("/Alternate", "") + elif color_space[0] == "/Indexed": + color_space_str = color_space[1].get_object() + mode, invert_color = _get_image_mode( + color_space_str, color_components, prev_mode, depth + 1 + ) + if mode in ("RGB", "CMYK"): + mode = "P" + return mode, invert_color + elif color_space[0] == "/Separation": + color_space_str = color_space[2].get_object() + mode, invert_color = _get_image_mode( + color_space_str, color_components, prev_mode, depth + 1 + ) + return mode, True + elif color_space[0] == "/DeviceN": + original_color_space = color_space + color_components = len(color_space[1]) + color_space_str = color_space[2].get_object() + if color_space_str == "/DeviceCMYK" and color_components == 1: + if original_color_space[1][0] != "/Black": + logger_warning( + "Color %(color)s converted to Gray. Please share PDF with pypdf dev team", + source=__name__, + color=original_color_space[1][0], + ) + return "L", True + mode, invert_color = _get_image_mode( + color_space_str, color_components, prev_mode, depth + 1 + ) + return mode, invert_color + + mode_map: dict[str, mode_str_type] = { + "1bit": "1", # must be zeroth position: color_components may index the values + "/DeviceGray": "L", # must be first position: color_components may index the values + "palette": "P", # must be second position: color_components may index the values + "/DeviceRGB": "RGB", # must be third position: color_components may index the values + "/DeviceCMYK": "CMYK", # must be fourth position: color_components may index the values + "2bit": "2bits", + "4bit": "4bits", + } + + mode_values = list(mode_map.values()) + mode = ( + mode_map.get(color_space_str) + or ( + mode_values[color_components] + if 0 <= color_components < len(mode_values) + else None + ) + or prev_mode + ) + + return mode, mode == "CMYK" + + +def bits2byte( + data: bytes, + size: tuple[int, int], + bits: int, + colors: int = 1, + scale: bool = False, +) -> bytes: + configuration = get_configuration() + + # Number of samples per row = pixels per row * components per pixel. + samples_per_row = size[0] * colors + buffer_size = samples_per_row * size[1] + if buffer_size > configuration.image_maximum_buffer_size: + raise LimitReachedError( + f"Requested buffer size {buffer_size} exceeds limit of {configuration.image_maximum_buffer_size}." + ) + + byte_buffer = bytearray(buffer_size) + mask = (1 << bits) - 1 + # Scale a b-bit sample up to the full 0-255 range (e.g. 4-bit 15 -> 255) + # when the output is consumed directly as color (RGB) rather than as a + # palette index. + factor = 255 // mask if scale and mask else 1 + + required = size[1] * ((samples_per_row * bits + 7) // 8) + if (length := len(data)) < required: + logger_warning("Image data is not rectangular. Adding padding.", source=__name__) + data += b"\x00" * (required - length) + + data_index = 0 + bit = 8 - bits + for y in range(size[1]): + if bit != 8 - bits: + data_index += 1 + bit = 8 - bits + for x in range(samples_per_row): + byte_buffer[x + y * samples_per_row] = ( + ((data[data_index] >> bit) & mask) * factor + ) + bit -= bits + if bit < 0: + data_index += 1 + bit = 8 - bits + return bytes(byte_buffer) + + +def _expand_low_bit_samples( + mode: mode_str_type, + size: tuple[int, int], + data: bytes, + color_space: Union[str, ArrayObject], +) -> tuple[mode_str_type, bytes]: + """ + Expand 2- or 4-bit-per-component samples to one byte per component. + + Pillow has no mode for sub-byte samples, so the packed data has to be + unpacked before an image can be built from it. For a single-component + color space the samples are palette/grayscale indices ("P"). For a + multi-component space such as /DeviceRGB they are interleaved color + components, so they are also scaled to the full 0-255 range and become + an "RGB" image. + + Returns the resulting mode and data, unchanged when the mode is not a + low-bit one. + """ + if mode not in ("2bits", "4bits"): + return mode, data + bits = int(mode[0]) + if color_space == "/DeviceRGB": + return "RGB", bits2byte(data, size, bits, colors=3, scale=True) + return "P", bits2byte(data, size, bits) + + +def _image_from_bytes( + mode: str, size: tuple[int, int], data: bytes +) -> Image.Image: + pixel_count = size[0] * size[1] + bytes_per_pixel = len(mode) + required_byte_count = pixel_count * bytes_per_pixel + + configuration = get_configuration() + if required_byte_count > configuration.image_maximum_buffer_size: + raise LimitReachedError( + f"Requested image buffer size {required_byte_count} exceeds limit " + f"{configuration.image_maximum_buffer_size}." + ) + + try: + img = Image.frombytes(mode, size, data) + except ValueError as exc: + data_length = len(data) + if data_length == 0: + raise EmptyImageDataError( + "Data is 0 bytes, cannot process an image from empty data." + ) from exc + if data_length % pixel_count != 0: + raise + k = required_byte_count / data_length + data = b"".join(bytes((x,) * int(k)) for x in data) + img = Image.frombytes(mode, size, data) + return img + + +def __handle_flate__indexed(color_space: ArrayObject) -> tuple[Any, Any, Any, Any]: + count = len(color_space) + if count == 4: + color_space, base, hival, lookup = (value.get_object() for value in color_space) + return color_space, base, hival, lookup + + # Deal with strange AutoDesk files where `base` and `hival` look like this: + # /DeviceRGB\x00255 + element1 = color_space[1] + element1 = element1 if isinstance(element1, str) else element1.get_object() + if count == 3 and "\x00" in element1: + color_space, lookup = color_space[0].get_object(), color_space[2].get_object() + base, hival = element1.split("\x00") + hival = int(hival) + return color_space, base, hival, lookup + raise PdfReadError(f"Expected color space with 4 values, got {count}: {color_space}") + + +def _handle_flate( + size: tuple[int, int], + data: bytes, + mode: mode_str_type, + color_space: Union[str, ArrayObject], + colors: int, + obj_as_text: str, +) -> tuple[Image.Image, str, str, bool]: + """ + Process image encoded using the zlib/deflate compression method, corresponds to the FlateDecode filter + Returns img, image_format, extension, color inversion + """ + base: Any + hival: Any + lookup: Any + if isinstance(color_space, ArrayObject) and color_space[0] == "/Indexed": + color_space, base, hival, lookup = __handle_flate__indexed(color_space) + mode, data = _expand_low_bit_samples(mode, size, data, color_space) + img = _image_from_bytes(mode, size, data) + if color_space == "/Indexed": + if isinstance(lookup, (EncodedStreamObject, DecodedStreamObject)): + lookup = lookup.get_data() + if isinstance(lookup, TextStringObject): + lookup = lookup.original_bytes + if isinstance(lookup, str): + lookup = lookup.encode() + try: + nb, conv, mode = { # type: ignore[assignment] + "1": (0, "", ""), + "L": (1, "P", "L"), + "P": (0, "", ""), + "RGB": (3, "P", "RGB"), + "CMYK": (4, "P", "CMYK"), + }[_get_image_mode(base, 0, "")[0]] + except KeyError: # pragma: no cover + logger_warning( + "Base %(base)s not coded. Please share PDF with pypdf dev team", + source=__name__, + base=base, + ) + lookup = None + else: + if img.mode == "1" and nb == 0: + # A two-color lookup needs at least one byte per color, but the + # base color space resolves to a 1-bit or palette image. + logger_warning( + "Cannot apply lookup for base %(base)s to image with mode 1. " + "Please share PDF with pypdf dev team", + source=__name__, + base=base, + ) + elif img.mode == "1": + # Two values ("high" and "low"). + expected_count = 2 * nb + actual_count = len(lookup) + if actual_count != expected_count: + if actual_count < expected_count: + logger_warning( + "Not enough lookup values: Expected %(expected_count)d, got %(actual_count)d.", + source=__name__, + expected_count=expected_count, + actual_count=actual_count, + ) + lookup += bytes([0] * (expected_count - actual_count)) + elif not check_if_whitespace_only(lookup[expected_count:]): + logger_warning( + "Too many lookup values: Expected %(expected_count)d, got %(actual_count)d.", + source=__name__, + expected_count=expected_count, + actual_count=actual_count, + ) + lookup = lookup[:expected_count] + colors_arr = [lookup[:nb], lookup[nb:]] + source = img.convert("L") + bands = [ + source.point( + [low_value] * 128 + [high_value] * 128 + ) + for low_value, high_value in zip(colors_arr[0], colors_arr[1]) + ] + img = bands[0] if nb == 1 else Image.merge(mode, bands) + else: + img = img.convert(conv) + if len(lookup) != (hival + 1) * nb: + logger_warning("Invalid Lookup Table in %(obj_as_text)s", source=__name__, obj_as_text=obj_as_text) + lookup = None + elif mode == "L": + # gray lookup does not work: it is converted to a similar RGB lookup + lookup = b"".join([bytes([b, b, b]) for b in lookup]) + mode = "RGB" + # TODO: https://github.com/py-pdf/pypdf/pull/2039 + # this is a work around until PIL is able to process CMYK images + elif mode == "CMYK": + _rgb = [] + for _c, _m, _y, _k in ( + lookup[n : n + 4] for n in range(0, 4 * (len(lookup) // 4), 4) + ): + _r = int(255 * (1 - _c / 255) * (1 - _k / 255)) + _g = int(255 * (1 - _m / 255) * (1 - _k / 255)) + _b = int(255 * (1 - _y / 255) * (1 - _k / 255)) + _rgb.append(bytes((_r, _g, _b))) + lookup = b"".join(_rgb) + mode = "RGB" + if lookup is not None: + img.putpalette(lookup, rawmode=mode) + img = img.convert("L" if base == ColorSpaces.DEVICE_GRAY else "RGB") + elif not is_null_or_none(color_space) and color_space[0] == "/ICCBased": + # Exclude pure black-and-white images. + # TODO: The remaining code still does not look correct. Shouldn't the proper way be + # to use the original image and apply the ICC transformation on it? + # For now, this just loads the original image with a different color space. + if mode != "1": + # Table 65 - Additional Entries Specific to an ICC Profile Stream Dictionary + mode2 = _get_image_mode(color_space, colors, mode)[0] + if mode != mode2: + img = Image.frombytes(mode, size, data) # reloaded as mode may have changed + if mode == "CMYK": + image_format = "TIFF" + extension = ".tif" + else: + image_format = "PNG" + extension = ".png" # mime_type: "image/png" + return img, image_format, extension, False + + +def _handle_jpx( + size: tuple[int, int], + data: bytes, + mode: mode_str_type, + color_space: str, + colors: int, +) -> tuple[Image.Image, str, str, bool]: + """ + Process image encoded as JPX/JPEG2000 + Returns img, image_format, extension, inversion + """ + img1: Image.Image = Image.open(BytesIO(data), formats=("JPEG2000",)) + mode, invert_color = _get_image_mode(color_space, colors, mode) + if mode == "": + mode = cast(mode_str_type, img1.mode) + invert_color = mode == "CMYK" + if img1.mode == "RGBA" and mode == "RGB": + mode = "RGBA" + # we need to convert to the good mode + if img1.mode == mode or {img1.mode, mode} == {"L", "P"}: # compare (unordered) sets + # L and P are indexed modes which should not be changed. + img = img1 + elif {img1.mode, mode} == {"RGBA", "CMYK"}: + # RGBA / CMYK are 4bytes encoding where + # the encoding should be corrected + img = Image.frombytes(mode, img1.size, img1.tobytes()) + else: # pragma: no cover + img = img1.convert(mode) + # CMYK conversion + # https://stackverflow.com/questions/38855022/ + if img.mode == "CMYK" and color_space == "/ICCBased": + img = img.convert("RGB") + return img, "JPEG2000", ".jp2", invert_color + + +def _apply_decode( + img: Image.Image, + x_object: dict[str, Any], + lfilters: FT, + color_space: Union[str, list[Any], Any], + invert_color: bool, +) -> Image.Image: + # CMYK image and other color spaces without decode + # requires reverting scale (cf p243,2§ last sentence) + if ImageAttributes.DECODE in x_object: + decode = x_object[ImageAttributes.DECODE] + if img.mode == "CMYK" and lfilters == FT.DCT_DECODE: + # Adobe writes CMYK JPEGs with inverted component values, which is + # what the [min max] -> [1 0] remap in the branches below + # compensates for. An explicit /Decode array is expressed in terms + # of the true values, so it has to be combined with that inversion + # rather than replacing it - otherwise an identity /Decode leaves + # the image inverted (#2931). + # + # Substituting s -> 1-s into dmin + s*(dmax-dmin) gives + # dmax + s*(dmin-dmax), which is the same pair swapped. A malformed + # odd-length array is left untouched here and dropped with a + # warning further down. + decode = [ + value + for index in range(0, len(decode) - 1, 2) + for value in (decode[index + 1], decode[index]) + ] + elif img.mode == "CMYK" and lfilters == FT.JPX_DECODE: + decode = [1.0, 0.0] if not invert_color else [0.0, 1.0] + decode = decode * len(img.getbands()) + elif (img.mode == "CMYK" and lfilters == FT.DCT_DECODE) or (invert_color and img.mode == "L"): + decode = [1.0, 0.0] * len(img.getbands()) + else: + decode = None + + if ( + isinstance(color_space, ArrayObject) + and color_space[0].get_object() == "/Indexed" + ): + decode = None # decode is meaningless if Indexed + if ( + isinstance(color_space, ArrayObject) + and color_space[0].get_object() == "/Separation" + ): + decode = [1.0, 0.0] * len(img.getbands()) + if decode is not None and ( + not isinstance(decode, list) or len(decode) % 2 != 0 + ): + # /Decode is read straight from the image dictionary and must hold an + # even number of values (a [min max] pair per component). A malformed + # array would otherwise read past its end at decode[i + 1]; drop it and + # render the image without the remap. + logger_warning( + "Ignoring malformed /Decode array %(decode)s; expected an even number of values.", + source=__name__, + decode=decode, + ) + decode = None + if decode is not None and not all(decode[i] == i % 2 for i in range(len(decode))): + lut: list[int] = [] + for i in range(0, len(decode), 2): + dmin = decode[i] + dmax = decode[i + 1] + lut.extend( + round(255.0 * (j / 255.0 * (dmax - dmin) + dmin)) for j in range(256) + ) + img = img.point(lut) + return img + + +def _get_mode_and_invert_color( + x_object: dict[str, Any], colors: int, color_space: Union[str, list[Any], Any] +) -> tuple[mode_str_type, bool]: + if ( + ImageAttributes.COLOR_SPACE in x_object + and x_object[ImageAttributes.COLOR_SPACE] == ColorSpaces.DEVICE_RGB + ): + # https://pillow.readthedocs.io/en/stable/handbook/concepts.html#modes + mode: mode_str_type = "RGB" + if x_object.get("/BitsPerComponent", 8) < 8: + mode, invert_color = _get_image_mode( + f"{x_object.get('/BitsPerComponent', 8)}bit", 0, "" + ) + else: + mode, invert_color = _get_image_mode( + color_space, + 2 + if ( + colors == 1 + and ( + not is_null_or_none(color_space) + and "Gray" not in color_space + ) + ) + else colors, + "", + ) + return mode, invert_color + + +def _apply_alpha( + *, + img: Image.Image, + x_object: dict[str, Any], + obj_as_text: str, + image_format: str, + extension: str, + visited: set[int], +) -> tuple[Image.Image, str, str]: + if ImageAttributes.S_MASK not in x_object: + return img, extension, image_format + + s_mask = x_object[ImageAttributes.S_MASK] + if id(s_mask) in visited: + # A soft mask that refers back to an image already being + # converted would recurse until the interpreter runs out of + # stack. Such a chain cannot describe a real alpha channel, so + # drop the mask and keep the image we have. + logger_warning( + "Ignoring cyclic /SMask reference in %(obj_as_text)s", + source=__name__, + obj_as_text=obj_as_text, + ) + return img, extension, image_format + + alpha = _xobj_to_image(s_mask, visited=visited)[2] + if img.size != alpha.size: + logger_warning( + "Image and mask size not matching: %(image_size)s vs. %(alpha_size)s %(obj_as_text)s", + source=__name__, + image_size=img.size, + alpha_size=alpha.size, + obj_as_text=obj_as_text, + ) + else: + # TODO: implement mask + if alpha.mode != "L": + alpha = alpha.convert("L") + if img.mode == "P": + img = img.convert("RGB") + elif img.mode == "1": + img = img.convert("L") + img.putalpha(alpha) + + if "JPEG" in image_format: + image_format = "JPEG2000" + extension = ".jp2" + else: + image_format = "PNG" + extension = ".png" + + return img, extension, image_format + + +def _xobj_to_image( + x_object: dict[str, Any], + pillow_parameters: Union[dict[str, Any], None] = None, + visited: Optional[set[int]] = None, +) -> tuple[Optional[str], bytes, Any]: + """ + Users need to have the pillow package installed. + + It's unclear if pypdf will keep this function here, hence it's private. + It might get removed at any point. + + Args: + x_object: + pillow_parameters: parameters provided to Pillow Image.save() method, + cf. + visited: Set of id() values of XObjects already being converted higher + up the /SMask chain, used to detect cyclic soft masks. + + Returns: + Tuple[file extension, bytes, PIL.Image.Image] + + """ + if visited is None: + visited = set() + visited.add(id(x_object)) + + # For error reporting + obj_as_text = ( + x_object.indirect_reference.__repr__() + if x_object is None # pragma: no cover + else x_object.__repr__() + ) + + # Get size and data + size = (cast(int, x_object[ImageAttributes.WIDTH]), cast(int, x_object[ImageAttributes.HEIGHT])) + data = x_object.get_data() # type: ignore[attr-defined] + if isinstance(data, str): # pragma: no cover + data = data.encode() + if len(data) % (size[0] * size[1]) == 1 and data[-1] == 0x0A: # i.e. '\n' + data = data[:-1] + + # Get color properties + colors = x_object.get("/Colors", 1) + color_space: Any = x_object.get("/ColorSpace", NullObject()).get_object() + if isinstance(color_space, list) and len(color_space) == 1: + color_space = color_space[0].get_object() + + mode, invert_color = _get_mode_and_invert_color(x_object, colors, color_space) + + # Get filters + filters = x_object.get(StreamAttributes.FILTER, NullObject()).get_object() + # An empty array is a valid way of saying that no filter is applied: treat it + # like a missing /Filter entry rather than raising IndexError on the lookup. + last_filter = filters[-1] if filters and isinstance(filters, list) else filters + decode_parms = x_object.get(StreamAttributes.DECODE_PARMS) + if decode_parms and isinstance(decode_parms, (tuple, list)): + decode_parms = decode_parms[0] + else: + decode_parms = {} + if not isinstance(decode_parms, dict): + decode_parms = {} + + extension = None + if last_filter in (FT.FLATE_DECODE, FT.RUN_LENGTH_DECODE): + img, image_format, extension, _ = _handle_flate( + size, + data, + mode, + color_space, + colors, + obj_as_text, + ) + elif last_filter in (FT.LZW_DECODE, FT.ASCII_85_DECODE): + # I'm not sure if the following logic is correct. + # There might not be any relationship between the filter and the + # extension + if last_filter == FT.LZW_DECODE: + image_format = "TIFF" + extension = ".tiff" # mime_type = "image/tiff" + else: + image_format = "PNG" + extension = ".png" # mime_type = "image/png" + try: + img = Image.open(BytesIO(data), formats=("TIFF", "PNG")) + except UnidentifiedImageError: + fallback_mode, fallback_data = _expand_low_bit_samples( + mode, size, data, color_space + ) + img = _image_from_bytes(fallback_mode, size, fallback_data) + elif last_filter == FT.DCT_DECODE: + img, image_format, extension = Image.open(BytesIO(data)), "JPEG", ".jpg" + # invert_color kept unchanged + elif last_filter == FT.JPX_DECODE: + img, image_format, extension, invert_color = _handle_jpx( + size, data, mode, color_space, colors + ) + elif last_filter == FT.CCITT_FAX_DECODE: + img, image_format, extension, invert_color = ( + Image.open(BytesIO(data), formats=("TIFF",)), + "TIFF", + ".tiff", + False, + ) + elif last_filter == FT.JBIG2_DECODE: + img, image_format, extension, invert_color = ( + Image.open(BytesIO(data), formats=("PNG", "PPM")), + "PNG", + ".png", + False, + ) + elif mode == "CMYK": + img, image_format, extension, invert_color = ( + _image_from_bytes(mode, size, data), + "TIFF", + ".tif", + False, + ) + elif mode == "": + raise PdfReadError(f"ColorSpace field not found in {x_object}") + else: + # Images without a filter (for example inline images) reach Pillow + # directly, so low-bit samples have to be expanded here as well. + mode, data = _expand_low_bit_samples(mode, size, data, color_space) + img, image_format, extension, invert_color = ( + _image_from_bytes(mode, size, data), + "PNG", + ".png", + False, + ) + + img = _apply_decode(img, x_object, last_filter, color_space, invert_color) + img, extension, image_format = _apply_alpha( + img=img, x_object=x_object, obj_as_text=obj_as_text, image_format=image_format, extension=extension, + visited=visited, + ) + + if pillow_parameters is None: + pillow_parameters = {} + # Preserve JPEG image quality - see issue #3515. + if image_format == "JPEG": + # This prevents: Cannot use 'keep' when original image is not a JPEG: + # "JPEG" is the value of PIL.JpegImagePlugin.JpegImageFile.format + img.format = "JPEG" + if "quality" not in pillow_parameters: + pillow_parameters["quality"] = "keep" + + # Save image to bytes + img_byte_arr = BytesIO() + try: + img.save(img_byte_arr, format=image_format, **pillow_parameters) + except OSError: # pragma: no cover # covered with pillow 10.3 + # in case of we convert to RGBA and then to PNG + img1 = img.convert("RGBA") + image_format = "PNG" + extension = ".png" + img_byte_arr = BytesIO() + img1.save(img_byte_arr, format=image_format) + data = img_byte_arr.getvalue() + + try: # temporary try/except until other fixes of images + img = Image.open(BytesIO(data)) + except Exception as exception: + logger_warning("Failed loading image: %(exception)s", source=__name__, exception=exception) + img = None # type: ignore[assignment,unused-ignore] # TODO: Remove unused-ignore on Python 3.10 + return extension, data, img diff --git a/app/vendor/pypdf/generic/_link.py b/app/vendor/pypdf/generic/_link.py new file mode 100644 index 0000000..fb1c45a --- /dev/null +++ b/app/vendor/pypdf/generic/_link.py @@ -0,0 +1,150 @@ +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +# This module contains code used by _writer.py to track links in pages +# being added to the writer until the links can be resolved. + +from typing import TYPE_CHECKING, Optional, Union, cast + +from .._utils import logger_warning +from . import ArrayObject, DictionaryObject, IndirectObject, PdfObject, TextStringObject, is_null_or_none + +if TYPE_CHECKING: + from .._page import PageObject + from .._reader import PdfReader + from .._writer import PdfWriter + + +class NamedReferenceLink: + """Named reference link being preserved until we can resolve it correctly.""" + + def __init__(self, reference: TextStringObject, source_pdf: "PdfReader") -> None: + """reference: TextStringObject with named reference""" + self._reference = reference + self._source_pdf = source_pdf + + def find_referenced_page(self) -> Union[IndirectObject, None]: + destination = self._source_pdf.named_destinations.get(str(self._reference)) + return destination.page if destination else None + + def patch_reference(self, target_pdf: "PdfWriter", new_page: IndirectObject) -> None: + """target_pdf: PdfWriter which the new link went into""" + # point named destination in new PDF to the new page + if str(self._reference) not in target_pdf.named_destinations: + target_pdf.add_named_destination(str(self._reference), new_page.page_number) + + +class DirectReferenceLink: + """Direct reference link being preserved until we can resolve it correctly.""" + + def __init__(self, reference: ArrayObject) -> None: + """reference: an ArrayObject whose first element is the Page indirect object""" + self._reference = reference + + def find_referenced_page(self) -> Union[IndirectObject, None]: + if not self._reference: + return None + return cast(IndirectObject, self._reference[0]) + + def patch_reference(self, target_pdf: "PdfWriter", new_page: IndirectObject) -> None: + """target_pdf: PdfWriter which the new link went into""" + self._reference[0] = new_page + + +ReferenceLink = Union[NamedReferenceLink, DirectReferenceLink] + + +def extract_links(new_page: "PageObject", old_page: "PageObject") -> list[tuple[ReferenceLink, ReferenceLink]]: + """Extracts links from two pages on the assumption that the two pages are + the same. Produces one list of (new link, old link) tuples. + + Non-link annotations are ignored before pairing to avoid dropping valid + links when one page includes additional non-link annotation entries. + """ + new_annotations = new_page.get("/Annots", ArrayObject()).get_object() + old_annotations = old_page.get("/Annots", ArrayObject()).get_object() + if is_null_or_none(new_annotations): + new_annotations = ArrayObject() + if is_null_or_none(old_annotations): + old_annotations = ArrayObject() + if not isinstance(new_annotations, ArrayObject) or not isinstance(old_annotations, ArrayObject): + logger_warning( + "Expected annotation arrays: %(old_annotations)s %(new_annotations)s. Ignoring annotations.", + source=__name__, + old_annotations=old_annotations, + new_annotations=new_annotations, + ) + return [] + new_links = [ + link + for annotation in new_annotations + if (link := _build_link(annotation, new_page)) is not None + ] + old_links = [ + link + for annotation in old_annotations + if (link := _build_link(annotation, old_page)) is not None + ] + + if len(new_links) != len(old_links): + logger_warning( + "Annotation sizes differ: %(old_count)d vs. %(new_count)d", + source=__name__, + old_count=len(old_links), + new_count=len(new_links), + ) + + return list(zip(new_links, old_links)) + + +def _build_link(indirect_object: IndirectObject, page: "PageObject") -> Optional[ReferenceLink]: + src = cast("PdfReader", page.pdf) + link = cast(DictionaryObject, indirect_object.get_object()) + if (not isinstance(link, DictionaryObject)) or link.get("/Subtype") != "/Link": + return None + + if "/A" in link: + action = cast(DictionaryObject, link["/A"]) + if action.get("/S") != "/GoTo": + return None + + if "/D" not in action: + return None + return _create_link(action["/D"], src) + + if "/Dest" in link: + return _create_link(link["/Dest"], src) + + return None # Nothing to do here + + +def _create_link(reference: PdfObject, source_pdf: "PdfReader") -> Optional[ReferenceLink]: + if isinstance(reference, TextStringObject): + return NamedReferenceLink(reference, source_pdf) + if isinstance(reference, ArrayObject): + return DirectReferenceLink(reference) + return None diff --git a/app/vendor/pypdf/generic/_outline.py b/app/vendor/pypdf/generic/_outline.py new file mode 100644 index 0000000..c76898b --- /dev/null +++ b/app/vendor/pypdf/generic/_outline.py @@ -0,0 +1,33 @@ +from typing import Union + +from .._utils import StreamType, deprecation_no_replacement +from ._base import NameObject +from ._data_structures import Destination + + +class OutlineItem(Destination): + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + stream.write(b"<<\n") + for key in [ + NameObject(x) + for x in ["/Title", "/Parent", "/First", "/Last", "/Next", "/Prev"] + if x in self + ]: + key.write_to_stream(stream) + stream.write(b" ") + value = self.raw_get(key) + value.write_to_stream(stream) + stream.write(b"\n") + key = NameObject("/Dest") + key.write_to_stream(stream) + stream.write(b" ") + value = self.dest_array + value.write_to_stream(stream) + stream.write(b"\n") + stream.write(b">>") diff --git a/app/vendor/pypdf/generic/_rectangle.py b/app/vendor/pypdf/generic/_rectangle.py new file mode 100644 index 0000000..f74fb23 --- /dev/null +++ b/app/vendor/pypdf/generic/_rectangle.py @@ -0,0 +1,135 @@ +from collections.abc import Sequence +from typing import Any, Union + +from ._base import FloatObject, NumberObject +from ._data_structures import ArrayObject + + +class RectangleObject(ArrayObject): + """ + This class is used to represent *page boxes* in pypdf. + + These boxes include: + + * :attr:`artbox ` + * :attr:`bleedbox ` + * :attr:`cropbox ` + * :attr:`mediabox ` + * :attr:`trimbox ` + """ + + def __init__(self, arr: Sequence[Any]) -> None: + # must have four points + assert len(arr) == 4 + # automatically convert arr[x] into NumberObject(arr[x]) if necessary + ArrayObject.__init__(self, [self._ensure_is_number(x) for x in arr]) + + def _ensure_is_number(self, value: Any) -> Union[FloatObject, NumberObject]: + if not isinstance(value, (FloatObject, NumberObject)): + return FloatObject(value) + return value + + def scale(self, sx: float, sy: float) -> "RectangleObject": + return RectangleObject( + ( + float(self.left) * sx, + float(self.bottom) * sy, + float(self.right) * sx, + float(self.top) * sy, + ) + ) + + def __repr__(self) -> str: + return f"RectangleObject({list(self)!r})" + + @property + def left(self) -> FloatObject: + value: FloatObject = self[0] + return value + + @left.setter + def left(self, f: float) -> None: + self[0] = FloatObject(f) + + @property + def bottom(self) -> FloatObject: + value: FloatObject = self[1] + return value + + @bottom.setter + def bottom(self, f: float) -> None: + self[1] = FloatObject(f) + + @property + def right(self) -> FloatObject: + value: FloatObject = self[2] + return value + + @right.setter + def right(self, f: float) -> None: + self[2] = FloatObject(f) + + @property + def top(self) -> FloatObject: + value: FloatObject = self[3] + return value + + @top.setter + def top(self, f: float) -> None: + self[3] = FloatObject(f) + + @property + def lower_left(self) -> tuple[float, float]: + """ + Property to read and modify the lower left coordinate of this box + in (x,y) form. + """ + return self.left, self.bottom + + @lower_left.setter + def lower_left(self, value: tuple[float, float]) -> None: + self[0], self[1] = (self._ensure_is_number(x) for x in value) + + @property + def lower_right(self) -> tuple[float, float]: + """ + Property to read and modify the lower right coordinate of this box + in (x,y) form. + """ + return self.right, self.bottom + + @lower_right.setter + def lower_right(self, value: tuple[float, float]) -> None: + self[2], self[1] = (self._ensure_is_number(x) for x in value) + + @property + def upper_left(self) -> tuple[float, float]: + """ + Property to read and modify the upper left coordinate of this box + in (x,y) form. + """ + return self.left, self.top + + @upper_left.setter + def upper_left(self, value: tuple[float, float]) -> None: + self[0], self[3] = (self._ensure_is_number(x) for x in value) + + @property + def upper_right(self) -> tuple[float, float]: + """ + Property to read and modify the upper right coordinate of this box + in (x,y) form. + """ + return self.right, self.top + + @upper_right.setter + def upper_right(self, value: tuple[float, float]) -> None: + self[2], self[3] = (self._ensure_is_number(x) for x in value) + + @property + def width(self) -> float: + return self.right - self.left + + @property + def height(self) -> float: + return self.top - self.bottom diff --git a/app/vendor/pypdf/generic/_utils.py b/app/vendor/pypdf/generic/_utils.py new file mode 100644 index 0000000..1d053d8 --- /dev/null +++ b/app/vendor/pypdf/generic/_utils.py @@ -0,0 +1,225 @@ +import codecs +from typing import Union + +from .._codecs import _pdfdoc_encoding +from .._utils import StreamType, logger_warning, read_non_whitespace +from ..errors import STREAM_TRUNCATED_PREMATURELY, PdfStreamError +from ._base import ByteStringObject, TextStringObject + + +def hex_to_rgb(value: str) -> tuple[float, float, float]: + return tuple(int(value.lstrip("#")[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore[return-value] + + +def read_hex_string_from_stream( + stream: StreamType, + forced_encoding: Union[str, list[str], dict[int, str], None] = None, +) -> Union["TextStringObject", "ByteStringObject"]: + stream.read(1) + arr = bytearray() + x = b"" + while True: + tok = read_non_whitespace(stream) + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + if tok == b">": + break + if tok not in b"0123456789abcdefABCDEF": + raise PdfStreamError( + f"Invalid hexadecimal character {tok!r} in hex string" + ) + x += tok + if len(x) == 2: + arr.append(int(x, base=16)) + x = b"" + if len(x) == 1: + x += b"0" + if x != b"": + arr.append(int(x, base=16)) + return create_string_object(bytes(arr), forced_encoding) + + +__ESCAPE_DICT__ = { + b"n": ord(b"\n"), + b"r": ord(b"\r"), + b"t": ord(b"\t"), + b"b": ord(b"\b"), + b"f": ord(b"\f"), + b"(": ord(b"("), + b")": ord(b")"), + b"/": ord(b"/"), + b"\\": ord(b"\\"), + b" ": ord(b" "), + b"%": ord(b"%"), + b"<": ord(b"<"), + b">": ord(b">"), + b"[": ord(b"["), + b"]": ord(b"]"), + b"#": ord(b"#"), + b"_": ord(b"_"), + b"&": ord(b"&"), + b"$": ord(b"$"), +} +__BACKSLASH_CODE__ = 92 + + +def read_string_from_stream( + stream: StreamType, + forced_encoding: Union[str, list[str], dict[int, str], None] = None, +) -> Union["TextStringObject", "ByteStringObject"]: + tok = stream.read(1) + parens = 1 + txt = [] + while True: + tok = stream.read(1) + if not tok: + raise PdfStreamError(STREAM_TRUNCATED_PREMATURELY) + if tok == b"(": + parens += 1 + elif tok == b")": + parens -= 1 + if parens == 0: + break + elif tok == b"\\": + tok = stream.read(1) + try: + txt.append(__ESCAPE_DICT__[tok]) + continue + except KeyError: + if b"0" <= tok <= b"7": + # "The number ddd may consist of one, two, or three + # octal digits; high-order overflow shall be ignored. + # Three octal digits shall be used, with leading zeros + # as needed, if the next character of the string is also + # a digit." (PDF reference 7.3.4.2, p 16) + sav = stream.tell() - 1 + for _ in range(2): + ntok = stream.read(1) + if b"0" <= ntok <= b"7": + tok += ntok + else: + stream.seek(-1, 1) # ntok has to be analyzed + break + i = int(tok, base=8) + if i > 255: + txt.append(__BACKSLASH_CODE__) + stream.seek(sav) + else: + txt.append(i) + continue + if tok in b"\n\r": + # This case is hit when a backslash followed by a line + # break occurs. If it's a multi-char EOL, consume the + # second character: + tok = stream.read(1) + if tok not in b"\n\r": + stream.seek(-1, 1) + # Then don't add anything to the actual string, since this + # line break was escaped: + continue + logger_warning( + "Unexpected escaped string: %(token)s", + source=__name__, + token=tok.decode("utf-8", "ignore"), + ) + txt.append(__BACKSLASH_CODE__) + txt.append(ord(tok)) + return create_string_object(bytes(txt), forced_encoding) + + +def create_string_object( + string: Union[str, bytes], + forced_encoding: Union[str, list[str], dict[int, str], None] = None, +) -> Union[TextStringObject, ByteStringObject]: + """ + Create a ByteStringObject or a TextStringObject from a string to represent the string. + + Args: + string: The data being used + forced_encoding: Typically None, or an encoding string + + Returns: + A ByteStringObject + + Raises: + TypeError: If string is not of type str or bytes. + + """ + if isinstance(string, str): + return TextStringObject(string) + if isinstance(string, bytes): + if isinstance(forced_encoding, (list, dict)): + out = [] + + if isinstance(forced_encoding, list): + list_length = len(forced_encoding) + else: + list_length = None + + for x in string: + if ( + (list_length is not None and x < list_length) or + (list_length is None and x in forced_encoding) + ): + value = forced_encoding[x] + else: + value = bytes((x,)).decode("charmap") + out.append(value) + obj = TextStringObject("".join(out)) + obj._original_bytes = string + return obj + if isinstance(forced_encoding, str): + if forced_encoding == "bytes": + return ByteStringObject(string) + obj = TextStringObject(string.decode(forced_encoding)) + obj._original_bytes = string + return obj + try: + if string.startswith((codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE)): + retval = TextStringObject(string.decode("utf-16")) + retval._original_bytes = string + retval.autodetect_utf16 = True + retval.utf16_bom = string[:2] + return retval + if string.startswith(b"\x00"): + retval = TextStringObject(string.decode("utf-16be")) + retval._original_bytes = string + retval.autodetect_utf16 = True + retval.utf16_bom = codecs.BOM_UTF16_BE + return retval + if string[1:2] == b"\x00": + retval = TextStringObject(string.decode("utf-16le")) + retval._original_bytes = string + retval.autodetect_utf16 = True + retval.utf16_bom = codecs.BOM_UTF16_LE + return retval + + # This is probably a big performance hit here, but we need + # to convert string objects into the text/unicode-aware + # version if possible... and the only way to check if that's + # possible is to try. + # Some strings are strings, some are just byte arrays. + retval = TextStringObject(decode_pdfdocencoding(string)) + retval._original_bytes = string + retval.autodetect_pdfdocencoding = True + return retval + except UnicodeDecodeError: + return ByteStringObject(string) + else: + raise TypeError("create_string_object should have str or unicode arg") + + +def decode_pdfdocencoding(byte_array: bytes) -> str: + retval = "" + for b in byte_array: + c = _pdfdoc_encoding[b] + if c == "\u0000": + raise UnicodeDecodeError( + "pdfdocencoding", + bytearray(b), + -1, + -1, + "does not exist in translation table", + ) + retval += c + return retval diff --git a/app/vendor/pypdf/generic/_viewerpref.py b/app/vendor/pypdf/generic/_viewerpref.py new file mode 100644 index 0000000..78c579e --- /dev/null +++ b/app/vendor/pypdf/generic/_viewerpref.py @@ -0,0 +1,185 @@ +# Copyright (c) 2023, Pubpub-ZZ +# +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * The name of the author may not be used to endorse or promote products +# derived from this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +from typing import ( + Any, + Optional, + cast, +) + +from ..constants import PageAttributes +from ._base import BooleanObject, NameObject, NumberObject, is_null_or_none +from ._data_structures import ArrayObject, DictionaryObject + +f_obj = BooleanObject(False) + +#: The page boundaries a viewer preference may name, per Table 147 in the 2.0 +#: reference. +BOX_NAMES = [ + PageAttributes.MEDIABOX, + PageAttributes.CROPBOX, + PageAttributes.BLEEDBOX, + PageAttributes.TRIMBOX, + PageAttributes.ARTBOX, +] + + +class ViewerPreferences(DictionaryObject): + def __init__(self, obj: Optional[DictionaryObject] = None) -> None: + super().__init__(self) + if not is_null_or_none(obj): + self.update(obj.items()) # type: ignore[union-attr] + try: + self.indirect_reference = obj.indirect_reference # type: ignore[union-attr] + except AttributeError: + pass + + def _get_bool(self, key: str, default: Optional[BooleanObject]) -> Optional[BooleanObject]: + return self.get(key, default) + + def _set_bool(self, key: str, v: bool) -> None: + self[NameObject(key)] = BooleanObject(v is True) + + def _get_name(self, key: str, default: Optional[NameObject]) -> Optional[NameObject]: + return self.get(key, default) + + def _set_name(self, key: str, lst: list[str], v: NameObject) -> None: + if v[0] != "/": + raise ValueError(f"{v} does not start with '/'") + if lst != [] and v not in lst: + raise ValueError(f"{v} is an unacceptable value") + self[NameObject(key)] = NameObject(v) + + def _get_arr(self, key: str, default: Optional[list[Any]]) -> Optional[ArrayObject]: + return self.get(key, None if default is None else ArrayObject(default)) + + def _set_arr(self, key: str, v: Optional[ArrayObject]) -> None: + if v is None: + try: + del self[NameObject(key)] + except KeyError: + pass + return + if not isinstance(v, ArrayObject): + raise ValueError("ArrayObject is expected") + if key == "/PrintPageRange" and len(v) % 2: + # The array holds first/last page pairs, so an odd length leaves a + # range without its end. + raise ValueError( + f"/PrintPageRange holds page pairs, got {len(v)} entries: {v}" + ) + self[NameObject(key)] = v + + def _get_int(self, key: str, default: Optional[NumberObject]) -> Optional[NumberObject]: + return self.get(key, default) + + def _set_int(self, key: str, v: int) -> None: + if v < 0: + raise ValueError(f"{v} is an unacceptable value for {key}") + self[NameObject(key)] = NumberObject(v) + + @property + def PRINT_SCALING(self) -> NameObject: + return NameObject("/PrintScaling") + + def __new__(cls: Any, value: Any = None) -> "ViewerPreferences": # noqa: PYI034 + def _add_prop_bool(key: str, default: Optional[BooleanObject]) -> property: + return property( + lambda self: self._get_bool(key, default), + lambda self, v: self._set_bool(key, v), + None, + f""" + Returns/Modify the status of {key}, Returns {default} if not defined + """, + ) + + def _add_prop_name( + key: str, lst: list[str], default: Optional[NameObject] + ) -> property: + return property( + lambda self: self._get_name(key, default), + lambda self, v: self._set_name(key, lst, v), + None, + f""" + Returns/Modify the status of {key}, Returns {default} if not defined. + Acceptable values: {lst} + """, + ) + + def _add_prop_arr(key: str, default: Optional[ArrayObject]) -> property: + return property( + lambda self: self._get_arr(key, default), + lambda self, v: self._set_arr(key, v), + None, + f""" + Returns/Modify the status of {key}, Returns {default} if not defined + """, + ) + + def _add_prop_int(key: str, default: Optional[int]) -> property: + return property( + lambda self: self._get_int(key, default), + lambda self, v: self._set_int(key, v), + None, + f""" + Returns/Modify the status of {key}, Returns {default} if not defined + """, + ) + + cls.hide_toolbar = _add_prop_bool("/HideToolbar", f_obj) + cls.hide_menubar = _add_prop_bool("/HideMenubar", f_obj) + cls.hide_windowui = _add_prop_bool("/HideWindowUI", f_obj) + cls.fit_window = _add_prop_bool("/FitWindow", f_obj) + cls.center_window = _add_prop_bool("/CenterWindow", f_obj) + cls.display_doctitle = _add_prop_bool("/DisplayDocTitle", f_obj) + + cls.non_fullscreen_pagemode = _add_prop_name( + "/NonFullScreenPageMode", + ["/UseNone", "/UseOutlines", "/UseThumbs", "/UseOC"], + NameObject("/UseNone"), + ) + cls.direction = _add_prop_name( + "/Direction", ["/L2R", "/R2L"], NameObject("/L2R") + ) + cls.view_area = _add_prop_name("/ViewArea", BOX_NAMES, None) + cls.view_clip = _add_prop_name("/ViewClip", BOX_NAMES, None) + cls.print_area = _add_prop_name("/PrintArea", BOX_NAMES, None) + cls.print_clip = _add_prop_name("/PrintClip", BOX_NAMES, None) + cls.print_scaling = _add_prop_name( + "/PrintScaling", ["/None", "/AppDefault"], None + ) + cls.duplex = _add_prop_name( + "/Duplex", ["/Simplex", "/DuplexFlipShortEdge", "/DuplexFlipLongEdge"], None + ) + cls.pick_tray_by_pdfsize = _add_prop_bool("/PickTrayByPDFSize", None) + cls.print_pagerange = _add_prop_arr("/PrintPageRange", None) + cls.num_copies = _add_prop_int("/NumCopies", None) + + cls.enforce = _add_prop_arr("/Enforce", ArrayObject()) + + return cast("ViewerPreferences", DictionaryObject.__new__(cls)) diff --git a/app/vendor/pypdf/pagerange.py b/app/vendor/pypdf/pagerange.py new file mode 100644 index 0000000..bd5a7e8 --- /dev/null +++ b/app/vendor/pypdf/pagerange.py @@ -0,0 +1,220 @@ +""" +Representation and utils for ranges of PDF file pages. + +Copyright (c) 2014, Steve Witham . +All rights reserved. This software is available under a BSD license; +see https://github.com/py-pdf/pypdf/blob/main/LICENSE +""" + +import re +from typing import Any, Optional, Union + +from .errors import ParseError + +_INT_RE = r"(0|-?[1-9]\d*)" # A decimal int, don't allow "-0". +PAGE_RANGE_RE = f"^({_INT_RE}|({_INT_RE}?(:{_INT_RE}?(:{_INT_RE}?)?)))$" +# groups: 12 34 5 6 7 8 + + +class PageRange: + """ + A slice-like representation of a range of page indices. + + For example, page numbers, only starting at zero. + + The syntax is like what you would put between brackets [ ]. + The slice is one of the few Python types that can't be subclassed, + but this class converts to and from slices, and allows similar use. + + - PageRange(str) parses a string representing a page range. + - PageRange(slice) directly "imports" a slice. + - to_slice() gives the equivalent slice. + - str() and repr() allow printing. + - indices(n) is like slice.indices(n). + """ + + def __init__(self, arg: Union[slice, "PageRange", str]) -> None: + """ + Initialize with either a slice -- giving the equivalent page range, + or a PageRange object -- making a copy, + or a string like + "int", "[int]:[int]" or "[int]:[int]:[int]", + where the brackets indicate optional ints. + Remember, page indices start with zero. + Page range expression examples: + + : all pages. -1 last page. + 22 just the 23rd page. :-1 all but the last page. + 0:3 the first three pages. -2 second-to-last page. + :3 the first three pages. -2: last two pages. + 5: from the sixth page onward. -3:-1 third & second to last. + The third, "stride" or "step" number is also recognized. + ::2 0 2 4 ... to the end. 3:0:-1 3 2 1 but not 0. + 1:10:2 1 3 5 7 9 2::-1 2 1 0. + ::-1 all pages in reverse order. + Note the difference between this notation and arguments to slice(): + slice(3) means the first three pages; + PageRange("3") means the range of only the fourth page. + However PageRange(slice(3)) means the first three pages. + """ + if isinstance(arg, slice): + self._slice = arg + return + + if isinstance(arg, PageRange): + self._slice = arg.to_slice() + return + + m = isinstance(arg, str) and re.match(PAGE_RANGE_RE, arg) + if not m: + raise ParseError(arg) + if m.group(2): + # Special case: just an int means a range of one page. + start = int(m.group(2)) + stop = start + 1 if start != -1 else None + self._slice = slice(start, stop) + else: + bounds = [int(g) if g else None for g in m.group(4, 6, 8)] + step = bounds[2] + if step == 0: + # A zero stride cannot select anything; slice() accepts it here + # but raises as soon as the range is applied. + raise ParseError(arg) + self._slice = slice(*bounds) + + @staticmethod + def valid(input: Any) -> bool: + """ + True if input is a valid initializer for a PageRange. + + Args: + input: A possible PageRange string or a PageRange object. + + Returns: + True, if the ``input`` is a valid PageRange. + + """ + if isinstance(input, (slice, PageRange)): + return True + if not isinstance(input, str): + return False + try: + PageRange(input) + except ParseError: + return False + return True + + def to_slice(self) -> slice: + """Return the slice equivalent of this page range.""" + return self._slice + + def __str__(self) -> str: + """A string like "1:2:3".""" + s = self._slice + indices: Union[tuple[int, int], tuple[int, int, int]] + if s.step is None: + if s.start is not None and s.stop == s.start + 1: + return str(s.start) + + indices = s.start, s.stop + else: + indices = s.start, s.stop, s.step + return ":".join("" if i is None else str(i) for i in indices) + + def __repr__(self) -> str: + """A string like "PageRange('1:2:3')".""" + return "PageRange(" + repr(str(self)) + ")" + + def indices(self, n: int) -> tuple[int, int, int]: + """ + Assuming a sequence of length n, calculate the start and stop indices, + and the stride length of the PageRange. + + See help(slice.indices). + + Args: + n: the length of the list of pages to choose from. + + Returns: + Arguments for range(). + + """ + return self._slice.indices(n) + + def __eq__(self, other: object) -> bool: + if not isinstance(other, PageRange): + return False + return self._slice == other._slice + + def __hash__(self) -> int: + return hash((self.__class__, (self._slice.start, self._slice.stop, self._slice.step))) + + def __add__(self, other: "PageRange") -> "PageRange": + if not isinstance(other, PageRange): + raise TypeError(f"Can't add PageRange and {type(other)}") + if self._slice.step is not None or other._slice.step is not None: + raise ValueError("Can't add PageRange with stride") + a = self._slice.start, self._slice.stop + b = other._slice.start, other._slice.stop + + # None start means "beginning" (-inf for ordering); None stop means "end" (+inf). + def _start_key(v: Optional[int]) -> float: + return float("-inf") if v is None else v + + def _stop_key(v: Optional[int]) -> float: + return float("inf") if v is None else v + + if _start_key(a[0]) > _start_key(b[0]): + a, b = b, a + + # Now `a` has the smaller (or equal) start. + if _start_key(b[0]) > _stop_key(a[1]): + # There is a gap between a and b. + raise ValueError("Can't add PageRanges with gap") + stop = b[1] if _stop_key(b[1]) > _stop_key(a[1]) else a[1] + return PageRange(slice(a[0], stop)) + + +PAGE_RANGE_ALL = PageRange(":") # The range of all pages. + + +def parse_filename_page_ranges( + args: list[Union[str, PageRange, None]] +) -> list[tuple[str, PageRange]]: + """ + Given a list of filenames and page ranges, return a list of (filename, page_range) pairs. + + Args: + args: A list where the first element is a filename. The other elements are + filenames, page-range expressions, slice objects, or PageRange objects. + A filename not followed by a page range indicates all pages of the file. + + Returns: + A list of (filename, page_range) pairs. + + """ + pairs: list[tuple[str, PageRange]] = [] + pdf_filename: Union[str, None] = None + did_page_range = False + for arg in [*args, None]: + if PageRange.valid(arg): + if not pdf_filename: + raise ValueError( + "The first argument must be a filename, not a page range." + ) + + assert arg is not None + pairs.append((pdf_filename, PageRange(arg))) + did_page_range = True + else: + # New filename or end of list - use the complete previous file? + if pdf_filename and not did_page_range: + pairs.append((pdf_filename, PAGE_RANGE_ALL)) + + assert not isinstance(arg, PageRange), arg + pdf_filename = arg + did_page_range = False + return pairs + + +PageRangeSpec = Union[str, PageRange, tuple[int, int], tuple[int, int, int], list[int]] diff --git a/app/vendor/pypdf/papersizes.py b/app/vendor/pypdf/papersizes.py new file mode 100644 index 0000000..ed09f34 --- /dev/null +++ b/app/vendor/pypdf/papersizes.py @@ -0,0 +1,52 @@ +"""Helper to get paper sizes.""" + +from typing import NamedTuple + + +class Dimensions(NamedTuple): + width: int + height: int + + +class PaperSize: + """(width, height) of the paper in portrait mode in pixels at 72 ppi.""" + + # Notes of how to calculate it: + # 1. Get the size of the paper in millimeters + # 2. Convert it to inches (25.4 millimeters is equal to 1 inch) + # 3. Convert it to pixels at 72dpi (1 inch is equal to 72 pixels) + + # All Din-A paper sizes follow this pattern: + # 2 x A(n - 1) = A(n) + # So the height of the next bigger one is the width of the smaller one + # The ratio is always approximately 1:2**0.5 + # Additionally, A0 is defined to have an area of 1 m**2 + # https://en.wikipedia.org/wiki/ISO_216 + # Be aware of rounding issues! + A0 = Dimensions(2384, 3370) # 841mm x 1189mm + A1 = Dimensions(1684, 2384) + A2 = Dimensions(1191, 1684) + A3 = Dimensions(842, 1191) + A4 = Dimensions( + 595, 842 + ) # Printer paper, documents - this is by far the most common + A5 = Dimensions(420, 595) # Paperback books + A6 = Dimensions(298, 420) # Postcards + A7 = Dimensions(210, 298) + A8 = Dimensions(147, 210) + + # Envelopes + C4 = Dimensions(649, 918) + + +_din_a = ( + PaperSize.A0, + PaperSize.A1, + PaperSize.A2, + PaperSize.A3, + PaperSize.A4, + PaperSize.A5, + PaperSize.A6, + PaperSize.A7, + PaperSize.A8, +) diff --git a/app/vendor/pypdf/py.typed b/app/vendor/pypdf/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/app/vendor/pypdf/types.py b/app/vendor/pypdf/types.py new file mode 100644 index 0000000..a1c4e49 --- /dev/null +++ b/app/vendor/pypdf/types.py @@ -0,0 +1,80 @@ +"""Helpers for working with PDF types.""" + +import sys +from typing import Literal, Union + +if sys.version_info[:2] >= (3, 10): + # Python 3.10+: https://www.python.org/dev/peps/pep-0484 + from typing import TypeAlias +else: + from typing_extensions import TypeAlias + +from .generic._base import NameObject, NullObject, NumberObject +from .generic._data_structures import ArrayObject, Destination +from .generic._outline import OutlineItem + +BorderArrayType: TypeAlias = list[Union[NameObject, NumberObject, ArrayObject]] + +OutlineItemType: TypeAlias = Union[OutlineItem, Destination] + +FitType: TypeAlias = Literal[ + "/XYZ", "/Fit", "/FitH", "/FitV", "/FitR", "/FitB", "/FitBH", "/FitBV" +] +# These go with the FitType, they specify values for the fit +ZoomArgType: TypeAlias = Union[NumberObject, NullObject, float] +ZoomArgsType: TypeAlias = list[ZoomArgType] + +# Recursive types like the following are not yet supported by Sphinx: +# OutlineType = List[Union[Destination, "OutlineType"]] +# Hence use this for the moment: +OutlineType = list[Union[Destination, list[Union[Destination, list[Destination]]]]] + +LayoutType: TypeAlias = Literal[ + "/NoLayout", + "/SinglePage", + "/OneColumn", + "/TwoColumnLeft", + "/TwoColumnRight", + "/TwoPageLeft", + "/TwoPageRight", +] + +PagemodeType: TypeAlias = Literal[ + "/UseNone", + "/UseOutlines", + "/UseThumbs", + "/FullScreen", + "/UseOC", + "/UseAttachments", +] + +AnnotationSubtype: TypeAlias = Literal[ + "/Text", + "/Link", + "/FreeText", + "/Line", + "/Square", + "/Circle", + "/Polygon", + "/PolyLine", + "/Highlight", + "/Underline", + "/Squiggly", + "/StrikeOut", + "/Caret", + "/Stamp", + "/Ink", + "/Popup", + "/FileAttachment", + "/Sound", + "/Movie", + "/Screen", + "/Widget", + "/PrinterMark", + "/TrapNet", + "/Watermark", + "/3D", + "/Redact", + "/Projection", + "/RichMedia", +] diff --git a/app/vendor/pypdf/xmp.py b/app/vendor/pypdf/xmp.py new file mode 100644 index 0000000..4f45d68 --- /dev/null +++ b/app/vendor/pypdf/xmp.py @@ -0,0 +1,812 @@ +""" +Anything related to Extensible Metadata Platform (XMP) metadata. + +https://en.wikipedia.org/wiki/Extensible_Metadata_Platform +""" + +import datetime +import decimal +import re +from collections.abc import Iterator +from string import hexdigits +from typing import ( + Any, + Callable, + Optional, + TypeVar, + Union, + cast, +) +from xml.dom.expatbuilder import ExpatBuilderNS +from xml.dom.minidom import Document +from xml.dom.minidom import Element as XmlElement +from xml.dom.xmlbuilder import Options +from xml.parsers.expat import ExpatError, XMLParserType + +from ._configuration import get_configuration +from ._protocols import XmpInformationProtocol +from ._utils import StreamType, deprecate_with_replacement, deprecation_no_replacement +from .errors import LimitReachedError, PdfReadError, XmpDocumentError +from .generic import ContentStream, PdfObject, StreamObject + +XMP_MAX_INPUT_LENGTH = 5_000_000 # DEPRECATED: Use pypdf.Configuration. +XMP_MAX_ELEMENT_COUNT = 100_000 # DEPRECATED: Use pypdf.Configuration. + +RDF_NAMESPACE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#" +DC_NAMESPACE = "http://purl.org/dc/elements/1.1/" +XMP_NAMESPACE = "http://ns.adobe.com/xap/1.0/" +PDF_NAMESPACE = "http://ns.adobe.com/pdf/1.3/" +XMPMM_NAMESPACE = "http://ns.adobe.com/xap/1.0/mm/" + +# What is the PDFX namespace, you might ask? +# It's documented here: https://github.com/adobe/xmp-docs/raw/master/XMPSpecifications/XMPSpecificationPart3.pdf +# This namespace is used to place "custom metadata" +# properties, which are arbitrary metadata properties with no semantic or +# documented meaning. +# +# Elements in the namespace are key/value-style storage, +# where the element name is the key and the content is the value. The keys +# are transformed into valid XML identifiers by substituting an invalid +# identifier character with \u2182 followed by the unicode hex ID of the +# original character. A key like "my car" is therefore "my\u21820020car". +# +# \u2182 is the unicode character \u{ROMAN NUMERAL TEN THOUSAND} +# +# The pdfx namespace should be avoided. +# A custom data schema and sensical XML elements could be used instead, as is +# suggested by Adobe's own documentation on XMP under "Extensibility of +# Schemas". +PDFX_NAMESPACE = "http://ns.adobe.com/pdfx/1.3/" + +# PDF/A +PDFAID_NAMESPACE = "http://www.aiim.org/pdfa/ns/id/" + +# Internal mapping of namespace URI → prefix +_NAMESPACE_PREFIX_MAP = { + DC_NAMESPACE: "dc", + XMP_NAMESPACE: "xmp", + PDF_NAMESPACE: "pdf", + XMPMM_NAMESPACE: "xmpMM", + PDFAID_NAMESPACE: "pdfaid", + PDFX_NAMESPACE: "pdfx", +} + +iso8601 = re.compile( + """ + (?P[0-9]{4}) + (- + (?P[0-9]{2}) + (- + (?P[0-9]+) + (T + (?P[0-9]{2}): + (?P[0-9]{2}) + (:(?P[0-9]{2}(.[0-9]+)?))? + (?PZ|[-+][0-9]{2}:[0-9]{2}) + )? + )? + )? + """, + re.VERBOSE, +) + + +K = TypeVar("K") + +# Minimal XMP template +_MINIMAL_XMP = f""" + + + + + + +""" + + +def _identity(value: K) -> K: + return value + + +def _converter_date(value: str) -> datetime.datetime: + matches = iso8601.match(value) + if matches is None: + raise ValueError(f"Invalid date format: {value}") + year = int(matches.group("year")) + month = int(matches.group("month") or "1") + day = int(matches.group("day") or "1") + hour = int(matches.group("hour") or "0") + minute = int(matches.group("minute") or "0") + second = decimal.Decimal(matches.group("second") or "0") + seconds_dec = second.to_integral(decimal.ROUND_FLOOR) + milliseconds_dec = (second - seconds_dec) * 1_000_000 + + seconds = int(seconds_dec) + milliseconds = int(milliseconds_dec) + + tzd = matches.group("tzd") or "Z" + dt = datetime.datetime(year, month, day, hour, minute, seconds, milliseconds) + if tzd != "Z": + tzd_hours, tzd_minutes = (int(x) for x in tzd.split(":")) + tzd_hours *= -1 + if tzd_hours < 0: + tzd_minutes *= -1 + dt = dt + datetime.timedelta(hours=tzd_hours, minutes=tzd_minutes) + return dt + + +def _format_datetime_utc(value: datetime.datetime) -> str: + """Format a datetime as UTC with trailing 'Z'. + + - If the input is timezone-aware, convert to UTC first. + - If naive, assume UTC. + """ + if value.tzinfo is not None and value.utcoffset() is not None: + value = value.astimezone(datetime.timezone.utc) + + value = value.replace(tzinfo=None) + return value.strftime("%Y-%m-%dT%H:%M:%S.%fZ") + + +def _generic_get( + element: XmlElement, self: "XmpInformation", list_type: str, converter: Callable[[Any], Any] = _identity +) -> Optional[list[str]]: + containers = element.getElementsByTagNameNS(RDF_NAMESPACE, list_type) + retval: list[Any] = [] + if len(containers): + for container in containers: + for item in container.getElementsByTagNameNS(RDF_NAMESPACE, "li"): + value = self._get_text(item) + value = converter(value) + retval.append(value) + return retval + return None + + +class _XmpBuilder(ExpatBuilderNS): + """ + Custom XML parser denying all entity declarations. + + This is a stripped down and typed version inspired by what *defusedxml* does. + + Why do we need this? The default limits of *libexpat* used by Python only block exponential entity expansion, + but not cases like quadratic entity expansion which can still cause quite some memory usage. + """ + + def __init__(self, options: Optional[Options] = None) -> None: + super().__init__(options=options) + self._element_count = 0 + self._configuration = get_configuration() + + def custom_entity_declaration_handler( + self, + entity_name: str, + # expat passes an int here rather than a bool, but typeshed declares + # the handler with a bool, so the annotation follows typeshed. + is_parameter_entity: bool, + value: Optional[str], + base: Optional[str], + system_id: str, + public_id: Optional[str], + notation_name: Optional[str], + ) -> None: + raise ExpatError(f"Forbidden entities: {entity_name!r}") + + def start_element_handler(self, name: str, attributes: list[str]) -> None: + self._element_count += 1 + if self._element_count > self._configuration.xmp_maximum_element_count: + raise LimitReachedError( + f"XMP metadata exceeds limit of {self._configuration.xmp_maximum_element_count} elements." + ) + super().start_element_handler(name=name, attributes=attributes) + + def install(self, parser: XMLParserType) -> None: + super().install(parser) + + parser.EntityDeclHandler = self.custom_entity_declaration_handler + parser.StartElementHandler = self.start_element_handler + + +class XmpInformation(XmpInformationProtocol, PdfObject): + """ + An object that represents Extensible Metadata Platform (XMP) metadata. + Usually accessed by :py:attr:`xmp_metadata()`. + + Raises: + PdfReadError: if XML is invalid + + """ + + def __init__(self, stream: StreamObject) -> None: + self.stream = stream + try: + data = self.stream.get_data() + configuration = get_configuration() + if (length := len(data)) > configuration.xmp_maximum_input_length: + raise LimitReachedError( + f"XMP stream size {length} exceeds limit of {configuration.xmp_maximum_input_length}." + ) + doc_root: Document = _XmpBuilder().parseString(data) + except (AttributeError, ExpatError) as e: + raise PdfReadError(f"XML in XmpInformation was invalid: {e}") + rdf_roots = doc_root.getElementsByTagNameNS(RDF_NAMESPACE, "RDF") + if not rdf_roots: + raise PdfReadError( + "XML in XmpInformation was invalid: Missing rdf:RDF root element" + ) + self.rdf_root: XmlElement = rdf_roots[0] + self.cache: dict[Any, Any] = {} + + @classmethod + def create(cls) -> "XmpInformation": + """ + Create a new XmpInformation object with minimal structure. + + Returns: + A new XmpInformation instance with empty metadata fields. + """ + stream = ContentStream(None, None) + stream.set_data(_MINIMAL_XMP.encode("utf-8")) + return cls(stream) + + def write_to_stream( + self, stream: StreamType, encryption_key: Union[str, bytes, None] = None + ) -> None: + deprecate_with_replacement( + "XmpInformation.write_to_stream", + "PdfWriter.xmp_metadata", + "6.0.0" + ) + if encryption_key is not None: # deprecated + deprecation_no_replacement( + "the encryption_key parameter of write_to_stream", "5.0.0" + ) + self.stream.write_to_stream(stream) + + def get_element(self, about_uri: str, namespace: str, name: str) -> Iterator[Any]: + for desc in self.rdf_root.getElementsByTagNameNS(RDF_NAMESPACE, "Description"): + if desc.getAttributeNS(RDF_NAMESPACE, "about") == about_uri: + attr = desc.getAttributeNodeNS(namespace, name) + if attr is not None: + yield attr + yield from desc.getElementsByTagNameNS(namespace, name) + + def get_nodes_in_namespace(self, about_uri: str, namespace: str) -> Iterator[Any]: + for desc in self.rdf_root.getElementsByTagNameNS(RDF_NAMESPACE, "Description"): + if desc.getAttributeNS(RDF_NAMESPACE, "about") == about_uri: + for i in range(desc.attributes.length): + attr = desc.attributes.item(i) + if attr and attr.namespaceURI == namespace: + yield attr + for child in desc.childNodes: + if child.namespaceURI == namespace: + yield child + + def _get_text(self, element: XmlElement) -> str: + text = "" + for child in element.childNodes: + if child.nodeType == child.TEXT_NODE: + text += child.data + return text + + def _get_single_value( + self, + namespace: str, + name: str, + converter: Callable[[str], Any] = _identity, + ) -> Optional[Any]: + cached = self.cache.get(namespace, {}).get(name) + if cached: + return cached + value = None + for element in self.get_element("", namespace, name): + if element.nodeType == element.ATTRIBUTE_NODE: + value = element.nodeValue + else: + value = self._get_text(element) + break + if value is not None: + value = converter(value) + ns_cache = self.cache.setdefault(namespace, {}) + ns_cache[name] = value + return value + + def _getter_bag(self, namespace: str, name: str) -> Optional[list[str]]: + cached = self.cache.get(namespace, {}).get(name) + if cached: + return cast(list[str], cached) + retval: list[str] = [] + for element in self.get_element("", namespace, name): + if (bags := _generic_get(element, self, list_type="Bag")) is not None: + retval.extend(bags) + else: + value = self._get_text(element) + retval.append(value) + ns_cache = self.cache.setdefault(namespace, {}) + ns_cache[name] = retval + return retval + + def _get_seq_values( + self, + namespace: str, + name: str, + converter: Callable[[Any], Any] = _identity, + ) -> Optional[list[Any]]: + cached = self.cache.get(namespace, {}).get(name) + if cached: + return cast(list[Any], cached) + retval: list[Any] = [] + for element in self.get_element("", namespace, name): + if (seqs := _generic_get(element, self, list_type="Seq", converter=converter)) is not None: + retval.extend(seqs) + elif (bags := _generic_get(element, self, list_type="Bag")) is not None: + # See issue at https://github.com/py-pdf/pypdf/issues/3324 + # Some applications violate the XMP metadata standard regarding `dc:creator` which should + # be an "ordered array" and thus a sequence, but use an unordered array (bag) instead. + # This seems to stem from the fact that the original Dublin Core specification does indeed + # use bags or direct values, while PDFs are expected to follow the XMP standard and ignore + # the plain Dublin Core variant. For this reason, add a fallback here to deal with such + # issues accordingly. + retval.extend(bags) + else: + value = converter(self._get_text(element)) + retval.append(value) + ns_cache = self.cache.setdefault(namespace, {}) + ns_cache[name] = retval + return retval + + def _get_langalt_values(self, namespace: str, name: str) -> Optional[dict[Any, Any]]: + cached = self.cache.get(namespace, {}).get(name) + if cached: + return cast(dict[Any, Any], cached) + retval: dict[Any, Any] = {} + for element in self.get_element("", namespace, name): + alts = element.getElementsByTagNameNS(RDF_NAMESPACE, "Alt") + if len(alts): + for alt in alts: + for item in alt.getElementsByTagNameNS(RDF_NAMESPACE, "li"): + value = self._get_text(item) + retval[item.getAttribute("xml:lang")] = value + else: + retval["x-default"] = self._get_text(element) + ns_cache = self.cache.setdefault(namespace, {}) + ns_cache[name] = retval + return retval + + @property + def dc_contributor(self) -> Optional[list[str]]: + """Contributors to the resource (other than the authors).""" + return self._getter_bag(DC_NAMESPACE, "contributor") + + @dc_contributor.setter + def dc_contributor(self, values: Optional[list[str]]) -> None: + self._set_bag_values(DC_NAMESPACE, "contributor", values) + + @property + def dc_coverage(self) -> Optional[str]: + """Text describing the extent or scope of the resource.""" + return self._get_single_value(DC_NAMESPACE, "coverage") + + @dc_coverage.setter + def dc_coverage(self, value: Optional[str]) -> None: + self._set_single_value(DC_NAMESPACE, "coverage", value) + + @property + def dc_creator(self) -> Optional[list[str]]: + """A sorted array of names of the authors of the resource, listed in order of precedence.""" + return self._get_seq_values(DC_NAMESPACE, "creator") + + @dc_creator.setter + def dc_creator(self, values: Optional[list[str]]) -> None: + self._set_seq_values(DC_NAMESPACE, "creator", values) + + @property + def dc_date(self) -> Optional[list[datetime.datetime]]: + """A sorted array of dates of significance to the resource. The dates and times are in UTC.""" + return self._get_seq_values(DC_NAMESPACE, "date", _converter_date) + + @dc_date.setter + def dc_date(self, values: Optional[list[Union[str, datetime.datetime]]]) -> None: + if values is None: + self._set_seq_values(DC_NAMESPACE, "date", None) + else: + date_strings = [] + for value in values: + if isinstance(value, datetime.datetime): + date_strings.append(_format_datetime_utc(value)) + else: + date_strings.append(str(value)) + self._set_seq_values(DC_NAMESPACE, "date", date_strings) + + @property + def dc_description(self) -> Optional[dict[str, str]]: + """A language-keyed dictionary of textual descriptions of the content of the resource.""" + return self._get_langalt_values(DC_NAMESPACE, "description") + + @dc_description.setter + def dc_description(self, values: Optional[dict[str, str]]) -> None: + self._set_langalt_values(DC_NAMESPACE, "description", values) + + @property + def dc_format(self) -> Optional[str]: + """The mime-type of the resource.""" + return self._get_single_value(DC_NAMESPACE, "format") + + @dc_format.setter + def dc_format(self, value: Optional[str]) -> None: + self._set_single_value(DC_NAMESPACE, "format", value) + + @property + def dc_identifier(self) -> Optional[str]: + """Unique identifier of the resource.""" + return self._get_single_value(DC_NAMESPACE, "identifier") + + @dc_identifier.setter + def dc_identifier(self, value: Optional[str]) -> None: + self._set_single_value(DC_NAMESPACE, "identifier", value) + + @property + def dc_language(self) -> Optional[list[str]]: + """An unordered array specifying the languages used in the resource.""" + return self._getter_bag(DC_NAMESPACE, "language") + + @dc_language.setter + def dc_language(self, values: Optional[list[str]]) -> None: + self._set_bag_values(DC_NAMESPACE, "language", values) + + @property + def dc_publisher(self) -> Optional[list[str]]: + """An unordered array of publisher names.""" + return self._getter_bag(DC_NAMESPACE, "publisher") + + @dc_publisher.setter + def dc_publisher(self, values: Optional[list[str]]) -> None: + self._set_bag_values(DC_NAMESPACE, "publisher", values) + + @property + def dc_relation(self) -> Optional[list[str]]: + """An unordered array of text descriptions of relationships to other documents.""" + return self._getter_bag(DC_NAMESPACE, "relation") + + @dc_relation.setter + def dc_relation(self, values: Optional[list[str]]) -> None: + self._set_bag_values(DC_NAMESPACE, "relation", values) + + @property + def dc_rights(self) -> Optional[dict[str, str]]: + """A language-keyed dictionary of textual descriptions of the rights the user has to this resource.""" + return self._get_langalt_values(DC_NAMESPACE, "rights") + + @dc_rights.setter + def dc_rights(self, values: Optional[dict[str, str]]) -> None: + self._set_langalt_values(DC_NAMESPACE, "rights", values) + + @property + def dc_source(self) -> Optional[str]: + """Unique identifier of the work from which this resource was derived.""" + return self._get_single_value(DC_NAMESPACE, "source") + + @dc_source.setter + def dc_source(self, value: Optional[str]) -> None: + self._set_single_value(DC_NAMESPACE, "source", value) + + @property + def dc_subject(self) -> Optional[list[str]]: + """An unordered array of descriptive phrases or keywords that specify the topic of the content.""" + return self._getter_bag(DC_NAMESPACE, "subject") + + @dc_subject.setter + def dc_subject(self, values: Optional[list[str]]) -> None: + self._set_bag_values(DC_NAMESPACE, "subject", values) + + @property + def dc_title(self) -> Optional[dict[str, str]]: + """A language-keyed dictionary of the title of the resource.""" + return self._get_langalt_values(DC_NAMESPACE, "title") + + @dc_title.setter + def dc_title(self, values: Optional[dict[str, str]]) -> None: + self._set_langalt_values(DC_NAMESPACE, "title", values) + + @property + def dc_type(self) -> Optional[list[str]]: + """An unordered array of textual descriptions of the document type.""" + return self._getter_bag(DC_NAMESPACE, "type") + + @dc_type.setter + def dc_type(self, values: Optional[list[str]]) -> None: + self._set_bag_values(DC_NAMESPACE, "type", values) + + @property + def pdf_keywords(self) -> Optional[str]: + """An unformatted text string representing document keywords.""" + return self._get_single_value(PDF_NAMESPACE, "Keywords") + + @pdf_keywords.setter + def pdf_keywords(self, value: Optional[str]) -> None: + self._set_single_value(PDF_NAMESPACE, "Keywords", value) + + @property + def pdf_pdfversion(self) -> Optional[str]: + """The PDF file version, for example 1.0 or 1.3.""" + return self._get_single_value(PDF_NAMESPACE, "PDFVersion") + + @pdf_pdfversion.setter + def pdf_pdfversion(self, value: Optional[str]) -> None: + self._set_single_value(PDF_NAMESPACE, "PDFVersion", value) + + @property + def pdf_producer(self) -> Optional[str]: + """The name of the tool that saved the document as a PDF.""" + return self._get_single_value(PDF_NAMESPACE, "Producer") + + @pdf_producer.setter + def pdf_producer(self, value: Optional[str]) -> None: + self._set_single_value(PDF_NAMESPACE, "Producer", value) + + @property + def xmp_create_date(self) -> Optional[datetime.datetime]: + """The date and time the resource was originally created. Returned as a UTC datetime object.""" + return self._get_single_value(XMP_NAMESPACE, "CreateDate", _converter_date) + + @xmp_create_date.setter + def xmp_create_date(self, value: Optional[datetime.datetime]) -> None: + if value: + date_str = _format_datetime_utc(value) + self._set_single_value(XMP_NAMESPACE, "CreateDate", date_str) + else: + self._set_single_value(XMP_NAMESPACE, "CreateDate", None) + + @property + def xmp_modify_date(self) -> Optional[datetime.datetime]: + """The date and time the resource was last modified. Returned as a UTC datetime object.""" + return self._get_single_value(XMP_NAMESPACE, "ModifyDate", _converter_date) + + @xmp_modify_date.setter + def xmp_modify_date(self, value: Optional[datetime.datetime]) -> None: + if value: + date_str = _format_datetime_utc(value) + self._set_single_value(XMP_NAMESPACE, "ModifyDate", date_str) + else: + self._set_single_value(XMP_NAMESPACE, "ModifyDate", None) + + @property + def xmp_metadata_date(self) -> Optional[datetime.datetime]: + """The date and time that any metadata for this resource was last changed. Returned as a UTC datetime object.""" + return self._get_single_value(XMP_NAMESPACE, "MetadataDate", _converter_date) + + @xmp_metadata_date.setter + def xmp_metadata_date(self, value: Optional[datetime.datetime]) -> None: + if value: + date_str = _format_datetime_utc(value) + self._set_single_value(XMP_NAMESPACE, "MetadataDate", date_str) + else: + self._set_single_value(XMP_NAMESPACE, "MetadataDate", None) + + @property + def xmp_creator_tool(self) -> Optional[str]: + """The name of the first known tool used to create the resource.""" + return self._get_single_value(XMP_NAMESPACE, "CreatorTool") + + @xmp_creator_tool.setter + def xmp_creator_tool(self, value: Optional[str]) -> None: + self._set_single_value(XMP_NAMESPACE, "CreatorTool", value) + + @property + def xmpmm_document_id(self) -> Optional[str]: + """The common identifier for all versions and renditions of this resource.""" + return self._get_single_value(XMPMM_NAMESPACE, "DocumentID") + + @xmpmm_document_id.setter + def xmpmm_document_id(self, value: Optional[str]) -> None: + self._set_single_value(XMPMM_NAMESPACE, "DocumentID", value) + + @property + def xmpmm_instance_id(self) -> Optional[str]: + """An identifier for a specific incarnation of a document, updated each time a file is saved.""" + return self._get_single_value(XMPMM_NAMESPACE, "InstanceID") + + @xmpmm_instance_id.setter + def xmpmm_instance_id(self, value: Optional[str]) -> None: + self._set_single_value(XMPMM_NAMESPACE, "InstanceID", value) + + @property + def pdfaid_part(self) -> Optional[str]: + """The part of the PDF/A standard that the document conforms to (e.g., 1, 2, 3).""" + return self._get_single_value(PDFAID_NAMESPACE, "part") + + @pdfaid_part.setter + def pdfaid_part(self, value: Optional[str]) -> None: + self._set_single_value(PDFAID_NAMESPACE, "part", value) + + @property + def pdfaid_conformance(self) -> Optional[str]: + """The conformance level within the PDF/A standard (e.g., 'A', 'B', 'U').""" + return self._get_single_value(PDFAID_NAMESPACE, "conformance") + + @pdfaid_conformance.setter + def pdfaid_conformance(self, value: Optional[str]) -> None: + self._set_single_value(PDFAID_NAMESPACE, "conformance", value) + + @property + def custom_properties(self) -> dict[Any, Any]: + """ + Retrieve custom metadata properties defined in the undocumented pdfx + metadata schema. + + Returns: + A dictionary of key/value items for custom metadata properties. + + """ + if not hasattr(self, "_custom_properties"): + self._custom_properties = {} + for node in self.get_nodes_in_namespace("", PDFX_NAMESPACE): + key = node.localName + start = 0 + while True: + # see documentation about PDFX_NAMESPACE earlier in file + idx = key.find("\u2182", start) + if idx == -1: + break + hex_id = key[idx + 1 : idx + 5] + if len(hex_id) != 4 or not all(c in hexdigits for c in hex_id): + # Not a well-formed escape; leave the marker untouched + # and continue past it instead of crashing. + start = idx + 1 + continue + key = key[:idx] + chr(int(hex_id, base=16)) + key[idx + 5 :] + start = idx + 1 + if node.nodeType == node.ATTRIBUTE_NODE: + value = node.nodeValue + else: + value = self._get_text(node) + self._custom_properties[key] = value + return self._custom_properties + + def _get_or_create_description(self, about_uri: str = "") -> XmlElement: + """Get or create an rdf:Description element with the given about URI.""" + for desc in self.rdf_root.getElementsByTagNameNS(RDF_NAMESPACE, "Description"): + if desc.getAttributeNS(RDF_NAMESPACE, "about") == about_uri: + return desc + + doc = self.rdf_root.ownerDocument + if doc is None: + raise XmpDocumentError("XMP Document is None") + desc = doc.createElementNS(RDF_NAMESPACE, "rdf:Description") + desc.setAttributeNS(RDF_NAMESPACE, "rdf:about", about_uri) + self.rdf_root.appendChild(desc) + return desc + + def _clear_cache_entry(self, namespace: str, name: str) -> None: + """Remove a cached value for a given namespace/name if present.""" + ns_cache = self.cache.get(namespace) + if ns_cache and name in ns_cache: + del ns_cache[name] + + def _set_single_value(self, namespace: str, name: str, value: Optional[str]) -> None: + """Set or remove a single metadata value.""" + self._clear_cache_entry(namespace, name) + desc = self._get_or_create_description() + + existing_elements = list(desc.getElementsByTagNameNS(namespace, name)) + for elem in existing_elements: + desc.removeChild(elem) + + if existing_attr := desc.getAttributeNodeNS(namespace, name): + desc.removeAttributeNode(existing_attr) + + if value is not None: + doc = self.rdf_root.ownerDocument + if doc is None: + raise XmpDocumentError("XMP Document is None") + prefix = self._get_namespace_prefix(namespace) + elem = doc.createElementNS(namespace, f"{prefix}:{name}") + text_node = doc.createTextNode(str(value)) + elem.appendChild(text_node) + desc.appendChild(elem) + + self._update_stream() + + def _set_bag_values(self, namespace: str, name: str, values: Optional[list[str]]) -> None: + """Set or remove bag values (unordered array).""" + self._clear_cache_entry(namespace, name) + desc = self._get_or_create_description() + + existing_elements = list(desc.getElementsByTagNameNS(namespace, name)) + for elem in existing_elements: + desc.removeChild(elem) + + if values: + doc = self.rdf_root.ownerDocument + if doc is None: + raise XmpDocumentError("XMP Document is None") + prefix = self._get_namespace_prefix(namespace) + elem = doc.createElementNS(namespace, f"{prefix}:{name}") + bag = doc.createElementNS(RDF_NAMESPACE, "rdf:Bag") + + for value in values: + li = doc.createElementNS(RDF_NAMESPACE, "rdf:li") + text_node = doc.createTextNode(str(value)) + li.appendChild(text_node) + bag.appendChild(li) + + elem.appendChild(bag) + desc.appendChild(elem) + + self._update_stream() + + def _set_seq_values(self, namespace: str, name: str, values: Optional[list[str]]) -> None: + """Set or remove sequence values (ordered array).""" + self._clear_cache_entry(namespace, name) + desc = self._get_or_create_description() + + existing_elements = list(desc.getElementsByTagNameNS(namespace, name)) + for elem in existing_elements: + desc.removeChild(elem) + + if values: + doc = self.rdf_root.ownerDocument + if doc is None: + raise XmpDocumentError("XMP Document is None") + prefix = self._get_namespace_prefix(namespace) + elem = doc.createElementNS(namespace, f"{prefix}:{name}") + seq = doc.createElementNS(RDF_NAMESPACE, "rdf:Seq") + + for value in values: + li = doc.createElementNS(RDF_NAMESPACE, "rdf:li") + text_node = doc.createTextNode(str(value)) + li.appendChild(text_node) + seq.appendChild(li) + + elem.appendChild(seq) + desc.appendChild(elem) + + self._update_stream() + + def _set_langalt_values(self, namespace: str, name: str, values: Optional[dict[str, str]]) -> None: + """Set or remove language alternative values.""" + self._clear_cache_entry(namespace, name) + desc = self._get_or_create_description() + + existing_elements = list(desc.getElementsByTagNameNS(namespace, name)) + for elem in existing_elements: + desc.removeChild(elem) + + if values: + doc = self.rdf_root.ownerDocument + if doc is None: + raise XmpDocumentError("XMP Document is None") + prefix = self._get_namespace_prefix(namespace) + elem = doc.createElementNS(namespace, f"{prefix}:{name}") + alt = doc.createElementNS(RDF_NAMESPACE, "rdf:Alt") + + for lang, value in values.items(): + li = doc.createElementNS(RDF_NAMESPACE, "rdf:li") + li.setAttribute("xml:lang", lang) + text_node = doc.createTextNode(str(value)) + li.appendChild(text_node) + alt.appendChild(li) + + elem.appendChild(alt) + desc.appendChild(elem) + + self._update_stream() + + def _get_namespace_prefix(self, namespace: str) -> str: + """Get the appropriate namespace prefix for a given namespace URI.""" + return _NAMESPACE_PREFIX_MAP.get(namespace, "unknown") + + def _update_stream(self) -> None: + """Update the stream with the current XML content.""" + doc = self.rdf_root.ownerDocument + if doc is None: + raise XmpDocumentError("XMP Document is None") + + xml_data = doc.toxml(encoding="utf-8") + self.stream.set_data(xml_data) diff --git a/requirements.txt b/requirements.txt index 3f3bc01..6eb78c4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,3 +15,4 @@ starlette==1.6.0 typing-inspection==0.4.4 typing_extensions==4.16.0 uvicorn==0.53.0 +pypdf>=5.0.0 diff --git a/tests/test_api.py b/tests/test_api.py index 232106a..04958a8 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -891,3 +891,123 @@ def test_duplicate_prevention_and_in_library_flag(): data_new = resp_lookup_new.json() assert data_new["in_library"] is False assert data_new["library_card"] is None + + +def make_mock_pdf(title="CCNA 200-301 Official Guide", author="Wendell Odom", with_outline=True): + import io + import pypdf + from pypdf.generic import DecodedStreamObject, NameObject, DictionaryObject + + writer = pypdf.PdfWriter() + + def add_text_page(text_lines): + page = writer.add_blank_page(width=400, height=400) + stream_content = [b"BT /F1 12 Tf 50 350 Td"] + for idx, l in enumerate(text_lines): + if idx > 0: + stream_content.append(b"0 -15 Td") + escaped = l.replace("\\", "\\\\").replace("(", "\\(").replace(")", "\\)") + stream_content.append(f"({escaped}) Tj".encode("latin1")) + stream_content.append(b"ET") + + content = DecodedStreamObject() + content.set_data(b" ".join(stream_content)) + page[NameObject("/Contents")] = content + page[NameObject("/Resources")] = DictionaryObject({ + NameObject("/Font"): DictionaryObject({ + NameObject("/F1"): DictionaryObject({ + NameObject("/Type"): NameObject("/Font"), + NameObject("/Subtype"): NameObject("/Type1"), + NameObject("/BaseFont"): NameObject("/Helvetica"), + }) + }) + }) + + p0_lines = ["Introduction to Networking", "CCNA Official Guide Header"] + [f"Introductory networking line {i} with several words for threshold." for i in range(12)] + p1_lines = ["Chapter 1: Ethernet Switching", "Router# show ip interface brief", "GigabitEthernet0/0 10.0.0.1 up up", "Router# configure terminal", "Switch(config)# vlan 10"] + [f"Ethernet switching explanation line {i} with concepts." for i in range(12)] + p2_lines = ["Chapter 2: IPv4 Routing", "Router# show ip route", "C 10.0.0.0/24 is directly connected"] + [f"IPv4 routing explanation line {i} with concepts." for i in range(12)] + + add_text_page(p0_lines) + add_text_page(p1_lines) + add_text_page(p2_lines) + + if with_outline: + writer.add_outline_item("Introduction", 0) + writer.add_outline_item("Chapter 1: Ethernet Switching", 1) + writer.add_outline_item("Chapter 2: IPv4 Routing", 2) + + buf = io.BytesIO() + writer.write(buf) + return buf.getvalue() + + +def test_pdf_reader_unit(): + import io + import pypdf + from app.reader import parse_pdf, format_cli_blocks, clean_page_lines + + # 1. Test CLI block detection + sample_cli = "Check interfaces:\nRouter# show ip interface brief\nGigabitEthernet0/0 10.0.0.1 up up\nRouter# exit\nAll done." + formatted = format_cli_blocks(sample_cli) + assert "```text" in formatted + assert "Router# show ip interface brief" in formatted + assert "```" in formatted + + # 2. Test outline-based PDF parsing + pdf_bytes = make_mock_pdf("CCNA Guide", "Cisco Press", with_outline=True) + title, author, chapters = parse_pdf(pdf_bytes, filename="ccna_guide.pdf") + assert title == "ccna_guide" + assert len(chapters) == 3 + assert chapters[0]["title"] == "Introduction" + assert chapters[1]["title"] == "Chapter 1: Ethernet Switching" + assert chapters[2]["title"] == "Chapter 2: IPv4 Routing" + assert "```text" in chapters[1]["content"] + + # 3. Test fallback when no outline (heading regex detection) + pdf_no_outline = make_mock_pdf("Manual", "Admin", with_outline=False) + t2, a2, chs2 = parse_pdf(pdf_no_outline, filename="network_manual.pdf") + assert len(chs2) >= 2 + assert any("Chapter 1" in ch["title"] for ch in chs2) + + # 4. Test Scanned PDF Guard (< 100 words raises ValueError) + empty_writer = pypdf.PdfWriter() + empty_writer.add_blank_page(width=100, height=100) + buf_scanned = io.BytesIO() + empty_writer.write(buf_scanned) + import pytest + with pytest.raises(ValueError, match="текстового слоя"): + parse_pdf(buf_scanned.getvalue(), filename="scan.pdf") + + +def test_books_pdf_multipart_upload(): + # 1. Successful PDF upload + pdf_data = make_mock_pdf() + files = {"file": ("ccna_cert_guide.pdf", pdf_data, "application/pdf")} + resp = client.post("/api/books", files=files) + assert resp.status_code == 200 + book = resp.json() + assert book["title"] == "ccna_cert_guide" + assert book["source_type"] == "pdf" + assert len(book["chapters"]) == 3 + assert book["total_words"] > 100 + book_id = book["id"] + + # Verify retrieval + get_resp = client.get(f"/api/books/{book_id}") + assert get_resp.status_code == 200 + assert get_resp.json()["source_type"] == "pdf" + + # 2. Upload scanned PDF without OCR text returns 400 with informative detail + import io, pypdf + empty_writer = pypdf.PdfWriter() + empty_writer.add_blank_page(width=100, height=100) + buf = io.BytesIO() + empty_writer.write(buf) + + scan_files = {"file": ("scanned_cisco.pdf", buf.getvalue(), "application/pdf")} + err_resp = client.post("/api/books", files=scan_files) + assert err_resp.status_code == 400 + assert "текстового слоя" in err_resp.json()["detail"] + + # Cleanup + client.delete(f"/api/books/{book_id}")