package main import ( "bytes" "fmt" "image" "image/color" _ "image/gif" _ "image/jpeg" "image/png" "io" "log" "math" "net/http" "os" "os/exec" "path/filepath" "strconv" "strings" "time" "github.com/anthonynsimon/bild/blur" "github.com/anthonynsimon/bild/effect" "github.com/anthonynsimon/bild/segment" "github.com/nfnt/resize" "simonwaldherr.de/go/zplgfa" ) const maxUploadSize = 20 << 20 // 20 MB func isPDF(data []byte) bool { return len(data) >= 4 && string(data[:4]) == "%PDF" } // pdfPageToImage renders one page of a PDF to an image using pdftoppm. // dpi controls the render resolution (use 150 for thumbnails, 300 for conversion). // Returns the image, total page count, and any error. func pdfPageToImage(data []byte, page, dpi int) (image.Image, int, error) { tmpDir, err := os.MkdirTemp("", "zplgfa-*") if err != nil { return nil, 0, err } defer os.RemoveAll(tmpDir) pdfPath := filepath.Join(tmpDir, "input.pdf") if err := os.WriteFile(pdfPath, data, 0600); err != nil { return nil, 0, err } // Get total page count via pdfinfo pageCount := 1 if out, err := exec.Command("pdfinfo", pdfPath).Output(); err == nil { for _, line := range strings.Split(string(out), "\n") { if strings.HasPrefix(line, "Pages:") { if n, err := strconv.Atoi(strings.TrimSpace(strings.TrimPrefix(line, "Pages:"))); err == nil { pageCount = n } } } } if page < 1 { page = 1 } if page > pageCount { page = pageCount } // Convert the requested page to PNG outBase := filepath.Join(tmpDir, "out") cmd := exec.Command("pdftoppm", "-png", "-r", strconv.Itoa(dpi), "-f", strconv.Itoa(page), "-l", strconv.Itoa(page), pdfPath, outBase) if out, err := cmd.CombinedOutput(); err != nil { return nil, pageCount, fmt.Errorf("pdftoppm: %s", string(out)) } matches, err := filepath.Glob(filepath.Join(tmpDir, "out-*.png")) if err != nil || len(matches) == 0 { return nil, pageCount, fmt.Errorf("pdftoppm produced no output") } f, err := os.Open(matches[0]) if err != nil { return nil, pageCount, err } defer f.Close() img, _, err := image.Decode(f) return img, pageCount, err } func getGraphicType(t string) zplgfa.GraphicType { switch strings.ToUpper(t) { case "ASCII": return zplgfa.ASCII case "BINARY": return zplgfa.Binary default: return zplgfa.CompressedASCII } } func invertImage(img image.Image) image.Image { b := img.Bounds() out := image.NewNRGBA(b) for y := b.Min.Y; y < b.Max.Y; y++ { for x := b.Min.X; x < b.Max.X; x++ { r, g, bv, a := img.At(x, y).RGBA() out.Set(x, y, color.RGBA{ R: uint8((65535 - r) >> 8), G: uint8((65535 - g) >> 8), B: uint8((65535 - bv) >> 8), A: uint8(a >> 8), }) } } return out } func monochromeImage(img image.Image) image.Image { b := img.Bounds() out := image.NewNRGBA(b) for y := b.Min.Y; y < b.Max.Y; y++ { for x := b.Min.X; x < b.Max.X; x++ { r, g, bv, a := img.At(x, y).RGBA() var v uint8 if r > math.MaxUint16/2 || g > math.MaxUint16/2 || bv > math.MaxUint16/2 { v = 255 } out.Set(x, y, color.RGBA{R: v, G: v, B: v, A: uint8(a >> 8)}) } } return out } func processImage(img image.Image, editFlag string, width, height uint) image.Image { if strings.Contains(editFlag, "monochrome") { img = monochromeImage(img) } bounds := img.Bounds() if strings.Contains(editFlag, "blur") { img = blur.Gaussian(img, float64(bounds.Dx())/300) } if strings.Contains(editFlag, "edge") { img = effect.Sobel(img) } if strings.Contains(editFlag, "segment") { img = segment.Threshold(img, 128) } if strings.Contains(editFlag, "invert") { img = invertImage(img) } if width > 0 || height > 0 { img = resize.Resize(width, height, img, resize.MitchellNetravali) } return img } func convertHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseMultipartForm(maxUploadSize); err != nil { http.Error(w, "file too large", http.StatusBadRequest) return } file, _, err := r.FormFile("file") if err != nil { http.Error(w, "missing file", http.StatusBadRequest) return } defer file.Close() data, err := io.ReadAll(file) if err != nil { http.Error(w, "failed to read file", http.StatusBadRequest) return } var img image.Image if isPDF(data) { page := 1 if p, e2 := strconv.Atoi(r.FormValue("page")); e2 == nil && p > 0 { page = p } img, _, err = pdfPageToImage(data, page, 300) if err != nil { http.Error(w, "PDF conversion failed: "+err.Error(), http.StatusBadRequest) return } } else { img, _, err = image.Decode(bytes.NewReader(data)) if err != nil { http.Error(w, fmt.Sprintf("cannot decode image: %s", err), http.StatusBadRequest) return } } var w2, h2 uint if v, err := strconv.ParseUint(r.FormValue("width"), 10, 32); err == nil { w2 = uint(v) } if v, err := strconv.ParseUint(r.FormValue("height"), 10, 32); err == nil { h2 = uint(v) } editFlag := r.FormValue("edit") graphicType := getGraphicType(r.FormValue("type")) img = processImage(img, editFlag, w2, h2) flat := zplgfa.FlattenImage(img) zpl := zplgfa.ConvertToZPL(flat, graphicType) w.Header().Set("Content-Type", "text/plain; charset=utf-8") w.Header().Set("Content-Disposition", `attachment; filename="label.zpl"`) fmt.Fprint(w, zpl) } func previewHandler(w http.ResponseWriter, r *http.Request) { // Returns image info (dimensions) before conversion if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseMultipartForm(maxUploadSize); err != nil { http.Error(w, "file too large", http.StatusBadRequest) return } file, _, err := r.FormFile("file") if err != nil { http.Error(w, "missing file", http.StatusBadRequest) return } defer file.Close() var buf bytes.Buffer buf.ReadFrom(file) data := buf.Bytes() if isPDF(data) { page := 1 if p, e2 := strconv.Atoi(r.FormValue("page")); e2 == nil && p > 0 { page = p } img, pageCount, err := pdfPageToImage(data, page, 150) if err != nil { http.Error(w, "PDF conversion failed: "+err.Error(), http.StatusBadRequest) return } b := img.Bounds() w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"width":%d,"height":%d,"pages":%d,"isPDF":true}`, b.Dx(), b.Dy(), pageCount) return } cfg, _, err := image.DecodeConfig(bytes.NewReader(data)) if err != nil { http.Error(w, "cannot decode image", http.StatusBadRequest) return } w.Header().Set("Content-Type", "application/json") fmt.Fprintf(w, `{"width":%d,"height":%d,"pages":1,"isPDF":false}`, cfg.Width, cfg.Height) } func pageThumbHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } if err := r.ParseMultipartForm(maxUploadSize); err != nil { http.Error(w, "file too large", http.StatusBadRequest) return } file, _, err := r.FormFile("file") if err != nil { http.Error(w, "missing file", http.StatusBadRequest) return } defer file.Close() data, err := io.ReadAll(file) if err != nil { http.Error(w, "failed to read file", http.StatusBadRequest) return } if !isPDF(data) { http.Error(w, "not a PDF", http.StatusBadRequest) return } page := 1 if p, e2 := strconv.Atoi(r.FormValue("page")); e2 == nil && p > 0 { page = p } img, _, err := pdfPageToImage(data, page, 150) if err != nil { http.Error(w, "PDF conversion failed: "+err.Error(), http.StatusBadRequest) return } w.Header().Set("Content-Type", "image/png") png.Encode(w, img) } var labelaryClient = &http.Client{Timeout: 15 * time.Second} func renderHandler(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } dpmm := r.URL.Query().Get("dpmm") width := r.URL.Query().Get("width") height := r.URL.Query().Get("height") if dpmm == "" { dpmm = "8" } if width == "" { width = "4" } if height == "" { height = "6" } body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "failed to read body", http.StatusBadRequest) return } url := fmt.Sprintf("http://api.labelary.com/v1/printers/%s/labels/%sx%s/0/", dpmm, width, height) req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) if err != nil { http.Error(w, "failed to build request", http.StatusInternalServerError) return } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") req.Header.Set("Accept", "image/png") resp, err := labelaryClient.Do(req) if err != nil { http.Error(w, "labelary unreachable: "+err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { http.Error(w, "labelary returned "+resp.Status, http.StatusBadGateway) return } w.Header().Set("Content-Type", "image/png") io.Copy(w, resp.Body) } func main() { mux := http.NewServeMux() mux.HandleFunc("/convert", convertHandler) mux.HandleFunc("/preview", previewHandler) mux.HandleFunc("/page-thumb", pageThumbHandler) mux.HandleFunc("/render", renderHandler) mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/html; charset=utf-8") w.Write([]byte(indexHTML)) }) addr := ":4543" log.Printf("zplgfa web UI listening on %s", addr) log.Fatal(http.ListenAndServe(addr, mux)) } const indexHTML = ` zplgfa – Image to ZPL Converter

zplgfa

Convert images to ZPL Graphic Fields for Zebra label printers

🖼️
Drop an image here or click to browse
PNG, JPEG, GIF, PDF — max 20 MB
Image Preview
preview
Dimensions in
Printer DPI
ZPL Output
Label Preview (rendered by Labelary)
label preview
`