codeannex 0.1.1__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codeannex-0.2.0/PKG-INFO +116 -0
- codeannex-0.2.0/README.md +85 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex/__main__.py +97 -5
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex/config.py +27 -0
- codeannex-0.2.0/codeannex/fonts.py +275 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex/pdf_builder.py +123 -77
- codeannex-0.2.0/codeannex/text_utils.py +131 -0
- codeannex-0.2.0/codeannex.egg-info/PKG-INFO +116 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex.egg-info/SOURCES.txt +5 -1
- {codeannex-0.1.1 → codeannex-0.2.0}/pyproject.toml +1 -1
- codeannex-0.2.0/tests/test_fonts_dynamic.py +47 -0
- codeannex-0.2.0/tests/test_interactive.py +65 -0
- codeannex-0.2.0/tests/test_summary_hierarchy.py +56 -0
- codeannex-0.2.0/tests/test_text_utils.py +25 -0
- codeannex-0.1.1/PKG-INFO +0 -147
- codeannex-0.1.1/README.md +0 -116
- codeannex-0.1.1/codeannex/fonts.py +0 -271
- codeannex-0.1.1/codeannex/text_utils.py +0 -128
- codeannex-0.1.1/codeannex.egg-info/PKG-INFO +0 -147
- {codeannex-0.1.1 → codeannex-0.2.0}/LICENSE +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex/__init__.py +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex/file_utils.py +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex/highlight.py +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex.egg-info/dependency_links.txt +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex.egg-info/entry_points.txt +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex.egg-info/requires.txt +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/codeannex.egg-info/top_level.txt +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/setup.cfg +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/tests/test_emoji.py +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/tests/test_file_management.py +0 -0
- {codeannex-0.1.1 → codeannex-0.2.0}/tests/test_pdf_layout.py +0 -0
codeannex-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: codeannex
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Generates a professional PDF source code annex with Smart Index, Images and Emoji support.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Repository, https://github.com/tanhleno/codeannex
|
|
7
|
+
Keywords: pdf,source-code,documentation,annex,reportlab,emoji
|
|
8
|
+
Classifier: Development Status :: 4 - Beta
|
|
9
|
+
Classifier: Environment :: Console
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Documentation
|
|
17
|
+
Classifier: Topic :: Software Development :: Documentation
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: reportlab>=4.0
|
|
22
|
+
Requires-Dist: Pillow>=10.0
|
|
23
|
+
Requires-Dist: Pygments>=2.16
|
|
24
|
+
Requires-Dist: pdfminer.six>=20231228
|
|
25
|
+
Provides-Extra: svg
|
|
26
|
+
Requires-Dist: cairosvg>=2.7; extra == "svg"
|
|
27
|
+
Requires-Dist: svglib>=1.5; extra == "svg"
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# 📂 codeannex
|
|
33
|
+
|
|
34
|
+
Generates a professional PDF annex from a project's source code — with syntax highlighting, a hierarchical table of contents, image rendering, and intelligent font discovery.
|
|
35
|
+
|
|
36
|
+
## 🚀 Key Features
|
|
37
|
+
|
|
38
|
+
- **Interactive Wizard** — Run without arguments to configure your PDF step-by-step. It intelligently skips irrelevant questions (like starting page if numbering is disabled).
|
|
39
|
+
- **Intelligent Font Discovery** — Automatically finds and registers fonts from your system (Windows, Linux, macOS) by name (e.g., `--title-font "Play"`).
|
|
40
|
+
- **Fully Customizable UI** — Change every label, color, and size via CLI. Default labels are in English for international consistency.
|
|
41
|
+
- **Hierarchical Summary** — Real tree-structured Table of Contents with terminal-like connection lines.
|
|
42
|
+
- **Customizable Primary Color** — Set the theme color for headers, folder icons, and accents with `--primary-color`.
|
|
43
|
+
- **Smart Contrast** — Automatically switches header text between black and white based on the brightness of your primary color for maximum legibility.
|
|
44
|
+
- **Flexible Page Styling** — Support for custom page background and code block colors.
|
|
45
|
+
- **Git-Aware** — Perfectly interprets `.gitignore` using Git's native engine.
|
|
46
|
+
|
|
47
|
+
## 🛠 Installation
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
pip install codeannex
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 📖 Usage
|
|
54
|
+
|
|
55
|
+
### Interactive Mode (Wizard)
|
|
56
|
+
Simply run without arguments to start the step-by-step configuration:
|
|
57
|
+
```bash
|
|
58
|
+
codeannex .
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Automation / CI
|
|
62
|
+
Use the `--no-input` flag to disable the wizard in automated environments:
|
|
63
|
+
```bash
|
|
64
|
+
codeannex . --no-input
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Professional Customization Example
|
|
68
|
+
```bash
|
|
69
|
+
python3 -m codeannex . \
|
|
70
|
+
--cover-title "Anexo I" \
|
|
71
|
+
--no-page-numbers \
|
|
72
|
+
--primary-color "#0f4761" \
|
|
73
|
+
--title-font "Play" --title-size 20 \
|
|
74
|
+
--normal-font "Times New Roman" --normal-size 12 \
|
|
75
|
+
--margin-top 2.5 --margin-bottom 2.5 --margin-left 3 --margin-right 3 \
|
|
76
|
+
--code-bg "#f5f5f5" --code-size 9
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
## ⚙️ Configuration Options
|
|
80
|
+
|
|
81
|
+
### Document & Labels
|
|
82
|
+
- `--cover-title TITLE` — Title for the cover (default: "ANEXO TÉCNICO").
|
|
83
|
+
- `--cover-subtitle SUB` — Subtitle for the cover.
|
|
84
|
+
- `--summary-title TITLE` — Title for the summary page.
|
|
85
|
+
- `--repo-label LABEL` — Prefix for repository link (default: "Repository: ").
|
|
86
|
+
- `--project-label LABEL` — Prefix for project name in footer.
|
|
87
|
+
- `--file-part-format FMT` — Format for file parts (default: "({current}/{total})").
|
|
88
|
+
- `--no-input` — Disable the interactive wizard.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
### Colors & Themes
|
|
92
|
+
- `--primary-color HEX` — Main color for headers and accents.
|
|
93
|
+
- `--page-bg-color HEX` — Background color for all pages.
|
|
94
|
+
- `--code-bg HEX` — Background color for code blocks.
|
|
95
|
+
- `--normal-color HEX` / `--title-color HEX` — Text colors.
|
|
96
|
+
|
|
97
|
+
### Fonts & Sizes
|
|
98
|
+
- `--title-font` / `--subtitle-font` / `--normal-font` — System font names.
|
|
99
|
+
- `--mono-font` — Font for code (e.g., "Consolas", "Ubuntu Mono").
|
|
100
|
+
- `--emoji-font` — Font for emojis (e.g., "Noto Emoji").
|
|
101
|
+
- `--code-size N` — Font size for code blocks.
|
|
102
|
+
|
|
103
|
+
### Layout
|
|
104
|
+
- `--margin CM` — General margin for all sides.
|
|
105
|
+
- `--margin-top`, `--margin-bottom`, `--margin-left`, `--margin-right` — Specific margins in cm.
|
|
106
|
+
- `--no-page-numbers` — Disable page numbering.
|
|
107
|
+
|
|
108
|
+
## 🧪 Testing
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
PYTHONPATH=. pytest
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## 📄 License
|
|
115
|
+
|
|
116
|
+
MIT
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# 📂 codeannex
|
|
2
|
+
|
|
3
|
+
Generates a professional PDF annex from a project's source code — with syntax highlighting, a hierarchical table of contents, image rendering, and intelligent font discovery.
|
|
4
|
+
|
|
5
|
+
## 🚀 Key Features
|
|
6
|
+
|
|
7
|
+
- **Interactive Wizard** — Run without arguments to configure your PDF step-by-step. It intelligently skips irrelevant questions (like starting page if numbering is disabled).
|
|
8
|
+
- **Intelligent Font Discovery** — Automatically finds and registers fonts from your system (Windows, Linux, macOS) by name (e.g., `--title-font "Play"`).
|
|
9
|
+
- **Fully Customizable UI** — Change every label, color, and size via CLI. Default labels are in English for international consistency.
|
|
10
|
+
- **Hierarchical Summary** — Real tree-structured Table of Contents with terminal-like connection lines.
|
|
11
|
+
- **Customizable Primary Color** — Set the theme color for headers, folder icons, and accents with `--primary-color`.
|
|
12
|
+
- **Smart Contrast** — Automatically switches header text between black and white based on the brightness of your primary color for maximum legibility.
|
|
13
|
+
- **Flexible Page Styling** — Support for custom page background and code block colors.
|
|
14
|
+
- **Git-Aware** — Perfectly interprets `.gitignore` using Git's native engine.
|
|
15
|
+
|
|
16
|
+
## 🛠 Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install codeannex
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## 📖 Usage
|
|
23
|
+
|
|
24
|
+
### Interactive Mode (Wizard)
|
|
25
|
+
Simply run without arguments to start the step-by-step configuration:
|
|
26
|
+
```bash
|
|
27
|
+
codeannex .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Automation / CI
|
|
31
|
+
Use the `--no-input` flag to disable the wizard in automated environments:
|
|
32
|
+
```bash
|
|
33
|
+
codeannex . --no-input
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Professional Customization Example
|
|
37
|
+
```bash
|
|
38
|
+
python3 -m codeannex . \
|
|
39
|
+
--cover-title "Anexo I" \
|
|
40
|
+
--no-page-numbers \
|
|
41
|
+
--primary-color "#0f4761" \
|
|
42
|
+
--title-font "Play" --title-size 20 \
|
|
43
|
+
--normal-font "Times New Roman" --normal-size 12 \
|
|
44
|
+
--margin-top 2.5 --margin-bottom 2.5 --margin-left 3 --margin-right 3 \
|
|
45
|
+
--code-bg "#f5f5f5" --code-size 9
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## ⚙️ Configuration Options
|
|
49
|
+
|
|
50
|
+
### Document & Labels
|
|
51
|
+
- `--cover-title TITLE` — Title for the cover (default: "ANEXO TÉCNICO").
|
|
52
|
+
- `--cover-subtitle SUB` — Subtitle for the cover.
|
|
53
|
+
- `--summary-title TITLE` — Title for the summary page.
|
|
54
|
+
- `--repo-label LABEL` — Prefix for repository link (default: "Repository: ").
|
|
55
|
+
- `--project-label LABEL` — Prefix for project name in footer.
|
|
56
|
+
- `--file-part-format FMT` — Format for file parts (default: "({current}/{total})").
|
|
57
|
+
- `--no-input` — Disable the interactive wizard.
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
### Colors & Themes
|
|
61
|
+
- `--primary-color HEX` — Main color for headers and accents.
|
|
62
|
+
- `--page-bg-color HEX` — Background color for all pages.
|
|
63
|
+
- `--code-bg HEX` — Background color for code blocks.
|
|
64
|
+
- `--normal-color HEX` / `--title-color HEX` — Text colors.
|
|
65
|
+
|
|
66
|
+
### Fonts & Sizes
|
|
67
|
+
- `--title-font` / `--subtitle-font` / `--normal-font` — System font names.
|
|
68
|
+
- `--mono-font` — Font for code (e.g., "Consolas", "Ubuntu Mono").
|
|
69
|
+
- `--emoji-font` — Font for emojis (e.g., "Noto Emoji").
|
|
70
|
+
- `--code-size N` — Font size for code blocks.
|
|
71
|
+
|
|
72
|
+
### Layout
|
|
73
|
+
- `--margin CM` — General margin for all sides.
|
|
74
|
+
- `--margin-top`, `--margin-bottom`, `--margin-left`, `--margin-right` — Specific margins in cm.
|
|
75
|
+
- `--no-page-numbers` — Disable page numbering.
|
|
76
|
+
|
|
77
|
+
## 🧪 Testing
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
PYTHONPATH=. pytest
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## 📄 License
|
|
84
|
+
|
|
85
|
+
MIT
|
|
@@ -5,7 +5,7 @@ from pathlib import Path
|
|
|
5
5
|
|
|
6
6
|
from .config import IMAGE_EXTENSIONS, BINARY_EXTENSIONS, PDFConfig
|
|
7
7
|
from .file_utils import get_project_files, classify_file, sort_files
|
|
8
|
-
from .fonts import init_sprites, register_best_font, register_emoji_font
|
|
8
|
+
from .fonts import init_sprites, register_best_font, register_emoji_font, auto_register_font
|
|
9
9
|
from .pdf_builder import ModernAnnexPDF
|
|
10
10
|
from reportlab.lib.units import cm
|
|
11
11
|
|
|
@@ -16,8 +16,6 @@ def check_emoji_font_style():
|
|
|
16
16
|
|
|
17
17
|
emoji_font = register_emoji_font()
|
|
18
18
|
if emoji_font:
|
|
19
|
-
# Tentar obter informações sobre a fonte
|
|
20
|
-
# Nota: Esta é uma simplificação - em produção teríamos o caminho armazenado
|
|
21
19
|
print(f"✅ Emoji font registered: {emoji_font}")
|
|
22
20
|
print("ℹ️ To check if using Google-like style, look for 'Noto' in the font path above")
|
|
23
21
|
print("💡 Tip: Install Google Noto fonts for authentic Google emoji style")
|
|
@@ -26,11 +24,50 @@ def check_emoji_font_style():
|
|
|
26
24
|
return emoji_font
|
|
27
25
|
|
|
28
26
|
|
|
27
|
+
def run_interactive_wizard(args):
|
|
28
|
+
"""Interactive wizard to configure the PDF when no arguments are provided."""
|
|
29
|
+
print("\n✨ Welcome to codeannex Interactive Wizard! ✨")
|
|
30
|
+
print("Press Enter to keep the [default] value.\n")
|
|
31
|
+
|
|
32
|
+
questions = [
|
|
33
|
+
("name", "Project Name", args.name or "Current Directory"),
|
|
34
|
+
("cover_title", "Cover Title", args.cover_title),
|
|
35
|
+
("cover_subtitle", "Cover Subtitle", args.cover_subtitle),
|
|
36
|
+
("primary_color", "Primary Color (HEX)", args.primary_color),
|
|
37
|
+
("title_font", "Title Font Name", args.title_font or "Play"),
|
|
38
|
+
("normal_font", "Normal Text Font Name", args.normal_font or "Tinos"),
|
|
39
|
+
("mono_font", "Monospace Font Name", args.mono_font or "NotoSansMono"),
|
|
40
|
+
("margin_top", "Top Margin (cm)", args.margin_top or 2.0),
|
|
41
|
+
("margin_bottom", "Bottom Margin (cm)", args.margin_bottom or 2.0),
|
|
42
|
+
("margin_left", "Left Margin (cm)", args.margin_left or 1.5),
|
|
43
|
+
("margin_right", "Right Margin (cm)", args.margin_right or 1.5),
|
|
44
|
+
("no_page_numbers", "Disable Page Numbers? (y/n)", "n"),
|
|
45
|
+
("start_page", "Starting Page Number", args.start_page or 1),
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
for i, (attr, label, default) in enumerate(questions, 1):
|
|
49
|
+
# Skip start_page question if page numbers were disabled in previous step
|
|
50
|
+
if attr == "start_page" and args.no_page_numbers:
|
|
51
|
+
continue
|
|
52
|
+
|
|
53
|
+
choice = input(f"[{i}/{len(questions)}] {label} [{default}]: ").strip()
|
|
54
|
+
if choice:
|
|
55
|
+
if attr == "no_page_numbers":
|
|
56
|
+
args.no_page_numbers = choice.lower() == 'y'
|
|
57
|
+
elif "margin" in attr or attr == "start_page":
|
|
58
|
+
setattr(args, attr, float(choice) if "." in choice else int(choice))
|
|
59
|
+
else:
|
|
60
|
+
setattr(args, attr, choice)
|
|
61
|
+
print("\n🚀 Configuration complete! Generating PDF...\n")
|
|
62
|
+
|
|
63
|
+
|
|
29
64
|
def main():
|
|
30
65
|
parser = argparse.ArgumentParser(description="Generates a PDF code annex with Smart Index and Images.")
|
|
31
66
|
parser.add_argument("dir", nargs="?", default=".", help="Project directory")
|
|
32
67
|
parser.add_argument("-o", "--output", default=None, help="Output PDF filename")
|
|
33
68
|
parser.add_argument("-n", "--name", default=None, help="Project name (default: directory name)")
|
|
69
|
+
parser.add_argument("--no-input", action="store_true", help="Disable interactive mode")
|
|
70
|
+
parser.add_argument("--cover-title", default="TECHNICAL ANNEX", help="Title for the cover page")
|
|
34
71
|
parser.add_argument("--margin", type=float, default=None, help="General margin in cm for all sides")
|
|
35
72
|
parser.add_argument("--margin-left", type=float, default=None, help="Left margin in cm (default: 1.5cm)")
|
|
36
73
|
parser.add_argument("--margin-right", type=float, default=None, help="Right margin in cm (default: 1.5cm)")
|
|
@@ -39,9 +76,29 @@ def main():
|
|
|
39
76
|
parser.add_argument("--start-page", type=int, default=1, help="Starting page number (default: 1)")
|
|
40
77
|
parser.add_argument("--show-project", action="store_true", help="Show project name in footer")
|
|
41
78
|
parser.add_argument("--repo-url", default=None, help="Repository URL to show on cover")
|
|
79
|
+
parser.add_argument("--repo-label", default="Repository: ", help="Label for repo URL (default: 'Repository: ')")
|
|
80
|
+
parser.add_argument("--project-label", default="Project: ", help="Label for project name in footer (default: 'Project: ')")
|
|
81
|
+
parser.add_argument("--file-part-format", default="({current}/{total})", help="Format for file parts continuation (e.g. '({current}/{total})')")
|
|
82
|
+
parser.add_argument("--summary-title", default="Summary / File Index", help="Title for the summary page")
|
|
83
|
+
parser.add_argument("--cover-subtitle", default="Source Code Documentation", help="Subtitle for the cover page")
|
|
84
|
+
|
|
42
85
|
parser.add_argument("--page-number-size", type=int, default=8, help="Font size for page numbers (default: 8)")
|
|
86
|
+
parser.add_argument("--page-number-format", default="{n}", help="Format for page numbers (e.g. 'Anexo I - {n}')")
|
|
87
|
+
parser.add_argument("--no-page-numbers", action="store_true", help="Disable page numbers")
|
|
88
|
+
parser.add_argument("--page-bg-color", default="#ffffff", help="Background color for pages (default: #ffffff)")
|
|
43
89
|
parser.add_argument("--normal-font", default=None, help="Normal text font (default: Helvetica)")
|
|
90
|
+
parser.add_argument("--normal-size", type=int, default=10, help="Normal text font size (default: 10)")
|
|
91
|
+
parser.add_argument("--normal-color", default="#4c4f69", help="Normal text color (default: #4c4f69)")
|
|
44
92
|
parser.add_argument("--bold-font", default=None, help="Bold text font (default: Helvetica-Bold)")
|
|
93
|
+
parser.add_argument("--title-font", default=None, help="Title font (default: bold-font)")
|
|
94
|
+
parser.add_argument("--title-size", type=int, default=28, help="Title font size (default: 28)")
|
|
95
|
+
parser.add_argument("--title-color", default="#1e1e2e", help="Title color (default: #1e1e2e)")
|
|
96
|
+
parser.add_argument("--subtitle-font", default=None, help="Subtitle font (default: normal-font)")
|
|
97
|
+
parser.add_argument("--subtitle-size", type=int, default=18, help="Subtitle font size (default: 18)")
|
|
98
|
+
parser.add_argument("--subtitle-color", default=None, help="Subtitle color (default: title-color)")
|
|
99
|
+
parser.add_argument("--primary-color", default="#1e66f5", help="Primary color for accents and headers (default: #1e66f5)")
|
|
100
|
+
parser.add_argument("--code-size", type=int, default=10, help="Font size for code (default: 10)")
|
|
101
|
+
parser.add_argument("--code-bg", default="#1e1e2e", help="Background color for code blocks (default: #1e1e2e)")
|
|
45
102
|
parser.add_argument("--mono-font", default=None, help="Monospace font for code (default: auto-detect)")
|
|
46
103
|
parser.add_argument("--emoji-font", default=None, help="Font for emojis (default: auto-detect)")
|
|
47
104
|
parser.add_argument("--emoji-description", action="store_true", help="Print [description] instead of emoji glyphs")
|
|
@@ -49,6 +106,22 @@ def main():
|
|
|
49
106
|
|
|
50
107
|
args, unknown = parser.parse_known_args()
|
|
51
108
|
|
|
109
|
+
# Check if we should run interactive mode:
|
|
110
|
+
# No optional arguments provided AND --no-input is False
|
|
111
|
+
import sys
|
|
112
|
+
is_interactive = len(sys.argv) <= 2 and not args.no_input
|
|
113
|
+
# If the second arg is actually a directory but no other flags were passed, we can still go interactive
|
|
114
|
+
if is_interactive:
|
|
115
|
+
run_interactive_wizard(args)
|
|
116
|
+
|
|
117
|
+
# Pre-register fonts specified in arguments dynamically from system
|
|
118
|
+
if args.normal_font: args.normal_font = auto_register_font(args.normal_font, required=True)
|
|
119
|
+
if args.bold_font: args.bold_font = auto_register_font(args.bold_font, required=True)
|
|
120
|
+
if args.title_font: args.title_font = auto_register_font(args.title_font, required=True)
|
|
121
|
+
if args.subtitle_font: args.subtitle_font = auto_register_font(args.subtitle_font, required=True)
|
|
122
|
+
if args.mono_font: args.mono_font = auto_register_font(args.mono_font, required=True)
|
|
123
|
+
if args.emoji_font: args.emoji_font = auto_register_font(args.emoji_font, required=True)
|
|
124
|
+
|
|
52
125
|
# Handle unknown arguments
|
|
53
126
|
if unknown:
|
|
54
127
|
for u in unknown:
|
|
@@ -95,6 +168,27 @@ def main():
|
|
|
95
168
|
emoji_description=args.emoji_description,
|
|
96
169
|
repo_url=args.repo_url,
|
|
97
170
|
page_number_size=args.page_number_size,
|
|
171
|
+
title_font=args.title_font,
|
|
172
|
+
title_size=args.title_size,
|
|
173
|
+
title_color=args.title_color,
|
|
174
|
+
subtitle_font=args.subtitle_font,
|
|
175
|
+
subtitle_size=args.subtitle_size,
|
|
176
|
+
subtitle_color=args.subtitle_color,
|
|
177
|
+
normal_text_size=args.normal_size,
|
|
178
|
+
normal_text_color=args.normal_color,
|
|
179
|
+
page_number_format=args.page_number_format,
|
|
180
|
+
show_page_numbers=not args.no_page_numbers,
|
|
181
|
+
cover_title=args.cover_title,
|
|
182
|
+
primary_color=args.primary_color,
|
|
183
|
+
# New dynamic fields
|
|
184
|
+
cover_subtitle=args.cover_subtitle,
|
|
185
|
+
summary_title=args.summary_title,
|
|
186
|
+
repo_label=args.repo_label,
|
|
187
|
+
project_label=args.project_label,
|
|
188
|
+
file_part_format=args.file_part_format,
|
|
189
|
+
code_font_size=args.code_size,
|
|
190
|
+
code_bg_color=args.code_bg,
|
|
191
|
+
page_bg_color=args.page_bg_color,
|
|
98
192
|
)
|
|
99
193
|
|
|
100
194
|
print(f"🔍 Analyzing directory: {root}")
|
|
@@ -113,7 +207,6 @@ def main():
|
|
|
113
207
|
|
|
114
208
|
ext = fp.suffix.lower()
|
|
115
209
|
|
|
116
|
-
# Check known extensions first (without reading file)
|
|
117
210
|
if ext == ".svg":
|
|
118
211
|
included += [(fp, "image"), (fp, "text")]
|
|
119
212
|
continue
|
|
@@ -124,7 +217,6 @@ def main():
|
|
|
124
217
|
ignored_binaries.append(fp)
|
|
125
218
|
continue
|
|
126
219
|
|
|
127
|
-
# Classify file with a single read
|
|
128
220
|
file_type = classify_file(fp)
|
|
129
221
|
if file_type == "text":
|
|
130
222
|
included.append((fp, "text"))
|
|
@@ -56,6 +56,33 @@ class PDFConfig:
|
|
|
56
56
|
repo_url: str | None = None # URL of the repository for the cover page
|
|
57
57
|
page_number_size: int = 8 # Font size for page numbers
|
|
58
58
|
|
|
59
|
+
# Customization fields
|
|
60
|
+
title_font: str | None = None
|
|
61
|
+
title_size: int = 28
|
|
62
|
+
title_color: str = "#1e1e2e"
|
|
63
|
+
subtitle_font: str | None = None
|
|
64
|
+
subtitle_size: int = 18
|
|
65
|
+
subtitle_color: str | None = None # Defaults to title_color
|
|
66
|
+
normal_text_size: int = 10
|
|
67
|
+
normal_text_color: str = "#4c4f69" # Default COLOR_TEXT_MAIN
|
|
68
|
+
page_number_format: str = "{n}" # e.g. "Anexo I - {n}" or just "{n}"
|
|
69
|
+
show_page_numbers: bool = True
|
|
70
|
+
cover_title: str = "TECHNICAL ANNEX"
|
|
71
|
+
cover_subtitle: str = "Source Code Documentation"
|
|
72
|
+
primary_color: str = "#1e66f5" # Professional dark blue
|
|
73
|
+
|
|
74
|
+
# Code style
|
|
75
|
+
code_font_size: int = CODE_FONT_SIZE
|
|
76
|
+
code_bg_color: str = "#1e1e2e" # Default COLOR_CODE_BG
|
|
77
|
+
page_bg_color: str = "#ffffff" # Default COLOR_PAGE_BG
|
|
78
|
+
|
|
79
|
+
# Labels (Internationalization)
|
|
80
|
+
summary_title: str = "Summary / File Index"
|
|
81
|
+
repo_label: str = "Repository: "
|
|
82
|
+
project_label: str = "Project: "
|
|
83
|
+
file_part_format: str = "({current}/{total})" # e.g. "(1/2)"
|
|
84
|
+
cover_subtitle: str = "Source Code Documentation"
|
|
85
|
+
|
|
59
86
|
def get_code_x(self) -> float:
|
|
60
87
|
"""Calculates the initial X position of the code."""
|
|
61
88
|
return self.margin_left + GUTTER_W
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import io
|
|
3
|
+
import logging
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from PIL import Image as PilImage, ImageDraw as PilImageDraw, ImageFont as PilImageFont
|
|
6
|
+
from reportlab.lib.utils import ImageReader
|
|
7
|
+
from reportlab.pdfbase import pdfmetrics
|
|
8
|
+
from reportlab.pdfbase.ttfonts import TTFont
|
|
9
|
+
|
|
10
|
+
from .config import CODE_FONT_SIZE, COLOR_GUTTER_FG
|
|
11
|
+
|
|
12
|
+
logging.getLogger("svglib").setLevel(logging.ERROR)
|
|
13
|
+
|
|
14
|
+
# ── Discovery ────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
def get_system_font_paths():
|
|
17
|
+
"""Returns a list of standard system font directories."""
|
|
18
|
+
home = os.path.expanduser("~")
|
|
19
|
+
paths = [
|
|
20
|
+
os.path.join(home, ".local/share/fonts"),
|
|
21
|
+
os.path.join(home, ".fonts"),
|
|
22
|
+
"/usr/share/fonts",
|
|
23
|
+
"/usr/local/share/fonts",
|
|
24
|
+
"/System/Library/Fonts",
|
|
25
|
+
"/Library/Fonts",
|
|
26
|
+
"C:\\Windows\\Fonts",
|
|
27
|
+
]
|
|
28
|
+
return [p for p in paths if os.path.exists(p)]
|
|
29
|
+
|
|
30
|
+
def find_font_file(font_name: str) -> str | None:
|
|
31
|
+
"""Dynamically searches for a font file by name in system directories."""
|
|
32
|
+
search_dirs = get_system_font_paths()
|
|
33
|
+
|
|
34
|
+
# Normalize name for searching: "NotoEmoji" -> "notoemoji"
|
|
35
|
+
clean_name = font_name.lower().replace(" ", "").replace("-", "")
|
|
36
|
+
|
|
37
|
+
extensions = [".ttf", ".otf", ".ttc"]
|
|
38
|
+
|
|
39
|
+
for base_dir in search_dirs:
|
|
40
|
+
for root, _, files in os.walk(base_dir):
|
|
41
|
+
for f in files:
|
|
42
|
+
f_lower = f.lower()
|
|
43
|
+
# Check if it's a font file
|
|
44
|
+
if any(f_lower.endswith(ext) for ext in extensions):
|
|
45
|
+
# Check if our clean_name is in the filename
|
|
46
|
+
# e.g. "notoemoji" in "notocoloremoji.ttf"
|
|
47
|
+
f_clean = f_lower.replace(" ", "").replace("-", "").replace("_", "")
|
|
48
|
+
if clean_name in f_clean:
|
|
49
|
+
return os.path.join(root, f)
|
|
50
|
+
return None
|
|
51
|
+
|
|
52
|
+
def auto_register_font(font_name: str, required: bool = False) -> str:
|
|
53
|
+
"""Attempts to register a font. Errors (🛑) stop execution, Warnings (⚠️) just alert."""
|
|
54
|
+
if not font_name:
|
|
55
|
+
return font_name
|
|
56
|
+
|
|
57
|
+
# Standard ReportLab fonts
|
|
58
|
+
if font_name in ["Helvetica", "Helvetica-Bold", "Times-Roman", "Times-Bold", "Courier"]:
|
|
59
|
+
return font_name
|
|
60
|
+
|
|
61
|
+
# Check if already registered
|
|
62
|
+
try:
|
|
63
|
+
pdfmetrics.getFont(font_name)
|
|
64
|
+
return font_name
|
|
65
|
+
except:
|
|
66
|
+
pass
|
|
67
|
+
|
|
68
|
+
# Try to find and register
|
|
69
|
+
path = find_font_file(font_name)
|
|
70
|
+
if path:
|
|
71
|
+
try:
|
|
72
|
+
print(f"✅ Found font '{font_name}' at: {path}")
|
|
73
|
+
pdfmetrics.registerFont(TTFont(font_name, path))
|
|
74
|
+
return font_name
|
|
75
|
+
except Exception as e:
|
|
76
|
+
if not required:
|
|
77
|
+
print(f"⚠️ Warning: Could not register font '{font_name}': {e}")
|
|
78
|
+
return font_name
|
|
79
|
+
# If required, it will fall through to the Error block below
|
|
80
|
+
|
|
81
|
+
# If font is not found or registration failed
|
|
82
|
+
if required:
|
|
83
|
+
msg = (
|
|
84
|
+
f"\n🛑 ERROR: Font '{font_name}' not found or invalid.\n"
|
|
85
|
+
f" Search paths: {', '.join(get_system_font_paths())}\n\n"
|
|
86
|
+
f" Fixes:\n"
|
|
87
|
+
f" - Install the font on your OS (Windows/Linux/macOS).\n"
|
|
88
|
+
f" - Check for typos in your command.\n"
|
|
89
|
+
f" - Use a built-in font: Helvetica, Times-Roman, Courier.\n"
|
|
90
|
+
)
|
|
91
|
+
import sys
|
|
92
|
+
print(msg, file=sys.stderr)
|
|
93
|
+
sys.exit(1)
|
|
94
|
+
else:
|
|
95
|
+
print(f"⚠️ WARNING: Font '{font_name}' not found. Emojis or text might not render correctly.")
|
|
96
|
+
|
|
97
|
+
return font_name
|
|
98
|
+
|
|
99
|
+
# ── Original Support Functions ───────────────────
|
|
100
|
+
|
|
101
|
+
TTF_SEARCH_PATHS = [
|
|
102
|
+
"/usr/share/fonts/truetype/noto/NotoSansMono-Regular.ttf",
|
|
103
|
+
"/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf",
|
|
104
|
+
"C:\\Windows\\Fonts\\consola.ttf",
|
|
105
|
+
]
|
|
106
|
+
|
|
107
|
+
EMOJI_SEARCH_PATHS = [
|
|
108
|
+
"/usr/share/fonts/truetype/noto/NotoEmoji-Regular.ttf",
|
|
109
|
+
"/usr/share/fonts/truetype/ancient-scripts/Symbola_hint.ttf",
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
def _register_font(name: str, paths: list, fallback):
|
|
113
|
+
for p in paths:
|
|
114
|
+
if os.path.exists(p):
|
|
115
|
+
try:
|
|
116
|
+
pdfmetrics.registerFont(TTFont(name, p))
|
|
117
|
+
return name, p
|
|
118
|
+
except Exception:
|
|
119
|
+
continue
|
|
120
|
+
return fallback, None
|
|
121
|
+
|
|
122
|
+
def register_best_font():
|
|
123
|
+
name, path = _register_font("CustomMono", TTF_SEARCH_PATHS, "Courier")
|
|
124
|
+
return name, name != "Courier", path
|
|
125
|
+
|
|
126
|
+
def register_emoji_font(error_on_missing=False):
|
|
127
|
+
name, path = _register_font("CustomEmoji", EMOJI_SEARCH_PATHS, None)
|
|
128
|
+
if name is None:
|
|
129
|
+
# Tenta busca dinâmica como última opção
|
|
130
|
+
dynamic_path = find_font_file("NotoEmoji") or find_font_file("Symbola")
|
|
131
|
+
if dynamic_path:
|
|
132
|
+
name, _ = _register_font("CustomEmoji", [dynamic_path], None)
|
|
133
|
+
|
|
134
|
+
if name is None and error_on_missing:
|
|
135
|
+
msg = (
|
|
136
|
+
"❌ Error: No emoji font found on your system.\n"
|
|
137
|
+
" Emojis cannot be rendered correctly without a dedicated font.\n\n"
|
|
138
|
+
" Solutions:\n"
|
|
139
|
+
" 1. Install a font like 'Google Noto Emoji' or 'DejaVu Sans'.\n"
|
|
140
|
+
" - Ubuntu/Debian: sudo apt install fonts-noto-color-emoji\n"
|
|
141
|
+
" - Fedora: sudo dnf install google-noto-emoji-color-fonts\n"
|
|
142
|
+
" - Arch: sudo pacman -S noto-fonts-emoji\n"
|
|
143
|
+
" 2. Use the --emoji-description flag to print [emoji descriptions] instead.\n"
|
|
144
|
+
" 3. Manually specify a font path using --emoji-font \"/path/to/font.ttf\""
|
|
145
|
+
)
|
|
146
|
+
import sys
|
|
147
|
+
print(msg, file=sys.stderr)
|
|
148
|
+
sys.exit(1)
|
|
149
|
+
return name
|
|
150
|
+
|
|
151
|
+
def get_emoji_font_style(font_path: str | None) -> str | None:
|
|
152
|
+
"""Detects the emoji style based on the font path."""
|
|
153
|
+
if not font_path:
|
|
154
|
+
return None
|
|
155
|
+
font_path_lower = font_path.lower()
|
|
156
|
+
if "noto" in font_path_lower:
|
|
157
|
+
if "color" in font_path_lower: return "Google Noto Color"
|
|
158
|
+
elif "emoji" in font_path_lower: return "Google Noto Emoji"
|
|
159
|
+
else: return "Google Noto"
|
|
160
|
+
if "apple" in font_path_lower: return "Apple"
|
|
161
|
+
if "segui" in font_path_lower or "windows" in font_path_lower: return "Microsoft/Windows"
|
|
162
|
+
if "symbola" in font_path_lower: return "Symbola (Unicode)"
|
|
163
|
+
if "dejavu" in font_path_lower: return "DejaVu"
|
|
164
|
+
if "ubuntu" in font_path_lower: return "Ubuntu"
|
|
165
|
+
return "Unknown"
|
|
166
|
+
|
|
167
|
+
def is_google_like_emoji_font(font_path: str | None) -> bool:
|
|
168
|
+
"""Checks if the emoji font is Google-like (Noto-based)."""
|
|
169
|
+
if not font_path: return False
|
|
170
|
+
return "noto" in font_path.lower()
|
|
171
|
+
|
|
172
|
+
def get_current_emoji_font_info() -> dict:
|
|
173
|
+
"""Returns information about the currently registered emoji font."""
|
|
174
|
+
if "CustomEmoji" in pdfmetrics._fonts:
|
|
175
|
+
return {
|
|
176
|
+
"name": "CustomEmoji",
|
|
177
|
+
"is_registered": True,
|
|
178
|
+
"is_google_like": False,
|
|
179
|
+
"style": "Unknown"
|
|
180
|
+
}
|
|
181
|
+
return {
|
|
182
|
+
"name": None,
|
|
183
|
+
"is_registered": False,
|
|
184
|
+
"is_google_like": False,
|
|
185
|
+
"style": None
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
# ── Character & Sprite Support ───────────────────
|
|
189
|
+
|
|
190
|
+
_FONT_CACHE: dict = {}
|
|
191
|
+
|
|
192
|
+
def is_char_supported(char: str, font_name: str) -> bool:
|
|
193
|
+
"""Checks if the font has a glyph for the character. True for standard ASCII in built-in fonts."""
|
|
194
|
+
if font_name not in _FONT_CACHE:
|
|
195
|
+
try:
|
|
196
|
+
_FONT_CACHE[font_name] = pdfmetrics.getFont(font_name)
|
|
197
|
+
except:
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
try:
|
|
201
|
+
f = _FONT_CACHE[font_name]
|
|
202
|
+
|
|
203
|
+
# ReportLab standard fonts (Type1) only support latin-1
|
|
204
|
+
if font_name in ["Helvetica", "Helvetica-Bold", "Times-Roman", "Times-Bold", "Courier"]:
|
|
205
|
+
return ord(char) < 256
|
|
206
|
+
|
|
207
|
+
# For TTFonts, check the actual glyph map
|
|
208
|
+
if hasattr(f, "face") and hasattr(f.face, "charToGlyph"):
|
|
209
|
+
return ord(char) in f.face.charToGlyph
|
|
210
|
+
|
|
211
|
+
return ord(char) < 256
|
|
212
|
+
except:
|
|
213
|
+
return False
|
|
214
|
+
|
|
215
|
+
def is_emoji(char: str) -> bool:
|
|
216
|
+
"""
|
|
217
|
+
Returns True if the character is likely an emoji/symbol.
|
|
218
|
+
Excludes box-drawing characters (U+2500 to U+257F).
|
|
219
|
+
"""
|
|
220
|
+
if not char: return False
|
|
221
|
+
cp = ord(char[0])
|
|
222
|
+
# Box Drawing range: 2500–257F
|
|
223
|
+
if 0x2500 <= cp <= 0x257F:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
import unicodedata
|
|
227
|
+
category = unicodedata.category(char[0])
|
|
228
|
+
return category in ['So', 'Sk', 'Cn']
|
|
229
|
+
|
|
230
|
+
DIGIT_SPRITES: dict | None = None
|
|
231
|
+
|
|
232
|
+
def get_digit_sprites() -> dict:
|
|
233
|
+
if DIGIT_SPRITES is None: raise RuntimeError("init_sprites() not called.")
|
|
234
|
+
return DIGIT_SPRITES
|
|
235
|
+
|
|
236
|
+
def init_sprites(is_ttf: bool, ttf_path: str | None):
|
|
237
|
+
global DIGIT_SPRITES
|
|
238
|
+
if DIGIT_SPRITES is not None: return
|
|
239
|
+
DIGIT_SPRITES = {}
|
|
240
|
+
color_hex = "#bac2de"
|
|
241
|
+
try:
|
|
242
|
+
import cairosvg
|
|
243
|
+
cairo_available = True
|
|
244
|
+
except ImportError:
|
|
245
|
+
cairo_available = False
|
|
246
|
+
|
|
247
|
+
for d in "0123456789":
|
|
248
|
+
if cairo_available:
|
|
249
|
+
svg_str = f'<svg xmlns="http://www.w3.org/2000/svg" width="50" height="50"><text x="25" y="40" font-family="monospace" font-size="40" fill="{color_hex}" text-anchor="middle">{d}</text></svg>'
|
|
250
|
+
png_data = cairosvg.svg2png(bytestring=svg_str.encode(), scale=4.0)
|
|
251
|
+
img = PilImage.open(io.BytesIO(png_data))
|
|
252
|
+
else:
|
|
253
|
+
box_size = 200
|
|
254
|
+
font = None
|
|
255
|
+
if is_ttf and ttf_path:
|
|
256
|
+
try: font = PilImageFont.truetype(ttf_path, box_size)
|
|
257
|
+
except: pass
|
|
258
|
+
font = font or PilImageFont.load_default()
|
|
259
|
+
img_large = PilImage.new("RGBA", (box_size, box_size), (0, 0, 0, 0))
|
|
260
|
+
draw = PilImageDraw.Draw(img_large)
|
|
261
|
+
draw.text((box_size // 2, int(box_size * 0.8)), d, font=font, fill=(186, 194, 222, 255), anchor="ms")
|
|
262
|
+
img = img_large.resize((50, 50), PilImage.Resampling.LANCZOS)
|
|
263
|
+
buf = io.BytesIO()
|
|
264
|
+
img.save(buf, format="PNG")
|
|
265
|
+
buf.seek(0)
|
|
266
|
+
DIGIT_SPRITES[d] = ImageReader(buf)
|
|
267
|
+
|
|
268
|
+
def register_font_file(name: str, path: str):
|
|
269
|
+
"""Fallback for manual registration if needed."""
|
|
270
|
+
if os.path.exists(path):
|
|
271
|
+
try:
|
|
272
|
+
pdfmetrics.registerFont(TTFont(name, path))
|
|
273
|
+
return name
|
|
274
|
+
except: pass
|
|
275
|
+
return None
|