github.com/cozy/cozy-stack@v0.0.0-20240603063001-31110fa4cae1/model/vfs/pdf.go (about)

     1  package vfs
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/http"
     7  	"os"
     8  	"os/exec"
     9  
    10  	"github.com/cozy/cozy-stack/pkg/config/config"
    11  	"github.com/cozy/cozy-stack/pkg/logger"
    12  	"github.com/cozy/cozy-stack/pkg/previewfs"
    13  )
    14  
    15  // ServePDFIcon will send the icon image for a PDF.
    16  func ServePDFIcon(w http.ResponseWriter, req *http.Request, fs VFS, doc *FileDoc) error {
    17  	name := fmt.Sprintf("%s-icon.jpg", doc.ID())
    18  	modtime := doc.UpdatedAt
    19  	if doc.CozyMetadata != nil && doc.CozyMetadata.UploadedAt != nil {
    20  		modtime = *doc.CozyMetadata.UploadedAt
    21  	}
    22  	buf, err := icon(fs, doc)
    23  	if err != nil {
    24  		return err
    25  	}
    26  	http.ServeContent(w, req, name, modtime, bytes.NewReader(buf.Bytes()))
    27  	return nil
    28  }
    29  
    30  func icon(fs VFS, doc *FileDoc) (*bytes.Buffer, error) {
    31  	cache := previewfs.SystemCache()
    32  	if buf, err := cache.GetIcon(doc.MD5Sum); err == nil {
    33  		return buf, nil
    34  	}
    35  
    36  	buf, err := generateIcon(fs, doc)
    37  	if err != nil {
    38  		return nil, err
    39  	}
    40  	_ = cache.SetIcon(doc.MD5Sum, buf)
    41  	return buf, nil
    42  }
    43  
    44  func generateIcon(fs VFS, doc *FileDoc) (*bytes.Buffer, error) {
    45  	f, err := fs.OpenFile(doc)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	defer f.Close()
    50  
    51  	tempDir, err := os.MkdirTemp("", "magick")
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	defer os.RemoveAll(tempDir)
    56  	envTempDir := fmt.Sprintf("MAGICK_TEMPORARY_PATH=%s", tempDir)
    57  	env := []string{envTempDir}
    58  
    59  	convertCmd := config.GetConfig().Jobs.ImageMagickConvertCmd
    60  	if convertCmd == "" {
    61  		convertCmd = "convert"
    62  	}
    63  	args := []string{
    64  		"-limit", "Memory", "1GB",
    65  		"-limit", "Map", "1GB",
    66  		"-[0]",           // Takes the input from stdin
    67  		"-quality", "99", // At small resolution, we want a very good quality
    68  		"-interlace", "none", // Don't use progressive JPEGs, they are heavier
    69  		"-thumbnail", "96x96", // Makes a thumbnail that fits inside the given format
    70  		"-background", "white", // Use white for the background
    71  		"-alpha", "remove", // JPEGs don't have an alpha channel
    72  		"-colorspace", "sRGB", // Use the colorspace recommended for web, sRGB
    73  		"jpg:-", // Send the output on stdout, in JPEG format
    74  	}
    75  
    76  	var stdout, stderr bytes.Buffer
    77  	cmd := exec.Command(convertCmd, args...)
    78  	cmd.Env = env
    79  	cmd.Stdin = f
    80  	cmd.Stdout = &stdout
    81  	cmd.Stderr = &stderr
    82  	if err := cmd.Run(); err != nil {
    83  		// Truncate very long messages
    84  		msg := stderr.String()
    85  		if len(msg) > 4000 {
    86  			msg = msg[:4000]
    87  		}
    88  		logger.WithNamespace("pdf_icon").
    89  			WithField("stderr", msg).
    90  			WithField("file_id", doc.ID()).
    91  			Errorf("imagemagick failed: %s", err)
    92  		return nil, err
    93  	}
    94  	return &stdout, nil
    95  }
    96  
    97  // ServePDFPreview will send the preview image for a PDF.
    98  func ServePDFPreview(w http.ResponseWriter, req *http.Request, fs VFS, doc *FileDoc) error {
    99  	name := fmt.Sprintf("%s-preview.jpg", doc.ID())
   100  	modtime := doc.UpdatedAt
   101  	if doc.CozyMetadata != nil && doc.CozyMetadata.UploadedAt != nil {
   102  		modtime = *doc.CozyMetadata.UploadedAt
   103  	}
   104  	buf, err := preview(fs, doc)
   105  	if err != nil {
   106  		return err
   107  	}
   108  	http.ServeContent(w, req, name, modtime, bytes.NewReader(buf.Bytes()))
   109  	return nil
   110  }
   111  
   112  func preview(fs VFS, doc *FileDoc) (*bytes.Buffer, error) {
   113  	cache := previewfs.SystemCache()
   114  	if buf, err := cache.GetPreview(doc.MD5Sum); err == nil {
   115  		return buf, nil
   116  	}
   117  
   118  	buf, err := generatePreview(fs, doc)
   119  	if err != nil {
   120  		return nil, err
   121  	}
   122  	_ = cache.SetPreview(doc.MD5Sum, buf)
   123  	return buf, nil
   124  }
   125  
   126  func generatePreview(fs VFS, doc *FileDoc) (*bytes.Buffer, error) {
   127  	f, err := fs.OpenFile(doc)
   128  	if err != nil {
   129  		return nil, err
   130  	}
   131  	defer f.Close()
   132  
   133  	tempDir, err := os.MkdirTemp("", "magick")
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  	defer os.RemoveAll(tempDir)
   138  	envTempDir := fmt.Sprintf("MAGICK_TEMPORARY_PATH=%s", tempDir)
   139  	env := []string{envTempDir}
   140  
   141  	convertCmd := config.GetConfig().Jobs.ImageMagickConvertCmd
   142  	if convertCmd == "" {
   143  		convertCmd = "convert"
   144  	}
   145  	args := []string{
   146  		"-limit", "Memory", "2GB",
   147  		"-limit", "Map", "3GB",
   148  		"-density", "300", // We want a high resolution for PDFs
   149  		"-[0]",           // Takes the input from stdin
   150  		"-quality", "82", // A good compromise between file size and quality
   151  		"-interlace", "none", // Don't use progressive JPEGs, they are heavier
   152  		"-thumbnail", "1080x1920>", // Makes a thumbnail that fits inside the given format
   153  		"-background", "white", // Use white for the background
   154  		"-alpha", "remove", // JPEGs don't have an alpha channel
   155  		"-colorspace", "sRGB", // Use the colorspace recommended for web, sRGB
   156  		"jpg:-", // Send the output on stdout, in JPEG format
   157  	}
   158  
   159  	var stdout, stderr bytes.Buffer
   160  	cmd := exec.Command(convertCmd, args...)
   161  	cmd.Env = env
   162  	cmd.Stdin = f
   163  	cmd.Stdout = &stdout
   164  	cmd.Stderr = &stderr
   165  	if err := cmd.Run(); err != nil {
   166  		// Truncate very long messages
   167  		msg := stderr.String()
   168  		if len(msg) > 4000 {
   169  			msg = msg[:4000]
   170  		}
   171  		logger.WithNamespace("pdf_preview").
   172  			WithField("stderr", msg).
   173  			WithField("file_id", doc.ID()).
   174  			Errorf("imagemagick failed: %s", err)
   175  		return nil, err
   176  	}
   177  	return &stdout, nil
   178  }