// Package docfast provides a Go client for the DocFast HTML/Markdown to PDF API. // // Usage: // // client := docfast.New("df_pro_your_key") // pdf, err := client.HTML("

Hello

", nil) package docfast import ( "bytes" "encoding/json" "fmt" "io" "net/http" "time" ) const defaultBaseURL = "https://docfast.dev" // Margin defines PDF page margins using CSS units (e.g. "20mm", "1in"). type Margin struct { Top string `json:"top,omitempty"` Bottom string `json:"bottom,omitempty"` Left string `json:"left,omitempty"` Right string `json:"right,omitempty"` } // PDFOptions configures PDF generation. All fields are optional. type PDFOptions struct { // Page size: A4, Letter, Legal, A3, A5, Tabloid. Ignored if Width/Height set. Format string `json:"format,omitempty"` // Landscape orientation. Landscape bool `json:"landscape,omitempty"` // Page margins. Margin *Margin `json:"margin,omitempty"` // Print background graphics and colors. Default: true. PrintBackground *bool `json:"printBackground,omitempty"` // Suggested filename for the PDF download. Filename string `json:"filename,omitempty"` // HTML template for page header. Requires DisplayHeaderFooter: true. // Supports CSS classes: date, title, url, pageNumber, totalPages. HeaderTemplate string `json:"headerTemplate,omitempty"` // HTML template for page footer. Same classes as HeaderTemplate. FooterTemplate string `json:"footerTemplate,omitempty"` // Show header and footer templates. DisplayHeaderFooter bool `json:"displayHeaderFooter,omitempty"` // Scale of webpage rendering (0.1 to 2.0). Default: 1. Scale float64 `json:"scale,omitempty"` // Paper ranges to print, e.g. "1-3,5". PageRanges string `json:"pageRanges,omitempty"` // Give CSS @page size priority over Format. PreferCSSPageSize bool `json:"preferCSSPageSize,omitempty"` // Paper width with units (e.g. "8.5in"). Overrides Format. Width string `json:"width,omitempty"` // Paper height with units (e.g. "11in"). Overrides Format. Height string `json:"height,omitempty"` } // Template describes an available PDF template. type Template struct { ID string `json:"id"` Name string `json:"name"` Description string `json:"description,omitempty"` } // Error is returned when the API responds with an error. type Error struct { StatusCode int Message string Code string } func (e *Error) Error() string { if e.Code != "" { return fmt.Sprintf("docfast: %s (code=%s, status=%d)", e.Message, e.Code, e.StatusCode) } return fmt.Sprintf("docfast: %s (status=%d)", e.Message, e.StatusCode) } // ClientOption configures the Client. type ClientOption func(*Client) // WithBaseURL sets a custom API base URL. func WithBaseURL(url string) ClientOption { return func(c *Client) { c.baseURL = url } } // WithHTTPClient sets a custom http.Client. func WithHTTPClient(hc *http.Client) ClientOption { return func(c *Client) { c.httpClient = hc } } // Client is the DocFast API client. type Client struct { apiKey string baseURL string httpClient *http.Client } // New creates a new DocFast client. func New(apiKey string, opts ...ClientOption) *Client { c := &Client{ apiKey: apiKey, baseURL: defaultBaseURL, httpClient: &http.Client{ Timeout: 60 * time.Second, }, } for _, o := range opts { o(c) } return c } func (c *Client) post(path string, body map[string]interface{}) ([]byte, error) { data, err := json.Marshal(body) if err != nil { return nil, fmt.Errorf("docfast: marshal error: %w", err) } req, err := http.NewRequest("POST", c.baseURL+path, bytes.NewReader(data)) if err != nil { return nil, fmt.Errorf("docfast: request error: %w", err) } req.Header.Set("Authorization", "Bearer "+c.apiKey) req.Header.Set("Content-Type", "application/json") resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("docfast: request failed: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("docfast: read error: %w", err) } if resp.StatusCode >= 400 { apiErr := &Error{StatusCode: resp.StatusCode, Message: fmt.Sprintf("HTTP %d", resp.StatusCode)} var errResp struct { Error string `json:"error"` Code string `json:"code"` } if json.Unmarshal(respBody, &errResp) == nil { if errResp.Error != "" { apiErr.Message = errResp.Error } apiErr.Code = errResp.Code } return nil, apiErr } return respBody, nil } func (c *Client) get(path string) ([]byte, error) { req, err := http.NewRequest("GET", c.baseURL+path, nil) if err != nil { return nil, fmt.Errorf("docfast: request error: %w", err) } req.Header.Set("Authorization", "Bearer "+c.apiKey) resp, err := c.httpClient.Do(req) if err != nil { return nil, fmt.Errorf("docfast: request failed: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("docfast: read error: %w", err) } if resp.StatusCode >= 400 { apiErr := &Error{StatusCode: resp.StatusCode, Message: fmt.Sprintf("HTTP %d", resp.StatusCode)} var errResp struct { Error string `json:"error"` Code string `json:"code"` } if json.Unmarshal(respBody, &errResp) == nil && errResp.Error != "" { apiErr.Message = errResp.Error apiErr.Code = errResp.Code } return nil, apiErr } return respBody, nil } func mergeOptions(body map[string]interface{}, opts *PDFOptions) { if opts == nil { return } if opts.Format != "" { body["format"] = opts.Format } if opts.Landscape { body["landscape"] = true } if opts.Margin != nil { body["margin"] = opts.Margin } if opts.PrintBackground != nil { body["printBackground"] = *opts.PrintBackground } if opts.Filename != "" { body["filename"] = opts.Filename } if opts.HeaderTemplate != "" { body["headerTemplate"] = opts.HeaderTemplate } if opts.FooterTemplate != "" { body["footerTemplate"] = opts.FooterTemplate } if opts.DisplayHeaderFooter { body["displayHeaderFooter"] = true } if opts.Scale != 0 { body["scale"] = opts.Scale } if opts.PageRanges != "" { body["pageRanges"] = opts.PageRanges } if opts.PreferCSSPageSize { body["preferCSSPageSize"] = true } if opts.Width != "" { body["width"] = opts.Width } if opts.Height != "" { body["height"] = opts.Height } } // HTML converts HTML content to PDF. Returns the raw PDF bytes. func (c *Client) HTML(html string, opts *PDFOptions) ([]byte, error) { body := map[string]interface{}{"html": html} mergeOptions(body, opts) return c.post("/v1/convert/html", body) } // HTMLWithCSS converts an HTML fragment with optional CSS to PDF. func (c *Client) HTMLWithCSS(html, css string, opts *PDFOptions) ([]byte, error) { body := map[string]interface{}{"html": html, "css": css} mergeOptions(body, opts) return c.post("/v1/convert/html", body) } // Markdown converts Markdown content to PDF. func (c *Client) Markdown(markdown string, opts *PDFOptions) ([]byte, error) { body := map[string]interface{}{"markdown": markdown} mergeOptions(body, opts) return c.post("/v1/convert/markdown", body) } // MarkdownWithCSS converts Markdown with optional CSS to PDF. func (c *Client) MarkdownWithCSS(markdown, css string, opts *PDFOptions) ([]byte, error) { body := map[string]interface{}{"markdown": markdown, "css": css} mergeOptions(body, opts) return c.post("/v1/convert/markdown", body) } // URL converts a web page at the given URL to PDF. func (c *Client) URL(url string, opts *PDFOptions) ([]byte, error) { body := map[string]interface{}{"url": url} mergeOptions(body, opts) return c.post("/v1/convert/url", body) } // Templates returns the list of available PDF templates. func (c *Client) Templates() ([]Template, error) { data, err := c.get("/v1/templates") if err != nil { return nil, err } var result struct { Templates []Template `json:"templates"` } if err := json.Unmarshal(data, &result); err != nil { return nil, fmt.Errorf("docfast: decode error: %w", err) } return result.Templates, nil } // RenderTemplate renders a template with the given data and returns PDF bytes. func (c *Client) RenderTemplate(templateID string, data map[string]interface{}) ([]byte, error) { body := map[string]interface{}{"data": data} return c.post("/v1/templates/"+templateID, body) } // Bool is a helper to create a *bool for PDFOptions.PrintBackground. func Bool(v bool) *bool { return &v }