package renderer import ( "fmt" "math" "os" "os/exec" "path/filepath" "strconv" "strings" "pdf-form-api/models" "github.com/pdfcpu/pdfcpu/pkg/api" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/model" "github.com/pdfcpu/pdfcpu/pkg/pdfcpu/types" ) func ComputeImageDimensions(pageWidth, pageHeight float64, density int, maxDim int) (imgWidth, imgHeight float64) { rawW := pageWidth / 72 * float64(density) rawH := pageHeight / 72 * float64(density) if rawW > float64(maxDim) || rawH > float64(maxDim) { scale := float64(maxDim) / math.Max(rawW, rawH) return rawW * scale, rawH * scale } return rawW, rawH } type PageInfo struct { Number int Width float64 Height float64 } func GetPageInfo(path string) ([]PageInfo, error) { f, err := os.Open(path) if err != nil { return nil, fmt.Errorf("opening pdf: %w", err) } defer f.Close() conf := model.NewDefaultConfiguration() ctx, err := api.ReadValidateAndOptimize(f, conf) if err != nil { return nil, fmt.Errorf("reading pdf: %w", err) } if err := ctx.XRefTable.EnsurePageCount(); err != nil { return nil, fmt.Errorf("getting page count: %w", err) } pageCount := ctx.XRefTable.PageCount if pageCount == 0 { return nil, fmt.Errorf("no pages found in pdf") } var pages []PageInfo for i := 1; i <= pageCount; i++ { box := getPageBox(ctx, i) pages = append(pages, PageInfo{ Number: i, Width: box.Width(), Height: box.Height(), }) } return pages, nil } func getPageBox(ctx *model.Context, pageNum int) *types.Rectangle { if pageNum < 1 || pageNum > ctx.XRefTable.PageCount { return types.NewRectangle(0, 0, 612, 792) } pageIndRef, err := ctx.XRefTable.PageDictIndRef(pageNum) if err != nil || pageIndRef == nil { return types.NewRectangle(0, 0, 612, 792) } pageDict, err := ctx.XRefTable.DereferenceDict(*pageIndRef) if err != nil || pageDict == nil { return types.NewRectangle(0, 0, 612, 792) } for _, key := range []string{"MediaBox", "CropBox"} { raw, ok := pageDict.Find(key) if !ok { continue } if arr, ok := raw.(types.Array); ok && len(arr) >= 4 { vals := make([]float64, 4) for i := 0; i < 4; i++ { switch v := arr[i].(type) { case types.Float: vals[i] = float64(v) case types.Integer: vals[i] = float64(v) } } return types.NewRectangle(vals[0], vals[1], vals[2], vals[3]) } } return types.NewRectangle(0, 0, 612, 792) } func RenderPages(pdfPath, outputDir string, density int) ([]string, error) { if err := os.MkdirAll(outputDir, 0o755); err != nil { return nil, fmt.Errorf("creating output dir: %w", err) } pages, err := GetPageInfo(pdfPath) if err != nil { return nil, fmt.Errorf("getting page info: %w", err) } var imagePaths []string for i, page := range pages { outPath := filepath.Join(outputDir, fmt.Sprintf("page_%d.png", page.Number)) cmd := exec.Command("convert", "-density", strconv.Itoa(density), "-resize", "1024x1024>", pdfPath+"["+strconv.Itoa(i)+"]", "-background", "white", "-depth", "8", "-flatten", outPath, ) if _, err := cmd.CombinedOutput(); err != nil { return imagePaths, fmt.Errorf("rendering page %d: %w", page.Number, err) } imagePaths = append(imagePaths, outPath) } return imagePaths, nil } func AnnotateImage(imagePath, outputPath string, fields []models.FormField, labelMap map[int64]string, pageWidth, pageHeight, imgWidth, imgHeight float64) error { if len(fields) == 0 { return nil } scaleX := imgWidth / pageWidth scaleY := imgHeight / pageHeight drawArgs := []string{} drawArgs = append(drawArgs, "-font", "Helvetica", "-pointsize", "11") for _, f := range fields { rectParts := strings.Fields(f.Rect) if len(rectParts) < 4 { continue } var x1, y1, x2, y2 float64 fmt.Sscanf(strings.Join(rectParts, " "), "%f %f %f %f", &x1, &y1, &x2, &y2) imgX1 := x1 * scaleX imgY1 := (pageHeight - y2) * scaleY imgX2 := x2 * scaleX imgY2 := (pageHeight - y1) * scaleY label := labelMap[f.ID] if label == "" { label = fmt.Sprintf("F%d", f.ID) } labelBarH := 12.0 labelY := imgY1 + labelBarH labelTextX := imgX1 + 11 labelTextY := imgY1 + 11 drawArgs = append(drawArgs, "-fill", "rgb(255,50,50)", "-stroke", "rgb(255,50,50)", "-strokewidth", "1", "-draw", fmt.Sprintf("rectangle %.0f,%.0f %.0f,%.0f", imgX1, imgY1, imgX2, imgY1+labelBarH), "-fill", "white", "-stroke", "none", "-draw", fmt.Sprintf("text %.0f,%.0f '%s'", labelTextX, labelTextY, label), "-fill", "rgb(255,50,50)", "-stroke", "rgb(255,50,50)", "-strokewidth", "1", "-draw", fmt.Sprintf("rectangle %.0f,%.0f %.0f,%.0f", imgX1, labelY, imgX2, imgY2), ) } args := []string{imagePath} args = append(args, drawArgs...) args = append(args, outputPath) cmd := exec.Command("convert", args...) if _, err := cmd.CombinedOutput(); err != nil { return fmt.Errorf("annotating image: %w", err) } return nil }