commit 79ca7ab62a7f2bda84ee305f3b4eea35f347c11b Author: Paul Ngo Date: Fri Jun 27 17:10:20 2025 -0700 first commit diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..b33af45 Binary files /dev/null and b/.DS_Store differ diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2288dd --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +*.pdf +*.tsv +*.csv diff --git a/DejaVuSansCondensed-Bold.ttf b/DejaVuSansCondensed-Bold.ttf new file mode 100644 index 0000000..91ccf5c Binary files /dev/null and b/DejaVuSansCondensed-Bold.ttf differ diff --git a/DejaVuSansCondensed-BoldOblique.ttf b/DejaVuSansCondensed-BoldOblique.ttf new file mode 100644 index 0000000..9f82d61 Binary files /dev/null and b/DejaVuSansCondensed-BoldOblique.ttf differ diff --git a/DejaVuSansCondensed-Oblique.ttf b/DejaVuSansCondensed-Oblique.ttf new file mode 100644 index 0000000..bb4872c Binary files /dev/null and b/DejaVuSansCondensed-Oblique.ttf differ diff --git a/DejaVuSansCondensed.ttf b/DejaVuSansCondensed.ttf new file mode 100644 index 0000000..2b79e64 Binary files /dev/null and b/DejaVuSansCondensed.ttf differ diff --git a/NotoColorEmoji-Regular.ttf b/NotoColorEmoji-Regular.ttf new file mode 100644 index 0000000..559002b Binary files /dev/null and b/NotoColorEmoji-Regular.ttf differ diff --git a/README.md b/README.md new file mode 100644 index 0000000..7caa856 --- /dev/null +++ b/README.md @@ -0,0 +1,50 @@ +# PDF generation End of Term Surveys + +This is how you can generate PDFs for End of Term Surveys. Some examples are provided in examples/ + +## Usage + +### If you do not want emoji support: + +``` +pip install fpdf2 +``` +Use `generate_pdf.py` to convert a tsv to a pdf. + +### If you do want emoji support: + +``` +pip install playwright jinja2 +playwright install +``` +Use `generate_pdf_emoji_support.py` to convert a tsv to a pdf. + +## Usage Examples + +``` +python generate_pdf.py -i "F2024 End of Term Survey Responses.tsv" -o "F24 End of Term Survey (Sorted by Locality).pdf" +``` + +``` +python generate_pdf_emoji_support.py -i "F2024 End of Term Survey Responses.tsv" -o "F24 End of Term Survey (Sorted by Locality).pdf" +``` + +``` +python generate_pdf.py -i "F2024 Fourth Term Survey Responses.tsv" -o "F24 Fourth Term Survey (Sorted by Locality).pdf" +``` + +``` +python generate_pdf_emoji_support.py -i "F2024 Fourth Term Survey Responses.tsv" -o "F24 Fourth Term Survey (Sorted by Locality).pdf" +``` + +## Actual Usage Example + +1. Find the original survey links that were shared with you from the training (to every trainee) +2. Go to the google sheet of responses (shared with only you) and add the correct headings based on the questions + - there are probably columns called "Column #". You should rename these according to the original survey + - they should be renamed to things like: "Please evaluate your progress in "Truth (e.g. study times, Bible reading, etc.)" for this past term (1-5, 5 indicating the most progress):" + - for comments about these, name them "Comments 1", "Comments 2", etc. + - at the end of Fourth Term Survey, it probably should be called "Further Comments" +3. Required columns: 'Timestamp', 'Email Address', 'Name (last, first):', 'Sending locality:'. WARNING: In previous terms, the column named "Name (last, first):" was called "Name (last, first): " with a space. You need to remove this space to make the output work. + + diff --git a/examples/.DS_Store b/examples/.DS_Store new file mode 100644 index 0000000..8426960 Binary files /dev/null and b/examples/.DS_Store differ diff --git a/generate_pdf.py b/generate_pdf.py new file mode 100644 index 0000000..539ac57 --- /dev/null +++ b/generate_pdf.py @@ -0,0 +1,99 @@ +import csv +import re +import argparse +from fpdf import FPDF + +def is_number(s): + try: + float(s) + return True + except ValueError: + return False + +def ensure_colon(heading: str) -> str: + heading = heading.strip() + # If it already ends with a colon, don't add another one + if not heading.endswith(":"): + heading += ":" + return heading + +def main(): + parser = argparse.ArgumentParser(description="Generate a PDF report from a TSV input.") + parser.add_argument("-i", "--input_tsv", required=True, help="Path to the input TSV file.") + parser.add_argument("-o", "--output_pdf", required=True, help="Path to the output PDF file.") + args = parser.parse_args() + + input_tsv = args.input_tsv + output_pdf = args.output_pdf + + # Read the TSV data + with open(input_tsv, 'r', encoding='utf-8-sig') as f: + reader = csv.DictReader(f, delimiter='\t') + rows = sorted(rows, key=lambda r: r.get("Sending locality:", "").strip()) + + pdf = FPDF(format='Letter', unit='pt') + pdf.set_auto_page_break(auto=True, margin=50) + + # Add all required font styles + pdf.add_font("DejaVu", "", "DejaVuSansCondensed.ttf") + pdf.add_font("DejaVu", "B", "DejaVuSansCondensed-Bold.ttf") + pdf.add_font("DejaVu", "I", "DejaVuSansCondensed-Oblique.ttf") + pdf.add_font("DejaVu", "BI", "DejaVuSansCondensed-BoldOblique.ttf") + + pdf.set_font("DejaVu", size=12) + + skip_cols = {'Timestamp', 'Email Address', 'Name (last, first):', 'Sending locality:'} + + for row in rows: + pdf.add_page() + + name = row.get('Name (last, first):', '').strip() + sending_locality = row.get('Sending locality:', '').strip() + + # Header section + pdf.set_font("DejaVu", '', 14) + pdf.cell(0, 20, name, new_x="LMARGIN", new_y="NEXT") + + pdf.set_font("DejaVu", '', 12) + pdf.cell(0, 20, f"Sending Locality: {sending_locality}", new_x="LMARGIN", new_y="NEXT") + + + for col in row.keys(): + if col in skip_cols: + continue + + col_title = col.strip() + value = row[col].strip() + if not value: + continue + + comment_match = re.match(r'^Comments\s\d+$', col_title, re.IGNORECASE) + if comment_match: + # Comments + pdf.set_font("DejaVu", 'I', 12) + pdf.write(16, "Comments: ") + pdf.set_font("DejaVu", '', 12) + pdf.multi_cell(0, 16, value, new_x="LMARGIN", new_y="NEXT") + pdf.ln(10) + continue + + # Non-comment columns + pdf.set_font("DejaVu", 'B', 12) + if is_number(value): + # Numeric value + col_title = ensure_colon(col_title) + pdf.multi_cell(0, 16, f"{col_title} {value}", new_x="LMARGIN", new_y="NEXT") + pdf.ln(10) + else: + # Non-numeric value + col_title = ensure_colon(col_title) + pdf.multi_cell(0, 16, col_title, new_x="LMARGIN", new_y="NEXT") + pdf.set_font("DejaVu", '', 12) + pdf.multi_cell(0, 16, value, new_x="LMARGIN", new_y="NEXT") + pdf.ln(10) + pdf.set_font("DejaVu", 'B', 12) + + pdf.output(output_pdf) + +if __name__ == "__main__": + main() diff --git a/generate_pdf_emoji_support.py b/generate_pdf_emoji_support.py new file mode 100644 index 0000000..78457e9 --- /dev/null +++ b/generate_pdf_emoji_support.py @@ -0,0 +1,236 @@ +import argparse +import csv +import re +import tempfile +import jinja2 +import os +from playwright.sync_api import sync_playwright + +def is_number(s): + """Returns True if s can be interpreted as a float.""" + try: + float(s) + return True + except ValueError: + return False + +def ensure_colon(heading: str) -> str: + """Append ':' only if heading does not already end with one.""" + heading = heading.strip() + if not heading.endswith(":"): + heading += ":" + return heading + +def main(): + parser = argparse.ArgumentParser(description="Generate a PDF report from a TSV input using Playwright.") + parser.add_argument("-i", "--input_tsv", required=True, help="Path to the input TSV file.") + parser.add_argument("-o", "--output_pdf", required=True, help="Path to the output PDF file.") + parser.add_argument("--browser", choices=["chromium", "firefox", "webkit"], default="chromium", + help="Which browser engine to use (default: chromium).") + args = parser.parse_args() + + input_tsv = args.input_tsv + output_pdf = args.output_pdf + + # 1. Read the TSV data + with open(input_tsv, 'r', encoding='utf-8-sig') as f: + reader = csv.DictReader(f, delimiter='\t') + rows = list(reader) + + # 2. Sort rows by sending locality + rows = sorted(rows, key=lambda r: r.get("Sending locality:", "").strip()) + + # Known columns to skip + skip_cols = { + 'Timestamp', + 'Email Address', + 'Name (last, first):', + 'Sending locality:' + } + + # 3. Process each row into a data structure for Jinja2 + processed_rows = [] + for row in rows: + name = row.get('Name (last, first):', '').strip() + locality = row.get('Sending locality:', '').strip() + + q_and_a = [] + for col in row.keys(): + if col in skip_cols: + continue + + col_title = col.strip() + value = row[col].strip() + if not value: + continue + + # Check if "Comments" column + if re.match(r'^Comments\s\d+$', col_title, re.IGNORECASE): + # e.g. "Comments 1" => italic "Comments:" + q_and_a.append({ + "type": "comments", + "label": "Comments", + "value": value + }) + else: + # Normal question + if is_number(value): + # Numeric => "Truth: 4" on one line + q_and_a.append({ + "type": "inline", + "label": ensure_colon(col_title), + "value": value + }) + else: + # Non-numeric => label on one line, answer on next + q_and_a.append({ + "type": "block", + "label": col_title, + "value": value + }) + + processed_rows.append({ + "name": name, + "locality": locality, + "entries": q_and_a + }) + + # 4. Create an HTML template (Jinja2) + # We'll use page-break-after so each row is on a new PDF page. + html_template_str = r""" + + + + + """ + rf""" + {os.path.basename(output_pdf)} + """ + r""" + + + + +{% for row in rows %} +
+ +

