@@ -5,6 +5,7 @@ package main
5
5
import (
6
6
"bytes"
7
7
"context"
8
+ "errors"
8
9
"fmt"
9
10
"image"
10
11
"image/color"
@@ -17,6 +18,7 @@ import (
17
18
"net/http"
18
19
"net/url"
19
20
"os"
21
+ "os/exec"
20
22
"path"
21
23
"path/filepath"
22
24
"regexp"
@@ -80,7 +82,6 @@ type Args struct {
80
82
ctx context.Context
81
83
logger func (string , ... any )
82
84
bgc color.Color
83
- once sync.Once
84
85
}
85
86
86
87
// run renders the specified files to w.
@@ -191,12 +192,16 @@ func (args *Args) renderFile(name string) (image.Image, string, error) {
191
192
g = args .renderMarkdown
192
193
case isImageBuiltin (typ ):
193
194
g = args .renderImage
195
+ case isVipsImage (typ ): // use vips
196
+ g = args .renderVips
194
197
case strings .HasPrefix (typ , "font/" ):
195
198
g = args .renderFont
196
- case strings .HasPrefix (typ , "image/" ): // use vips
197
- g = args .renderVips
198
199
case strings .HasPrefix (typ , "video/" ):
199
200
g , notStream = args .renderAstiav , true
201
+ case isLibreOffice (typ ):
202
+ g , notStream = args .renderLibreOffice , true
203
+ default :
204
+ return nil , "" , fmt .Errorf ("mime type %q not supported" , typ )
200
205
}
201
206
if notStream {
202
207
if err := f .Close (); err != nil {
@@ -260,7 +265,7 @@ func (args *Args) renderFont(r io.Reader, name string) (image.Image, error) {
260
265
261
266
// renderVips opens a vips image from the reader.
262
267
func (args * Args ) renderVips (r io.Reader , name string ) (image.Image , error ) {
263
- args . once .Do (vipsInit (args .logger , args .Verbose , args .VipsConcurrency ))
268
+ vipsOnce .Do (vipsInit (args .logger , args .Verbose , args .VipsConcurrency ))
264
269
start := time .Now ()
265
270
buf , err := io .ReadAll (r )
266
271
if err != nil {
@@ -289,8 +294,68 @@ func (args *Args) renderVips(r io.Reader, name string) (image.Image, error) {
289
294
return args .vipsExport (v )
290
295
}
291
296
297
+ // renderAstiv renders the image using the astiav (ffmpeg) library.
292
298
func (args * Args ) renderAstiav (r io.Reader , name string ) (image.Image , error ) {
293
- return nil , fmt .Errorf ("not supported! %q" , name )
299
+ astiavOnce .Do (astiavInit (args .logger , args .Verbose ))
300
+ return nil , nil
301
+ }
302
+
303
+ // renderLibreOffice renders the image using the `soffice` command.
304
+ func (args * Args ) renderLibreOffice (_ io.Reader , pathName string ) (image.Image , error ) {
305
+ sofficeOnce .Do (func () {
306
+ sofficePath , _ = exec .LookPath ("soffice" )
307
+ })
308
+ if sofficePath == "" {
309
+ return nil , errors .New ("soffice not in path" )
310
+ }
311
+ tmpDir , err := os .MkdirTemp ("" , name + "." )
312
+ if err != nil {
313
+ return nil , err
314
+ }
315
+ args .logger ("temp dir: %s" , tmpDir )
316
+ params := []string {
317
+ `--headless` ,
318
+ `--convert-to` , `pdf` ,
319
+ `--outdir` , tmpDir ,
320
+ pathName ,
321
+ }
322
+ args .logger ("executing: %s %s" , sofficePath , strings .Join (params , " " ))
323
+ start := time .Now ()
324
+ cmd := exec .CommandContext (
325
+ args .ctx ,
326
+ sofficePath ,
327
+ params ... ,
328
+ )
329
+ buf , err := cmd .CombinedOutput ()
330
+ if err != nil {
331
+ if len (buf ) > 100 {
332
+ buf = buf [:100 ]
333
+ }
334
+ return nil , fmt .Errorf ("%w: %s" , err , string (buf ))
335
+ }
336
+ args .logger ("soffice render: %v" , time .Now ().Sub (start ))
337
+ pdf := filepath .Join (
338
+ tmpDir ,
339
+ strings .TrimSuffix (filepath .Base (pathName ), filepath .Ext (pathName ))+ ".pdf" ,
340
+ )
341
+ args .logger ("rendering soffice output: %q" , pdf )
342
+ f , err := os .OpenFile (pdf , os .O_RDONLY , 0 )
343
+ if err != nil {
344
+ return nil , err
345
+ }
346
+ img , err := args .renderVips (f , pdf )
347
+ if err != nil {
348
+ defer f .Close ()
349
+ return nil , err
350
+ }
351
+ if err := f .Close (); err != nil {
352
+ return nil , err
353
+ }
354
+ args .logger ("removing: %s" , tmpDir )
355
+ if err := os .RemoveAll (tmpDir ); err != nil {
356
+ return nil , err
357
+ }
358
+ return img , nil
294
359
}
295
360
296
361
// vipsExport exports the vips image as a png image.
@@ -397,7 +462,7 @@ func (args *Args) Write(buf []byte) (int, error) {
397
462
return len (buf ), nil
398
463
}
399
464
400
- // vipsInit initializes the vip package.
465
+ // vipsInit initializes the vips package.
401
466
func vipsInit (logger func (string , ... any ), verbose bool , concurrency int ) func () {
402
467
return func () {
403
468
start := time .Now ()
@@ -438,6 +503,12 @@ func vipsLevel(level vips.LogLevel) string {
438
503
return fmt .Sprintf ("(%d)" , level )
439
504
}
440
505
506
+ // astiavInit initializes the astiav package.
507
+ func astiavInit (logger func (string , ... any ), verbose bool ) func () {
508
+ return func () {
509
+ }
510
+ }
511
+
441
512
type files struct {
442
513
args * Args
443
514
}
@@ -564,4 +635,37 @@ func isImageBuiltin(typ string) bool {
564
635
return false
565
636
}
566
637
638
+ // isVipsImage returns true if the mime type is supported by libvips.
639
+ func isVipsImage (typ string ) bool {
640
+ switch typ {
641
+ case "application/pdf" :
642
+ return true
643
+ }
644
+ return strings .HasPrefix (typ , "image/" )
645
+ }
646
+
647
+ // isLibreOffice returns true if the mime type is supported by the `soffice`
648
+ // command.
649
+ func isLibreOffice (typ string ) bool {
650
+ switch {
651
+ case
652
+ strings .HasPrefix (typ , "application/vnd.openxmlformats-officedocument." ), // pptx, xlsx, ...
653
+ strings .HasPrefix (typ , "application/vnd.ms-" ), // ppt, xls, ...
654
+ strings .HasPrefix (typ , "application/vnd.oasis.opendocument." ), // otp, otp, odg, ...
655
+ typ == "text/rtf" ,
656
+ typ == "text/csv" ,
657
+ typ == "text/tab-separated-values" :
658
+ return true
659
+ }
660
+ return false
661
+ }
662
+
567
663
var urlRE = regexp .MustCompile (`(?i)^https?` )
664
+
665
+ var (
666
+ vipsOnce sync.Once
667
+ astiavOnce sync.Once
668
+ sofficeOnce sync.Once
669
+ )
670
+
671
+ var sofficePath string
0 commit comments