acmc/PDFuzz
0
1#!/usr/bin/env python32"""Simple Gradio demo for the PDF attacker tools3 4Allows entering text, choosing attack type, and downloading the generated PDF.5"""6import os7import time8from typing import Tuple9 10import PyPDF211import gradio as gr12 13from pdf_attacker import PDFAttacker14 15 16def _resolve_font_path(choice: str, uploaded_file) -> str:17 """Return a font path given a dropdown choice or uploaded file.18 19 If choice is 'auto' return None so PDFAttacker will pick a reasonable default.20 """21 if choice == 'auto' or not choice:22 return None23 24 # known presets mapped to candidate system paths (try first existing)25 presets = {26 'DejaVu Serif': [27 '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf',28 ],29 'Liberation Serif': [30 '/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf',31 ],32 'FreeSerif': [33 '/usr/share/fonts/truetype/freefont/FreeSerif.ttf',34 ],35 'DejaVu Sans': [36 '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',37 ],38 'Arial': [39 '/usr/share/fonts/truetype/msttcorefonts/Arial.ttf',40 '/usr/share/fonts/truetype/msttcorefonts/arial.ttf',41 '/usr/share/fonts/truetype/arial/arial.ttf',42 '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',43 ],44 'Helvetica': [45 '/usr/share/fonts/truetype/urw-base35/Helvetica.ttf',46 '/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf',47 '/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf',48 ],49 'Times New Roman': [50 '/usr/share/fonts/truetype/msttcorefonts/Times_New_Roman.ttf',51 '/usr/share/fonts/truetype/msttcorefonts/Times_New_Roman.ttf',52 '/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf',53 ],54 'Roboto': [55 '/usr/share/fonts/truetype/roboto/Roboto-Regular.ttf',56 '/usr/share/fonts/truetype/roboto/Roboto-Regular.ttf',57 ],58 'Courier': [59 '/usr/share/fonts/truetype/liberation/LiberationMono-Regular.ttf',60 '/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf',61 ],62 'Times': [63 '/usr/share/fonts/truetype/liberation/LiberationSerif-Regular.ttf',64 ],65 }66 67 if choice in presets:68 for p in presets[choice]:69 if os.path.exists(p):70 return p71 return None72 73 # custom uploaded file: gradio returns a local path-like string or dict74 if choice == 'Custom' and uploaded_file:75 # uploaded_file may be a dict-like object or a str path76 if isinstance(uploaded_file, dict) and 'name' in uploaded_file:77 return uploaded_file['name']78 return uploaded_file79 80 return None81 82# Theme customization per request83theme = gr.themes.Soft(84 primary_hue="fuchsia",85 secondary_hue="cyan",86 neutral_hue="gray",87 radius_size="none",88 font=[89 gr.themes.GoogleFont("IBM Plex Sans"),90 "ui-sans-serif",91 "system-ui",92 "sans-serif",93 ],94 font_mono=[95 gr.themes.GoogleFont("IBM Plex Mono"),96 "ui-monospace",97 "Consolas",98 "monospace",99 ],100)101 102 103def _ensure_tmp_dir() -> str:104 """Ensure tmp dir exists and return its path"""105 path = os.path.join(os.getcwd(), "tmp")106 os.makedirs(path, exist_ok=True)107 return path108 109 110def _extract_text_from_pdf(pdf_path: str) -> str:111 """Extract text from a PDF file for preview"""112 try:113 with open(pdf_path, 'rb') as f:114 reader = PyPDF2.PdfReader(f)115 text = ""116 for page in reader.pages:117 page_text = page.extract_text()118 if page_text:119 text += page_text120 return text.strip()121 except Exception as e:122 return f"Error extracting text: {e}"123 124 125def generate_pdf(126 text: str,127 mode: str,128 attack_factor: float = 0.7,129 target_text: str = "",130 font_choice: str = 'auto',131 uploaded_font=None,132 wrap_on_words: bool = True,133) -> Tuple[str, str, str]:134 """Generate selected PDF and return (pdf_path, extracted_text)135 136 Inputs: text, mode: 'normal'|'attacked'|'targeted', attack_factor, target_text137 Outputs: path to generated PDF, extracted text preview138 """139 tmp_dir = _ensure_tmp_dir()140 timestamp = int(time.time() * 1000)141 filename = f"{mode}_{timestamp}.pdf"142 output_path = os.path.join(tmp_dir, filename)143 144 # Clean input text145 clean_text = " ".join(text.split())146 147 # resolve font path and create an attacker instance for this request148 font_path = _resolve_font_path(choice=font_choice, uploaded_file=uploaded_font)149 attacker = PDFAttacker(font_path=font_path)150 # apply wrap mode151 attacker.wrap_on_words = wrap_on_words152 153 # Build a contextual status string for the UI154 resolved_font = font_path or "(auto/default)"155 status_lines = [f"Font resolved to: {resolved_font}", f"Wrap on words: {wrap_on_words}"]156 157 try:158 if mode == 'normal':159 attacker.create_normal_pdf(text=clean_text, output_path=output_path)160 elif mode == 'attacked':161 attacker.create_attacked_pdf(text=clean_text, output_path=output_path, attack_factor=attack_factor)162 elif mode == 'targeted':163 # Targeted may raise ValueError if not feasible164 attacker.create_targeted_pdf(text=clean_text, target_text=target_text, output_path=output_path)165 else:166 return "", f"Unknown mode: {mode}"167 168 except Exception as e:169 # Surface errors to the UI170 return "", f"Error extracting text: {e}", f"Error: {e}"171 172 # Extract text to show how the copied/extracted text looks173 extracted = _extract_text_from_pdf(output_path)174 175 return output_path, extracted, "\n".join(status_lines)176 177 178def build_demo():179 """Construct and return the Gradio Blocks demo"""180 with gr.Blocks(theme=theme) as demo:181 gr.Markdown("# PDF Humanizer: Attack demo\nGenerate PDFs that look normal but extract differently when copied")182 183 with gr.Row():184 txt = gr.Textbox(lines=8, label="Input text", value="Enter or paste text here...")185 with gr.Column():186 mode = gr.Radio(choices=['normal', 'attacked', 'targeted'], value='attacked', label='Mode')187 attack_factor = gr.Slider(minimum=0.0, maximum=1.0, step=0.05, value=0.7, label='Attack factor (attacked mode)')188 target_text = gr.Textbox(lines=2, label='Target text (targeted mode)')189 generate = gr.Button('Generate PDF')190 191 # Font selection: presets + custom upload192 font_choice = gr.Dropdown(choices=['auto', 'DejaVu Serif', 'Liberation Serif', 'FreeSerif', 'Arial', 'Helvetica', 'Times New Roman', 'Roboto', 'Courier', 'Custom'], value='auto', label='Font')193 upload_font = gr.File(label='Upload TTF/OTF (optional)', file_count='single')194 wrap_on_words = gr.Checkbox(label='Wrap on words', value=True)195 196 download_file = gr.File(label='Download generated PDF')197 extracted_preview = gr.Textbox(lines=8, label='Extracted text preview')198 status_box = gr.Textbox(lines=4, label='Status')199 200 def _on_generate(text, mode, attack_factor, target_text, font_choice, upload_font, wrap_on_words):201 path, extracted, status = generate_pdf(text=text, mode=mode, attack_factor=attack_factor, target_text=target_text, font_choice=font_choice, uploaded_font=upload_font, wrap_on_words=wrap_on_words)202 if not path:203 # Return empty file and error message in preview204 return None, extracted, status205 return path, extracted, status206 207 generate.click(fn=_on_generate, inputs=[txt, mode, attack_factor, target_text, font_choice, upload_font, wrap_on_words], outputs=[download_file, extracted_preview, status_box])208 209 return demo210 211 212if __name__ == '__main__':213 app = build_demo()214 app.launch(server_name='0.0.0.0', server_port=7860)215 