package api import ( "context" "errors" "fmt" "log/slog" "os" "os/exec" "strings" "sync" "syscall" "time" "github.com/alexliesenfeld/health" flag "github.com/spf13/pflag" "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" "go.opentelemetry.io/otel/metric" semconv "go.opentelemetry.io/otel/semconv/v1.40.0" "go.opentelemetry.io/otel/trace" "github.com/gotenberg/gotenberg/v8/pkg/gotenberg" "github.com/gotenberg/gotenberg/v8/pkg/modules/api" ) func init() { gotenberg.MustRegisterModule(new(Api)) } var ( // ErrInvalidPdfFormats happens if LibreOffice cannot handle the PDF // formats option. ErrInvalidPdfFormats = errors.New("invalid PDF formats") // ErrUnoException happens when unoconverter returns exit code 5. ErrUnoException = errors.New("uno exception") // ErrRuntimeException happens when unoconverter returns exit code 6. ErrRuntimeException = errors.New("runtime exception") // ErrCoreDumped happens randomly; sometimes a conversion will work as // expected, and some other time the same conversion will fail. // See https://github.com/gotenberg/gotenberg/issues/639. ErrCoreDumped = errors.New("core dumped") ) // Api is a module that provides a [Uno] to interact with LibreOffice. type Api struct { autoStart bool args libreOfficeArguments logger *slog.Logger libreOffice libreOffice supervisor gotenberg.ProcessSupervisor version string versionOnce sync.Once reqsCounter metric.Int64Counter errsCounter metric.Int64Counter conversionDurationCounter metric.Float64Histogram queueWaitDurationCounter metric.Float64Histogram pdfOutputSizeCounter metric.Int64Histogram coreDumpedRetriesCounter metric.Int64Counter } // Options gathers available options when converting a document to PDF. // See: https://help.libreoffice.org/latest/en-US/text/shared/guide/pdf_params.html. type Options struct { // Password specifies the password for opening the source file. Password string // #nosec // Landscape allows changing the orientation of the resulting PDF. Landscape bool // PageRanges allows selecting the pages to convert. PageRanges string // UpdateIndexes specifies whether to update the indexes before conversion, // keeping in mind that doing so might result in missing links in the final // PDF. UpdateIndexes bool // ExportFormFields specifies whether form fields are exported as widgets // or only their fixed print representation is exported. ExportFormFields bool // AllowDuplicateFieldNames specifies whether multiple form fields exported // are allowed to have the same field name. AllowDuplicateFieldNames bool // ExportBookmarks specifies if bookmarks are exported to PDF. ExportBookmarks bool // ExportBookmarksToPdfDestination specifies that the bookmarks contained // in the source LibreOffice file should be exported to the PDF file as // Named Destination. ExportBookmarksToPdfDestination bool // ExportPlaceholders exports the placeholder fields visual markings only. // The exported placeholder is ineffective. ExportPlaceholders bool // ExportNotes specifies if notes are exported to PDF. ExportNotes bool // ExportNotesPages specifies if notes pages are exported to PDF. // Notes pages are available in Impress documents only. ExportNotesPages bool // ExportOnlyNotesPages specifies if the property ExportNotesPages is set // to true if only notes pages are exported to PDF. ExportOnlyNotesPages bool // ExportNotesInMargin specifies if notes in the margin are exported to // PDF. ExportNotesInMargin bool // ConvertOooTargetToPdfTarget specifies that the target documents with // .od[tpgs] extension will have that extension changed to .pdf when the // link is exported to PDF. The source document remains untouched. ConvertOooTargetToPdfTarget bool // ExportLinksRelativeFsys specifies that the file system related // hyperlinks (file:// protocol) present in the document will be exported // as relative to the source document location. ExportLinksRelativeFsys bool // ExportHiddenSlides exports, for LibreOffice Impress, slides that are not // included in slide shows. ExportHiddenSlides bool // SkipEmptyPages specifies that automatically inserted empty pages are // suppressed. This option is active only if storing Writer documents. SkipEmptyPages bool // AddOriginalDocumentAsStream specifies that a stream is inserted to the // PDF file which contains the original document for archiving purposes. AddOriginalDocumentAsStream bool // SinglePageSheets ignores each sheet’s paper size, print ranges and // shown/hidden status and puts every sheet (even hidden sheets) on exactly // one page. SinglePageSheets bool // InitialView specifies how the PDF document should be displayed when // opened. 0 = neither outlines nor thumbnails, 1 = outline pane open, // 2 = thumbnail pane open. InitialView int // InitialPage specifies the page on which the PDF document should be // opened in the viewer. InitialPage int // Magnification specifies the action to be performed when the PDF document // is opened. 0 = default, 1 = fit entire page, 2 = fit page width, // 3 = fit visible, 4 = use zoom value from Zoom property. Magnification int // Zoom specifies the zoom level the PDF document is opened with. Only // used if Magnification is set to 4. Zoom int // PageLayout specifies the page layout when the document is opened. // 0 = default, 1 = single page, 2 = one column, 3 = two columns. PageLayout int // FirstPageOnLeft is used with PageLayout value 3. If true, the first // page is displayed on the left side. FirstPageOnLeft bool // ResizeWindowToInitialPage specifies that the PDF viewer window is // resized to show the whole initial page. ResizeWindowToInitialPage bool // CenterWindow specifies that the PDF viewer window is centered on the // screen. CenterWindow bool // OpenInFullScreenMode specifies that the PDF viewer window is opened // full screen. OpenInFullScreenMode bool // DisplayPDFDocumentTitle specifies that the document title is displayed // in the PDF viewer title bar. DisplayPDFDocumentTitle bool // HideViewerMenubar specifies whether to hide the PDF viewer menubar. HideViewerMenubar bool // HideViewerToolbar specifies whether to hide the PDF viewer toolbar. HideViewerToolbar bool // HideViewerWindowControls specifies whether to hide the PDF viewer // window controls. HideViewerWindowControls bool // UseTransitionEffects specifies that slide transitions are exported to // PDF. Only active for Impress documents. UseTransitionEffects bool // OpenBookmarkLevels specifies how many bookmark levels should be opened // in the reader. -1 = all levels, 1-10 = specific level. OpenBookmarkLevels int // LosslessImageCompression specifies if images are exported to PDF using // a lossless compression format like PNG or compressed using the JPEG // format. LosslessImageCompression bool // Quality specifies the quality of the JPG export. A higher value produces // a higher-quality image and a larger file. Between 1 and 100. Quality int // ReduceImageResolution specifies if the resolution of each image is // reduced to the resolution specified by the property MaxImageResolution. ReduceImageResolution bool // MaxImageResolution, if the property ReduceImageResolution is set to // true, tells if all images will be reduced to the given value in DPI. // Possible values are: 75, 150, 300, 600 and 1200. MaxImageResolution int // NativeWatermarkText specifies the text for a watermark to be drawn on // every page of the exported PDF file. // See https://help.libreoffice.org/latest/en-US/text/shared/guide/pdf_params.html. NativeWatermarkText string // NativeWatermarkColor specifies the color for the watermark text as a // decimal long value. Default is 8388223 (light green). NativeWatermarkColor int // NativeWatermarkFontHeight specifies the font size for the watermark text. NativeWatermarkFontHeight int // NativeWatermarkRotateAngle specifies the rotation angle for the watermark // text in tenths of a degree (e.g., 450 = 45°). NativeWatermarkRotateAngle int // NativeWatermarkFontName specifies the font name for the watermark text. // Default is "Helvetica". NativeWatermarkFontName string // NativeTiledWatermarkText specifies the tiled watermark text. NativeTiledWatermarkText string // PdfFormats allows to convert the resulting PDF to PDF/A-1b, PDF/A-2b, // PDF/A-3b and PDF/UA. PdfFormats gotenberg.PdfFormats } // DefaultOptions returns the default values for Options. func DefaultOptions() Options { return Options{ Password: "", Landscape: false, PageRanges: "", UpdateIndexes: true, ExportFormFields: true, AllowDuplicateFieldNames: false, ExportBookmarks: true, ExportBookmarksToPdfDestination: false, ExportPlaceholders: false, ExportNotes: false, ExportNotesPages: false, ExportOnlyNotesPages: false, ExportNotesInMargin: false, ConvertOooTargetToPdfTarget: false, ExportLinksRelativeFsys: false, ExportHiddenSlides: false, SkipEmptyPages: false, AddOriginalDocumentAsStream: false, SinglePageSheets: false, InitialView: 0, InitialPage: 1, Magnification: 0, Zoom: 100, PageLayout: 0, FirstPageOnLeft: false, ResizeWindowToInitialPage: false, CenterWindow: false, OpenInFullScreenMode: false, DisplayPDFDocumentTitle: true, HideViewerMenubar: false, HideViewerToolbar: false, HideViewerWindowControls: false, UseTransitionEffects: true, OpenBookmarkLevels: -1, LosslessImageCompression: false, Quality: 90, ReduceImageResolution: false, MaxImageResolution: 300, NativeWatermarkText: "", NativeWatermarkColor: 8388223, NativeWatermarkFontHeight: 0, NativeWatermarkRotateAngle: 0, NativeWatermarkFontName: "Helvetica", NativeTiledWatermarkText: "", PdfFormats: gotenberg.PdfFormats{ PdfA: "", PdfUa: false, }, } } // Uno is an abstraction on top of the Universal Network Objects API. type Uno interface { Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error Extensions() []string } // Provider is a module interface that exposes a method for creating a // [Uno] for other modules. // // func (m *YourModule) Provision(ctx *gotenberg.Context) error { // provider, _ := ctx.Module(new(libreofficeapi.Provider)) // libreOffice, _ := provider.(api.Provider).LibreOffice() // } type Provider interface { LibreOffice() (Uno, error) } // Descriptor returns a [Api]'s module descriptor. func (a *Api) Descriptor() gotenberg.ModuleDescriptor { return gotenberg.ModuleDescriptor{ ID: "libreoffice-api", FlagSet: func() *flag.FlagSet { fs := flag.NewFlagSet("api", flag.ExitOnError) fs.Int64("libreoffice-restart-after", 10, "Number of conversions after which LibreOffice will automatically restart. Set to 0 to disable this feature") fs.Int64("libreoffice-max-queue-size", 0, "Maximum request queue size for LibreOffice. Set to 0 to disable this feature") fs.Duration("libreoffice-idle-shutdown-timeout", 0, "Shutdown LibreOffice after being idle for the given duration. Set to 0 to disable this feature") fs.Bool("libreoffice-auto-start", false, "Automatically launch LibreOffice upon initialization if set to true; otherwise, LibreOffice will start at the time of the first conversion") fs.Duration("libreoffice-start-timeout", time.Duration(20)*time.Second, "Maximum duration to wait for LibreOffice to start or restart") fs.StringSlice("libreoffice-allow-list", []string{}, "Set the allowed URLs for LibreOffice outbound fetches (embedded images, linked content) using regular expressions - supports multiple values") fs.StringSlice("libreoffice-deny-list", []string{}, "Set the denied URLs for LibreOffice outbound fetches using regular expressions - supports multiple values") fs.Bool("libreoffice-deny-private-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a non-public IP address (loopback, RFC1918, link-local, unique-local). Enable on deployments that accept untrusted documents to mitigate SSRF against internal services") fs.Bool("libreoffice-deny-public-ips", false, "Reject LibreOffice outbound URLs whose host resolves to a public IP address. Enable on air-gapped or data-governed deployments to prevent outbound traffic from leaving a private network") return fs }(), New: func() gotenberg.Module { return new(Api) }, } } // Provision sets the module properties. func (a *Api) Provision(ctx *gotenberg.Context) error { flags := ctx.ParsedFlags() a.autoStart = flags.MustBool("libreoffice-auto-start") libreOfficeBinPath, ok := os.LookupEnv("LIBREOFFICE_BIN_PATH") if !ok { return errors.New("LIBREOFFICE_BIN_PATH environment variable is not set") } unoBinPath, ok := os.LookupEnv("UNOCONVERTER_BIN_PATH") if !ok { return errors.New("UNOCONVERTER_BIN_PATH environment variable is not set") } a.args = libreOfficeArguments{ binPath: libreOfficeBinPath, unoBinPath: unoBinPath, startTimeout: flags.MustDuration("libreoffice-start-timeout"), proxyOptions: outboundProxyOptions{ allowList: flags.MustRegexpSlice("libreoffice-allow-list"), denyList: flags.MustRegexpSlice("libreoffice-deny-list"), denyPrivateIPs: flags.MustBool("libreoffice-deny-private-ips"), denyPublicIPs: flags.MustBool("libreoffice-deny-public-ips"), }, } // Logger. a.logger = gotenberg.Logger(a).With(slog.String("logger", "libreoffice")) // Process. a.libreOffice = newLibreOfficeProcess(a.args) a.supervisor = gotenberg.NewProcessSupervisor(a.logger, "libreoffice", a.libreOffice, flags.MustInt64("libreoffice-restart-after"), flags.MustInt64("libreoffice-max-queue-size"), 1, flags.MustDuration("libreoffice-idle-shutdown-timeout")) // Metrics. meter := gotenberg.Meter() // Observable gauges. var err error _, err = meter.Int64ObservableGauge( "libreoffice.requests.active", metric.WithDescription("Current number of active LibreOffice requests"), metric.WithUnit("{request}"), metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error { o.Observe(a.supervisor.ActiveTasksCount()) return nil }), ) if err != nil { return fmt.Errorf("create libreoffice.requests.active gauge: %w", err) } _, err = meter.Int64ObservableGauge( "libreoffice.requests.queue_size", metric.WithDescription("Current number of LibreOffice conversion requests waiting to be treated"), metric.WithUnit("{request}"), metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error { o.Observe(a.supervisor.ReqQueueSize()) return nil }), ) if err != nil { return fmt.Errorf("create libreoffice.requests.queue_size gauge: %w", err) } _, err = meter.Int64ObservableCounter( "libreoffice.process.restarts.total", metric.WithDescription("Current number of LibreOffice restarts"), metric.WithUnit("{restart}"), metric.WithInt64Callback(func(_ context.Context, o metric.Int64Observer) error { o.Observe(a.supervisor.RestartsCount()) return nil }), ) if err != nil { return fmt.Errorf("create libreoffice.process.restarts.total counter: %w", err) } // Counters. a.reqsCounter, err = meter.Int64Counter( "libreoffice.requests.total", metric.WithDescription("Total number of LibreOffice conversion requests"), metric.WithUnit("{request}"), ) if err != nil { return fmt.Errorf("create libreoffice.requests.total counter: %w", err) } a.errsCounter, err = meter.Int64Counter( "libreoffice.errors.total", metric.WithDescription("Total number of LibreOffice conversion errors"), metric.WithUnit("{error}"), ) if err != nil { return fmt.Errorf("create libreoffice.errors.total counter: %w", err) } // Histograms. durationBuckets := metric.WithExplicitBucketBoundaries(0.5, 1, 2, 5, 10, 30, 60) a.conversionDurationCounter, err = meter.Float64Histogram( "libreoffice.conversion.duration", metric.WithDescription("Duration of LibreOffice conversions"), metric.WithUnit("s"), durationBuckets, ) if err != nil { return fmt.Errorf("create libreoffice.conversion.duration histogram: %w", err) } a.queueWaitDurationCounter, err = meter.Float64Histogram( "libreoffice.queue.wait.duration", metric.WithDescription("Duration of waiting in queue for LibreOffice conversions"), metric.WithUnit("s"), durationBuckets, ) if err != nil { return fmt.Errorf("create libreoffice.queue.wait.duration histogram: %w", err) } a.pdfOutputSizeCounter, err = meter.Int64Histogram( "libreoffice.pdf.output.size", metric.WithDescription("Size of PDF output from LibreOffice conversions"), metric.WithUnit("By"), ) if err != nil { return fmt.Errorf("create libreoffice.pdf.output.size histogram: %w", err) } a.coreDumpedRetriesCounter, err = meter.Int64Counter( "libreoffice.conversion.retries.total", metric.WithDescription("Total number of LibreOffice conversion retries after a core dump"), metric.WithUnit("{retry}"), ) if err != nil { return fmt.Errorf("create libreoffice.conversion.retries.total counter: %w", err) } return nil } // Validate validates the module properties. func (a *Api) Validate() error { var err error _, statErr := os.Stat(a.args.binPath) if os.IsNotExist(statErr) { err = errors.Join(err, fmt.Errorf("LibreOffice binary does not exist at %q; check the LIBREOFFICE_BIN_PATH environment variable: %w", a.args.binPath, statErr)) } _, statErr = os.Stat(a.args.unoBinPath) if os.IsNotExist(statErr) { err = errors.Join(err, fmt.Errorf("unoconverter binary does not exist at %q; check the UNOCONVERTER_BIN_PATH environment variable: %w", a.args.unoBinPath, statErr)) } return err } // Start does nothing if auto-start is not enabled. Otherwise, it starts a // LibreOffice instance. func (a *Api) Start() error { if !a.autoStart { return nil } err := a.supervisor.Launch() if err != nil { return fmt.Errorf("launch supervisor: %w", err) } return nil } // StartupMessage returns a custom startup message. func (a *Api) StartupMessage() string { if !a.autoStart { return "LibreOffice ready to start" } return "LibreOffice automatically started" } // Stop stops the current browser instance. func (a *Api) Stop(ctx context.Context) error { // Block until the context is done so that another module may gracefully // stop before we do a shutdown. a.logger.DebugContext(ctx, "wait for the end of grace duration") <-ctx.Done() err := a.supervisor.Shutdown() if err == nil { return nil } return fmt.Errorf("stop LibreOffice: %w", err) } // Debug returns additional debug data. func (a *Api) Debug() map[string]any { return map[string]any{"version": a.detectVersion()} } // detectVersion resolves the LibreOffice version once, preferring the value // captured at image build time so it never spawns LibreOffice at runtime. It // falls back to running soffice --version for local or non-Docker builds. func (a *Api) detectVersion() string { a.versionOnce.Do(func() { if v, ok := gotenberg.BuildVersion("libreoffice-api"); ok { a.version = v return } cmd := exec.Command(a.args.binPath, "--version") //nolint:gosec cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true} output, err := cmd.Output() if err != nil { a.version = err.Error() return } a.version = strings.TrimSpace(string(output)) }) return a.version } // spanAttrs returns the client-span attributes for a LibreOffice invocation: // the server address and the LibreOffice version, plus any extra attributes. // The version rides on every conversion span so a trace records which // LibreOffice rendered the document. func (a *Api) spanAttrs(extra ...attribute.KeyValue) []attribute.KeyValue { attrs := make([]attribute.KeyValue, 0, 2+len(extra)) attrs = append(attrs, semconv.ServerAddress(a.args.binPath)) if v := a.detectVersion(); v != "" { attrs = append(attrs, attribute.String("gotenberg.libreoffice.version", v)) } return append(attrs, extra...) } // Metrics returns the metrics. func (a *Api) Metrics() ([]gotenberg.Metric, error) { return []gotenberg.Metric{ { Name: "libreoffice_requests_queue_size", Description: "Current number of LibreOffice conversion requests waiting to be treated.", Read: func() float64 { return float64(a.supervisor.ReqQueueSize()) }, }, { Name: "libreoffice_restarts_count", Description: "Current number of LibreOffice restarts.", Read: func() float64 { return float64(a.supervisor.RestartsCount()) }, }, }, nil } // Checks adds a health check that verifies if LibreOffice is healthy. func (a *Api) Checks() ([]health.CheckerOption, error) { return []health.CheckerOption{ health.WithCheck(health.Check{ Name: "libreoffice", Check: func(_ context.Context) error { if a.supervisor.Healthy() { return nil } return errors.New("LibreOffice is unhealthy") }, }), }, nil } // Ready returns no error if the module is ready. func (a *Api) Ready() error { if !a.autoStart { return nil } ctx, cancel := context.WithTimeout(context.Background(), a.args.startTimeout) defer cancel() ticker := time.NewTicker(time.Duration(100) * time.Millisecond) for { select { case <-ctx.Done(): ticker.Stop() return fmt.Errorf("context done while waiting for LibreOffice to be ready: %w", ctx.Err()) case <-ticker.C: ok := a.libreOffice.Healthy(a.logger) if ok { ticker.Stop() return nil } continue } } } // LibreOffice returns a [Uno] for interacting with LibreOffice. func (a *Api) LibreOffice() (Uno, error) { return a, nil } // Pdf converts a document to PDF. func (a *Api) Pdf(ctx context.Context, logger *slog.Logger, inputPath, outputPath string, options Options) error { ctx, span := gotenberg.Tracer().Start(ctx, "libreoffice.Pdf", trace.WithSpanKind(trace.SpanKindClient), trace.WithAttributes(a.spanAttrs()...), ) defer span.End() span.SetAttributes( attribute.Int64("gotenberg.queue.depth_at_arrival", a.supervisor.ReqQueueSize()), attribute.Int64("gotenberg.conversions_since_last_restart", a.supervisor.ConversionsSinceRestart()), ) span.SetAttributes(conversionRequestAttributes(inputPath, options)...) // ErrCoreDumped happens randomly (https://github.com/gotenberg/gotenberg/issues/639); // retry the conversion, but cap the retries so a permanently failing // document cannot loop forever. Each attempt records its own metrics. const maxCoreDumpedRetries = 10 var err error var reason string for attempt := 0; ; attempt++ { start := time.Now() var conversionStart time.Time err = a.supervisor.Run(ctx, logger, func() error { conversionStart = time.Now() return a.libreOffice.pdf(ctx, logger, inputPath, outputPath, options) }) // Determine status and error reason. status := "success" reason = "" if err != nil { status = "error" if errors.Is(err, context.DeadlineExceeded) { status = "timeout" } reason = libreofficeErrorType(err) } // Record metrics for this attempt. attrs := metric.WithAttributes(attribute.String("status", status)) a.reqsCounter.Add(ctx, 1, attrs) if reason != "" { a.errsCounter.Add(ctx, 1, metric.WithAttributes(attribute.String("reason", reason))) } if !conversionStart.IsZero() { queueWait := conversionStart.Sub(start).Seconds() a.queueWaitDurationCounter.Record(ctx, queueWait, attrs) conversionDuration := time.Since(conversionStart).Seconds() a.conversionDurationCounter.Record(ctx, conversionDuration, attrs) } if err == nil { stat, statErr := os.Stat(outputPath) if statErr == nil { a.pdfOutputSizeCounter.Record(ctx, stat.Size(), attrs) span.SetAttributes(attribute.Int64("gotenberg.conversion.output.bytes", stat.Size())) } span.SetStatus(codes.Ok, "") return nil } if errors.Is(err, ErrCoreDumped) && attempt < maxCoreDumpedRetries { logger.DebugContext(ctx, fmt.Sprintf("got a '%s' error, retry conversion (attempt %d)", err, attempt+1)) span.AddEvent("conversion.retry", trace.WithAttributes( attribute.Int("attempt", attempt+1), )) a.coreDumpedRetriesCounter.Add(ctx, 1) continue } break } gotenberg.SpanErrorType(span, reason) span.RecordError(err) span.SetStatus(codes.Error, err.Error()) return fmt.Errorf("supervisor run task: %w", err) } // conversionRequestAttributes derives low-cardinality attributes describing the // requested conversion: the input document size and the requested PDF format // options. func conversionRequestAttributes(inputPath string, options Options) []attribute.KeyValue { attrs := []attribute.KeyValue{ attribute.String("gotenberg.libreoffice.pdf_a", options.PdfFormats.PdfA), attribute.Bool("gotenberg.libreoffice.pdf_ua", options.PdfFormats.PdfUa), attribute.Bool("gotenberg.conversion.landscape", options.Landscape), attribute.Bool("gotenberg.conversion.has_page_ranges", options.PageRanges != ""), } if info, err := os.Stat(inputPath); err == nil { attrs = append(attrs, attribute.Int64("gotenberg.conversion.input.bytes", info.Size())) } return attrs } // libreofficeErrorType maps a conversion error to LibreOffice's bounded reason // value, reused as the span error.type. Generic failures fall back to // [gotenberg.ClassifyError]. func libreofficeErrorType(err error) string { switch { case errors.Is(err, ErrInvalidPdfFormats): return gotenberg.ErrorTypeInvalidInput case errors.Is(err, ErrUnoException), errors.Is(err, ErrRuntimeException): return "libreoffice_exception" case errors.Is(err, gotenberg.ErrMaximumQueueSizeExceeded), errors.Is(err, gotenberg.ErrProcessAlreadyRestarting): return "libreoffice_unavailable" default: return gotenberg.ClassifyError(err) } } // Extensions returns the file extensions available for conversions. // FIXME: don't care, take all on the route level? func (a *Api) Extensions() []string { return []string{ ".123", ".602", ".abw", ".bib", ".bmp", ".cdr", ".cgm", ".cmx", ".csv", ".cwk", ".dbf", ".dif", ".doc", ".docm", ".docx", ".dot", ".dotm", ".dotx", ".dxf", ".emf", ".eps", ".epub", ".fodg", ".fodp", ".fods", ".fodt", ".fopd", ".gif", ".htm", ".html", ".hwp", ".jpeg", ".jpg", ".key", ".ltx", ".lwp", ".mcw", ".met", ".mml", ".mw", ".numbers", ".odd", ".odg", ".odm", ".odp", ".ods", ".odt", ".otg", ".oth", ".otp", ".ots", ".ott", ".pages", ".pbm", ".pcd", ".pct", ".pcx", ".pdb", ".pdf", ".pgm", ".png", ".pot", ".potm", ".potx", ".ppm", ".pps", ".ppt", ".pptm", ".pptx", ".psd", ".psw", ".pub", ".pwp", ".pxl", ".ras", ".rtf", ".sda", ".sdc", ".sdd", ".sdp", ".sdw", ".sgl", ".slk", ".smf", ".stc", ".std", ".sti", ".stw", ".svg", ".svm", ".swf", ".sxc", ".sxd", ".sxg", ".sxi", ".sxm", ".sxw", ".tga", ".tif", ".tiff", ".txt", ".uof", ".uop", ".uos", ".uot", ".vdx", ".vor", ".vsd", ".vsdm", ".vsdx", ".wb2", ".wk1", ".wks", ".wmf", ".wpd", ".wpg", ".wps", ".xbm", ".xhtml", ".xls", ".xlsb", ".xlsm", ".xlsx", ".xlt", ".xltm", ".xltx", ".xlw", ".xml", ".xpm", ".zabw", } } // Interface guards. var ( _ gotenberg.Module = (*Api)(nil) _ gotenberg.Provisioner = (*Api)(nil) _ gotenberg.Validator = (*Api)(nil) _ gotenberg.App = (*Api)(nil) _ gotenberg.Debuggable = (*Api)(nil) _ gotenberg.MetricsProvider = (*Api)(nil) _ api.HealthChecker = (*Api)(nil) _ Uno = (*Api)(nil) _ Provider = (*Api)(nil) )