package main import ( "database/sql" "encoding/base64" "encoding/json" "fmt" "io" "log" "net/http" "os" "os/exec" "path/filepath" "regexp" "sort" "strings" "text/template" "time" "unicode" _ "github.com/mattn/go-sqlite3" ) const ( dbPath = "data/forms.db" pdfDir = "data/pdfs" port = ":8080" maxUpload = 10 << 20 // 10MB ) // Form represents a stored PDF form type Form struct { ID int64 `json:"id"` Name string `json:"name"` PDFPath string `json:"pdf_path"` HTMLContent string `json:"html_content"` FieldMeta string `json:"field_meta"` // JSON mapping of html_name -> pdf_name + type CreatedAt time.Time `json:"created_at"` } // PDFField represents a form field in a PDF type PDFField struct { Name string Type string DefaultValue string Rect string Required bool MaxLength int Options []string // from FieldStateOption Alt string // from FieldNameAlt } // FieldMapping stores the relationship between HTML names and PDF names type FieldMapping struct { HTMLName string `json:"html_name"` PDFName string `json:"pdf_name"` Type string `json:"type"` PseudoCheckbox bool `json:"pseudo_checkbox"` // CheckboxField is the html_name of the paired "Other" checkbox, set on the "Other specify" text input CheckboxField string `json:"checkbox_field"` } var db *sql.DB var templates *template.Template func isPDFTKInstalled() bool { _, err := exec.LookPath("pdftk") return err == nil } func isPDFToPPMInstalled() bool { _, err := exec.LookPath("pdftoppm") return err == nil } func main() { os.MkdirAll("data", 0755) os.MkdirAll(pdfDir, 0755) os.MkdirAll("static", 0755) var err error db, err = sql.Open("sqlite3", dbPath+"?_busy_timeout=5000") if err != nil { log.Fatalf("Failed to open database: %v", err) } defer db.Close() db.SetMaxOpenConns(1) if err = initDB(); err != nil { log.Fatalf("Failed to initialize database: %v", err) } templates, err = template.ParseGlob("templates/*.html") if err != nil { log.Fatalf("Failed to parse templates: %v", err) } http.HandleFunc("/admin/", handleAdmin) http.HandleFunc("/admin/upload", handleAdminUpload) http.HandleFunc("/admin/delete", handleDelete) http.HandleFunc("/admin/convert", handleConvert) http.HandleFunc("/form/", handleForm) http.Handle("/", http.FileServer(http.Dir("static"))) log.Printf("Server starting on %s", port) log.Printf("Admin panel: http://localhost%s/admin", port) log.Fatal(http.ListenAndServe(port, nil)) } func initDB() error { schema := ` CREATE TABLE IF NOT EXISTS forms ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, pdf_path TEXT NOT NULL, html_content TEXT NOT NULL DEFAULT '', field_meta TEXT NOT NULL DEFAULT '', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ); ` _, err := db.Exec(schema) return err } // ---- ADMIN HANDLERS ---- type adminData struct { Forms []Form Status string OpenAIKey bool OpenAIModel string PDFTK bool PDFToPPM bool } func handleAdmin(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/admin" && r.URL.Path != "/admin/" { http.NotFound(w, r) return } if r.Method != http.MethodGet { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } forms, err := getForms() if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } data := adminData{ Forms: forms, Status: r.URL.Query().Get("status"), OpenAIKey: os.Getenv("OPENAI_API_KEY") != "", OpenAIModel: os.Getenv("OPENAI_MODEL"), PDFTK: isPDFTKInstalled(), PDFToPPM: isPDFToPPMInstalled(), } if data.OpenAIModel == "" { data.OpenAIModel = "gpt-4o" } templates.ExecuteTemplate(w, "admin.html", data) } func handleAdminUpload(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } r.Body = http.MaxBytesReader(w, r.Body, maxUpload) if err := r.ParseMultipartForm(maxUpload); err != nil { http.Error(w, "File too large", http.StatusBadRequest) return } file, header, err := r.FormFile("pdf") if err != nil { http.Error(w, "No file uploaded", http.StatusBadRequest) return } defer file.Close() name := r.FormValue("name") if name == "" { name = header.Filename } timestamp := time.Now().Format("20060102_150405") ext := filepath.Ext(header.Filename) sanitized := strings.ReplaceAll(name, "/", "_") sanitized = strings.ReplaceAll(sanitized, "\\", "_") filename := fmt.Sprintf("%s_%s%s", timestamp, sanitized, ext) pdfPath := filepath.Join(pdfDir, filename) dst, err := os.Create(pdfPath) if err != nil { http.Error(w, "Failed to save file", http.StatusInternalServerError) return } defer dst.Close() if _, err = io.Copy(dst, file); err != nil { http.Error(w, "Failed to save file", http.StatusInternalServerError) return } _, err = db.Exec("INSERT INTO forms (name, pdf_path, html_content, field_meta) VALUES (?, ?, '', '')", name, pdfPath) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin?status=uploaded", http.StatusSeeOther) } func handleDelete(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } id := r.FormValue("id") if id == "" { http.Error(w, "No ID provided", http.StatusBadRequest) return } form, err := getFormByID(id) if err != nil { http.Error(w, "Form not found", http.StatusNotFound) return } os.Remove(form.PDFPath) _, err = db.Exec("DELETE FROM forms WHERE id = ?", form.ID) if err != nil { http.Error(w, "Database error", http.StatusInternalServerError) return } http.Redirect(w, r, "/admin?status=deleted", http.StatusSeeOther) } func handleConvert(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } id := r.FormValue("id") if id == "" { http.Error(w, "No ID provided", http.StatusBadRequest) return } form, err := getFormByID(id) if err != nil { http.Error(w, "Form not found", http.StatusNotFound) return } if _, err := exec.LookPath("pdftk"); err != nil { http.Error(w, "pdftk is not installed", http.StatusServiceUnavailable) return } // Extract form fields from PDF fields, err := extractPDFFields(form.PDFPath) if err != nil { http.Error(w, fmt.Sprintf("Failed to extract form fields: %v", err), http.StatusInternalServerError) return } if len(fields) == 0 { http.Error(w, "No form fields found in PDF", http.StatusBadRequest) return } apiKey := os.Getenv("OPENAI_API_KEY") if apiKey == "" { http.Error(w, "OPENAI_API_KEY environment variable is not set", http.StatusServiceUnavailable) return } apiURL := os.Getenv("OPENAI_API_URL") if apiURL == "" { apiURL = "https://api.openai.com/v1/chat/completions" } model := os.Getenv("OPENAI_MODEL") if model == "" { model = "gpt-4o" } // Convert PDF pages to images imageURIs, err := pdfToImages(form.PDFPath) if err != nil { http.Error(w, fmt.Sprintf("Failed to process PDF: %v", err), http.StatusInternalServerError) return } // Build the field mapping and prompt fieldMap := buildFieldMapping(fields) mappingJSON, _ := json.Marshal(fieldMap) // Build a table for the prompt var tableRows []string for i, m := range fieldMap { typeLabel := m.Type if m.Type == "Button" { typeLabel = "checkbox" } else if m.Type == "Text" || m.Type == "" { typeLabel = "text" } else if m.Type == "Signature" { typeLabel = "text" } tableRows = append(tableRows, fmt.Sprintf(" %d. name=\"%s\" type=\"%s\" (PDF field: \"%s\")", i+1, m.HTMLName, typeLabel, m.PDFName)) } tableStr := strings.Join(tableRows, "\n") prompt := fmt.Sprintf(`You are a PDF form to HTML converter. I've attached a PDF form document. You must generate a complete, self-contained HTML file that replicates this PDF form as a MULTI-STEP WIZARD. FIELD MAPPING - You MUST use these EXACT "name" attribute values: %s CRITICAL RULES: 1. Generate a SINGLE complete HTML document ( through ) 2. Use inline CSS for all styling 3. Use inline JavaScript for step navigation (no external libraries) 4. Organize the fields into logical STEPS (3-8 steps) based on the PDF sections 5. Use the EXACT name attribute values shown above (e.g. name="telephone_number") 6. For checkboxes (type="checkbox"), use 7. For text fields, use 8. Add clear labels for every field based on what you see in the PDF 9. The form should POST to "/form/%d" with method="post" and enctype="multipart/form-data" 10. Do NOT include any markdown code fences - output raw HTML only 11. Use a clean, professional design 12. Do NOT add any hidden input fields - they will be added automatically 13. ADA COMPLIANCE: every must be wrapped in a