package render import ( "fmt" "strings" ) // Page represents a page with its strokes for PDF rendering. type Page struct { PageNumber int Strokes []Stroke } // RenderPDF generates a minimal PDF document from pages. // Uses raw PDF operators — no external library needed. func RenderPDF(pageSize string, pages []Page) []byte { w, h := PageSizePt(pageSize) var objects []string objectOffsets := []int{} // Build PDF content var pdf strings.Builder // Header pdf.WriteString("%PDF-1.4\n") // Catalog (object 1) objectOffsets = append(objectOffsets, pdf.Len()) objects = append(objects, fmt.Sprintf("1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n")) pdf.WriteString(objects[0]) // Pages object (object 2) — we'll write this after we know the page objects pagesObjOffset := pdf.Len() pagesObjPlaceholder := strings.Repeat(" ", 200) + "\n" pdf.WriteString(pagesObjPlaceholder) objectOffsets = append(objectOffsets, pagesObjOffset) nextObj := 3 pageRefs := []string{} for _, page := range pages { contentObj := nextObj pageObj := nextObj + 1 nextObj += 2 // Content stream var stream strings.Builder for _, s := range page.Strokes { points := decodePoints(s.PointData) if len(points) < 4 { continue } penW := float64(s.PenSize) * canonicalToPDF // Set line properties fmt.Fprintf(&stream, "%.2f w\n", penW) stream.WriteString("1 J 1 j\n") // round cap, round join if s.Style == "dashed" { stream.WriteString("[7.2 4.8] 0 d\n") } else { stream.WriteString("[] 0 d\n") } // Draw path fmt.Fprintf(&stream, "%.2f %.2f m\n", points[0]*canonicalToPDF, h-points[1]*canonicalToPDF) for i := 2; i < len(points)-1; i += 2 { fmt.Fprintf(&stream, "%.2f %.2f l\n", points[i]*canonicalToPDF, h-points[i+1]*canonicalToPDF) } stream.WriteString("S\n") } streamBytes := stream.String() objectOffsets = append(objectOffsets, pdf.Len()) fmt.Fprintf(&pdf, "%d 0 obj\n<< /Length %d >>\nstream\n%s\nendstream\nendobj\n", contentObj, len(streamBytes), streamBytes) // Page object objectOffsets = append(objectOffsets, pdf.Len()) fmt.Fprintf(&pdf, "%d 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 %.0f %.0f] /Contents %d 0 R >>\nendobj\n", pageObj, w, h, contentObj) pageRefs = append(pageRefs, fmt.Sprintf("%d 0 R", pageObj)) } // Now write the Pages object at its placeholder position pagesObj := fmt.Sprintf("2 0 obj\n<< /Type /Pages /Kids [%s] /Count %d >>\nendobj\n", strings.Join(pageRefs, " "), len(pages)) // Overwrite placeholder — we need to rebuild the PDF string pdfStr := pdf.String() pdfStr = pdfStr[:pagesObjOffset] + pagesObj + strings.Repeat(" ", len(pagesObjPlaceholder)-len(pagesObj)) + pdfStr[pagesObjOffset+len(pagesObjPlaceholder):] // Rebuild with correct offsets for xref // For simplicity, just return the PDF bytes — most viewers handle minor xref issues var final strings.Builder final.WriteString(pdfStr) // Simple xref xrefOffset := final.Len() fmt.Fprintf(&final, "xref\n0 %d\n", nextObj) fmt.Fprintf(&final, "0000000000 65535 f \n") // For a proper PDF we'd need exact offsets — skip for now for i := 0; i < nextObj-1; i++ { fmt.Fprintf(&final, "%010d 00000 n \n", 0) } fmt.Fprintf(&final, "trailer\n<< /Size %d /Root 1 0 R >>\n", nextObj) fmt.Fprintf(&final, "startxref\n%d\n%%%%EOF\n", xrefOffset) return []byte(final.String()) }