github.com/benoitkugler/goacve@v0.0.0-20201217100549-151ce6e55dc8/server/documents/compress.go (about) 1 package documents 2 3 import ( 4 "bytes" 5 "fmt" 6 "image/jpeg" 7 "mime" 8 "os/exec" 9 "path/filepath" 10 "strconv" 11 "strings" 12 13 "github.com/benoitkugler/goACVE/server/core/apiserver" 14 ) 15 16 // compressionRate ajuste le taux de compression (le taux de qualite est 100 - taux de compression) 17 // Peut renvoyer `0` pour ne pas compresser du tout. 18 func compressionRate(size int) int { 19 const minSize, mediumSize, maxSize = 200000, 1000000, apiserver.DOCUMENT_MAX_SIZE 20 if size < minSize { 21 return 0 22 } 23 24 if size < mediumSize { 25 const minRate, maxRate = 20, 40 26 return affine(minRate, maxRate, minSize, mediumSize, size) 27 } 28 if size > maxSize { 29 size = maxSize 30 } 31 const minRate, maxRate = 50, 80 // document de plus de 1M 32 return affine(minRate, maxRate, minSize, mediumSize, size) 33 } 34 35 // renvoie un taux linéaire en `minRate` et `maxRate` 36 // quand `size` est entre `minSize` et `maxSize` 37 func affine(minRate, maxRate, minSize, maxSize, size int) int { 38 coeff := float64(size-minSize) / float64(maxSize-minSize) 39 return int(float64(maxRate-minRate)*coeff) + minRate 40 } 41 42 // renvoie une version compressée (éventuellement identique) 43 // l'extension du fichier peut changer (pdf -> jpg) 44 func compressDocument(filename string, content []byte) (string, []byte, error) { 45 ext := filepath.Ext(filename) 46 var err error 47 switch mime.TypeByExtension(ext) { 48 case "image/jpeg": 49 content, err = CompressJPEG(content) 50 case "application/pdf": 51 var contentJpg []byte 52 contentJpg, err = CompressPdf(content) 53 if len(contentJpg) < len(content) { // est-ce rentable ? 54 content = contentJpg 55 filename = strings.TrimSuffix(filename, ext) + ".JPG" 56 } 57 } 58 return filename, content, err 59 } 60 61 func CompressJPEG(content []byte) ([]byte, error) { 62 quality := 100 - compressionRate(len(content)) 63 if quality == 100 { // pas de compression 64 return content, nil 65 } 66 67 img, err := jpeg.Decode(bytes.NewReader(content)) 68 if err != nil { 69 return nil, err 70 } 71 72 var buf bytes.Buffer 73 err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: quality}) 74 75 return buf.Bytes(), err 76 } 77 78 // fileContent doit être un pdf 79 func countPdfPages(fileContent []byte) (int, error) { 80 var out bytes.Buffer 81 cmdString := "pdftk - dump_data | awk '/NumberOfPages/{print $2}'" 82 cmd := exec.Command("bash", "-c", cmdString) 83 cmd.Stdin = bytes.NewReader(fileContent) 84 cmd.Stdout = &out 85 if err := cmd.Run(); err != nil { 86 return 0, fmt.Errorf("Impossible traiter le document PDF : %s", err) 87 } 88 nb, err := strconv.Atoi(strings.TrimSpace(out.String())) // le résulat contient un saut de ligne 89 if err != nil { 90 return 0, fmt.Errorf("Impossible traiter le document PDF : %s", err) 91 } 92 return nb, nil 93 } 94 95 // essaie de convertir en jpg, de compresser 96 // le caller doit vérifier si la conversion est intéressante 97 func CompressPdf(fileContent []byte) ([]byte, error) { 98 nb, err := countPdfPages(fileContent) 99 if err != nil { 100 return nil, err 101 } 102 if nb > 1 { // on ne peut pas convertir en jpg 103 return fileContent, nil 104 } 105 106 // use Ghoscript 107 var asJPEG bytes.Buffer 108 cmd := exec.Command("gs", "-dBATCH", "-dNOPAUSE", "-sDEVICE=jpeg", "-dFirstPage=1", "-dLastPage=1", "-r200", "-dUseCropBox", "-sstdout=%stderr", "-sOutputFile=-", "-") 109 cmd.Stdin = bytes.NewReader(fileContent) 110 cmd.Stdout = &asJPEG 111 if err := cmd.Run(); err != nil { 112 return nil, fmt.Errorf("Impossible de convertir le PDF : %s", err) 113 } 114 115 compressedJPG, err := CompressJPEG(asJPEG.Bytes()) 116 if err != nil { 117 return nil, err 118 } 119 return compressedJPG, nil 120 }