{{ row.name }}

+ + +

Sending Locality: {{ row.locality }}

+ + {% for entry in row.entries %} + {% if entry.type == "comments" %} + +

Comments:

+

{{ entry.value }}

+ + {% elif entry.type == "inline" %} + +

+ {{ entry.label }} {{ entry.value }} +

+ + {% else %} + +

{{ entry.label }}

+

{{ entry.value }}

+ {% endif %} + {% endfor %} +
+{% endfor %} + + + +""" + + # 5. Render the template with Jinja2 + template = jinja2.Template(html_template_str) + rendered_html = template.render(rows=processed_rows) + + # 6. Convert the HTML to PDF using Playwright + # We'll create a temporary HTML file, open it in a headless browser, and save as PDF. + with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as tmp_file: + tmp_html_path = tmp_file.name + tmp_file.write(rendered_html.encode("utf-8")) + tmp_file.flush() + + with sync_playwright() as p: + # launch the selected browser (chromium/firefox/webkit) + browser = p.__getattribute__(args.browser).launch() + context = browser.new_context() + page = context.new_page() + + # Load the local HTML file + page.goto(f"file://{tmp_html_path}") + + # PDF Options: letter format, etc. + # For more options, see: https://playwright.dev/python/docs/api/class-page#pagepdfoptions + page.pdf( + path=output_pdf, + format="letter", + margin={"top": "0.75in", "right": "0.75in", "bottom": "0.75in", "left": "0.75in"} + ) + + browser.close() + +if __name__ == "__main__": + main()