package main // The label PRINT path: pick the paper-QSL queue, review each address, choose // the routing, and export PDFs whose pages are the exact label size — one PDF // per label kind, because a roll printer holds one stock at a time and a file // mixing 29 mm addresses with 62 mm QSO labels could not be printed at all. // // The pages arrive from the frontend already rasterised: the designer's canvas // renderer draws them at the stock's dpi, so what was previewed is — pixel for // pixel — what lands in the PDF. Go only carries them to disk. import ( "encoding/base64" "fmt" "os" "os/exec" "path/filepath" "strings" "time" "hamlog/internal/applog" "hamlog/internal/pdf" "hamlog/internal/qso" ) // LabelPaperQueue returns the contacts whose paper QSL is REQUESTED or QUEUED // (ADIF qsl_sent R/Q) — the natural worklist for a labelling session. The // frontend groups them by callsign. func (a *App) LabelPaperQueue() ([]qso.QSO, error) { if a.qso == nil { return nil, fmt.Errorf("db not initialized") } return a.qso.List(a.ctx, qso.ListFilter{ QSLSentIn: []string{"R", "Q"}, Limit: 10_000, }) } // LabelPDFPage is one page of the session's output: a rasterised label and its // physical size. Sizes vary WITHIN one document — the operator asked for a // single PDF holding QSO labels, addresses and return labels together, and PDF // pages each carry their own MediaBox, so a 90×29 page can follow a 100×62 one. type LabelPDFPage struct { PNG string `json:"png"` // base64, data-URL prefix tolerated WMm float64 `json:"w_mm"` HMm float64 `json:"h_mm"` } // LabelOpenPDF writes the session's labels to ONE temporary PDF and opens it in // the system viewer, from which the operator prints. No save dialog by choice: // the file is a print run, not a document to keep — anyone who wants to keep it // saves from the viewer. func (a *App) LabelOpenPDF(pages []LabelPDFPage) (string, error) { if len(pages) == 0 { return "", fmt.Errorf("nothing to print") } var doc pdf.Doc for i, pg := range pages { if pg.WMm < 5 || pg.HMm < 5 || pg.WMm > 400 || pg.HMm > 400 { return "", fmt.Errorf("page %d: label size out of range", i+1) } p := pg.PNG if idx := strings.Index(p, ","); idx >= 0 && strings.Contains(p[:idx], "base64") { p = p[idx+1:] } raw, err := base64.StdEncoding.DecodeString(p) if err != nil { return "", fmt.Errorf("page %d: %w", i+1, err) } if err := doc.AddImagePage(raw, pg.WMm, pg.HMm); err != nil { return "", fmt.Errorf("page %d: %w", i+1, err) } } out, err := doc.Bytes() if err != nil { return "", err } // A timestamped name in the temp dir: two sessions in one evening must not // fight over the file, least of all while a viewer holds the first one open. path := filepath.Join(os.TempDir(), fmt.Sprintf("opslog-labels-%s.pdf", time.Now().Format("20060102-150405"))) if err := os.WriteFile(path, out, 0o644); err != nil { return "", err } applog.Printf("labels: wrote %d page(s) to %s", len(pages), path) if err := exec.Command("rundll32", "url.dll,FileProtocolHandler", path).Start(); err != nil { applog.Printf("labels: could not open the PDF viewer: %v", err) return "", fmt.Errorf("the PDF was written to %s but no viewer opened: %w", path, err) } return path, nil }