Fix: perform two-step xlsx->pdf->jpg conversion to preserve gridlines for thumbnails
All checks were successful
Automated Container Build / build-and-push (push) Successful in 1m4s

This commit is contained in:
Elijah 2026-05-25 15:47:45 -07:00
parent 2e463a3104
commit 130aa1b806

View file

@ -154,81 +154,120 @@ func generateOfficeThumbnail(filePath, destPath string, cfg *config.Config) erro
ext = "docx"
}
payload := map[string]interface{}{
"async": false,
"filetype": ext,
"key": fmt.Sprintf("thumb_%d", time.Now().UnixNano()),
"outputtype": "jpg",
"title": "preview.jpg",
"url": fileUrl,
"thumbnail": map[string]interface{}{
"aspect": 1,
"first": true,
"width": 1024,
"height": 1024,
},
}
if ext == "xlsx" {
payload["spreadsheetLayout"] = map[string]interface{}{
"ignorePrintArea": true,
"printGridlines": true,
"printHeadings": true,
"fitToWidth": 1,
callConvert := func(payload map[string]interface{}) (string, error) {
payloadBytes, err := json.Marshal(payload)
if err != nil {
return "", err
}
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
return err
}
req, err := http.NewRequest("POST", "https://office.elijahkuntz.com/ConvertService.ashx", bytes.NewBuffer(payloadBytes))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
// Sign the payload with the OnlyOffice JWT
if cfg.OnlyOfficeJWT != "" {
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload))
signedToken, err := jwtToken.SignedString([]byte(cfg.OnlyOfficeJWT))
if err == nil {
req.Header.Set("Authorization", "Bearer "+signedToken)
req, err := http.NewRequest("POST", "https://office.elijahkuntz.com/ConvertService.ashx", bytes.NewBuffer(payloadBytes))
if err != nil {
return "", err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if cfg.OnlyOfficeJWT != "" {
jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims(payload))
signedToken, err := jwtToken.SignedString([]byte(cfg.OnlyOfficeJWT))
if err == nil {
req.Header.Set("Authorization", "Bearer "+signedToken)
}
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return "", fmt.Errorf("conversion request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", fmt.Errorf("conversion request returned status: %d", resp.StatusCode)
}
var result struct {
Error *int `json:"error,omitempty"`
FileUrl string `json:"fileUrl,omitempty"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return "", fmt.Errorf("failed to decode conversion response: %w", err)
}
if result.Error != nil && *result.Error != 0 {
return "", fmt.Errorf("conversion service returned error code: %d", *result.Error)
}
if result.FileUrl == "" {
return "", fmt.Errorf("no fileUrl in conversion response")
}
return result.FileUrl, nil
}
client := &http.Client{Timeout: 30 * time.Second}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("conversion request failed: %w", err)
}
defer resp.Body.Close()
var finalImgUrl string
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("conversion request returned status: %d", resp.StatusCode)
}
var result struct {
Error *int `json:"error,omitempty"`
FileUrl string `json:"fileUrl,omitempty"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return fmt.Errorf("failed to decode conversion response: %w", err)
}
// Wait, the API might return error 0 for success
if result.Error != nil && *result.Error != 0 {
return fmt.Errorf("conversion service returned error code: %d", *result.Error)
}
if result.FileUrl == "" {
return fmt.Errorf("no fileUrl in conversion response")
if ext == "xlsx" || ext == "xls" {
pdfPayload := map[string]interface{}{
"async": false,
"filetype": ext,
"key": fmt.Sprintf("thumb_pdf_%d", time.Now().UnixNano()),
"outputtype": "pdf",
"title": "preview.pdf",
"url": fileUrl,
"spreadsheetLayout": map[string]interface{}{
"ignorePrintArea": true,
"printGridlines": true,
"printHeadings": true,
"fitToWidth": 1,
},
}
pdfUrl, err := callConvert(pdfPayload)
if err != nil {
return fmt.Errorf("failed to convert xlsx to pdf: %w", err)
}
jpgPayload := map[string]interface{}{
"async": false,
"filetype": "pdf",
"key": fmt.Sprintf("thumb_jpg_%d", time.Now().UnixNano()),
"outputtype": "jpg",
"title": "preview.jpg",
"url": pdfUrl,
"thumbnail": map[string]interface{}{
"aspect": 1,
"first": true,
"width": 1024,
"height": 1024,
},
}
finalImgUrl, err = callConvert(jpgPayload)
if err != nil {
return fmt.Errorf("failed to convert pdf to jpg: %w", err)
}
} else {
payload := map[string]interface{}{
"async": false,
"filetype": ext,
"key": fmt.Sprintf("thumb_%d", time.Now().UnixNano()),
"outputtype": "jpg",
"title": "preview.jpg",
"url": fileUrl,
"thumbnail": map[string]interface{}{
"aspect": 1,
"first": true,
"width": 1024,
"height": 1024,
},
}
var err error
finalImgUrl, err = callConvert(payload)
if err != nil {
return err
}
}
// Download the resulting image
imgResp, err := http.Get(result.FileUrl)
imgResp, err := http.Get(finalImgUrl)
if err != nil {
return fmt.Errorf("failed to download converted image: %w", err)
}