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 }