Skip to content

Commit 4298105

Browse files
committed
Adding support for office documents
1 parent 9cd4fcc commit 4298105

File tree

1 file changed

+110
-6
lines changed

1 file changed

+110
-6
lines changed

main.go

+110-6
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ package main
55
import (
66
"bytes"
77
"context"
8+
"errors"
89
"fmt"
910
"image"
1011
"image/color"
@@ -17,6 +18,7 @@ import (
1718
"net/http"
1819
"net/url"
1920
"os"
21+
"os/exec"
2022
"path"
2123
"path/filepath"
2224
"regexp"
@@ -80,7 +82,6 @@ type Args struct {
8082
ctx context.Context
8183
logger func(string, ...any)
8284
bgc color.Color
83-
once sync.Once
8485
}
8586

8687
// run renders the specified files to w.
@@ -191,12 +192,16 @@ func (args *Args) renderFile(name string) (image.Image, string, error) {
191192
g = args.renderMarkdown
192193
case isImageBuiltin(typ):
193194
g = args.renderImage
195+
case isVipsImage(typ): // use vips
196+
g = args.renderVips
194197
case strings.HasPrefix(typ, "font/"):
195198
g = args.renderFont
196-
case strings.HasPrefix(typ, "image/"): // use vips
197-
g = args.renderVips
198199
case strings.HasPrefix(typ, "video/"):
199200
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)
200205
}
201206
if notStream {
202207
if err := f.Close(); err != nil {
@@ -260,7 +265,7 @@ func (args *Args) renderFont(r io.Reader, name string) (image.Image, error) {
260265

261266
// renderVips opens a vips image from the reader.
262267
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))
264269
start := time.Now()
265270
buf, err := io.ReadAll(r)
266271
if err != nil {
@@ -289,8 +294,68 @@ func (args *Args) renderVips(r io.Reader, name string) (image.Image, error) {
289294
return args.vipsExport(v)
290295
}
291296

297+
// renderAstiv renders the image using the astiav (ffmpeg) library.
292298
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
294359
}
295360

296361
// vipsExport exports the vips image as a png image.
@@ -397,7 +462,7 @@ func (args *Args) Write(buf []byte) (int, error) {
397462
return len(buf), nil
398463
}
399464

400-
// vipsInit initializes the vip package.
465+
// vipsInit initializes the vips package.
401466
func vipsInit(logger func(string, ...any), verbose bool, concurrency int) func() {
402467
return func() {
403468
start := time.Now()
@@ -438,6 +503,12 @@ func vipsLevel(level vips.LogLevel) string {
438503
return fmt.Sprintf("(%d)", level)
439504
}
440505

506+
// astiavInit initializes the astiav package.
507+
func astiavInit(logger func(string, ...any), verbose bool) func() {
508+
return func() {
509+
}
510+
}
511+
441512
type files struct {
442513
args *Args
443514
}
@@ -564,4 +635,37 @@ func isImageBuiltin(typ string) bool {
564635
return false
565636
}
566637

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+
567663
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

Comments
 (0)