mirror of
https://github.com/gotenberg/gotenberg.git
synced 2026-07-02 00:17:40 +08:00
555 lines
20 KiB
Go
555 lines
20 KiB
Go
package libreoffice
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"slices"
|
|
"strconv"
|
|
|
|
"github.com/labstack/echo/v4"
|
|
|
|
"github.com/gotenberg/gotenberg/v8/pkg/gotenberg"
|
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/api"
|
|
libreofficeapi "github.com/gotenberg/gotenberg/v8/pkg/modules/libreoffice/api"
|
|
"github.com/gotenberg/gotenberg/v8/pkg/modules/pdfengines"
|
|
)
|
|
|
|
// convertRoute returns an [api.Route] which can convert LibreOffice documents
|
|
// to PDF.
|
|
func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) api.Route {
|
|
return api.Route{
|
|
Method: http.MethodPost,
|
|
Path: "/forms/libreoffice/convert",
|
|
IsMultipart: true,
|
|
Handler: func(c echo.Context) error {
|
|
ctx := c.Get("context").(*api.Context)
|
|
defaultOptions := libreofficeapi.DefaultOptions()
|
|
|
|
form := ctx.FormData()
|
|
splitMode := pdfengines.FormDataPdfSplitMode(form, false)
|
|
pdfFormats := pdfengines.FormDataPdfFormats(form)
|
|
metadata := pdfengines.FormDataPdfMetadata(form, false)
|
|
encrypt := pdfengines.FormDataPdfEncrypt(form)
|
|
embedPaths := pdfengines.FormDataPdfEmbeds(form)
|
|
watermark := pdfengines.FormDataPdfWatermark(form, false)
|
|
watermarkFile := pdfengines.FormDataPdfWatermarkFile(form)
|
|
stamp := pdfengines.FormDataPdfStamp(form, false)
|
|
stampFile := pdfengines.FormDataPdfStampFile(form)
|
|
angle, rotatePages := pdfengines.FormDataPdfRotate(form, false)
|
|
embedsMetadata := pdfengines.FormDataPdfEmbedsMetadata(form)
|
|
facturX, facturxXmlPath := pdfengines.FormDataPdfFacturX(form)
|
|
|
|
zeroValuedSplitMode := gotenberg.SplitMode{}
|
|
|
|
var (
|
|
inputPaths []string
|
|
password string
|
|
landscape bool
|
|
nativePageRanges string
|
|
updateIndexes bool
|
|
exportFormFields bool
|
|
allowDuplicateFieldNames bool
|
|
exportBookmarks bool
|
|
exportBookmarksToPdfDestination bool
|
|
exportPlaceholders bool
|
|
exportNotes bool
|
|
exportNotesPages bool
|
|
exportOnlyNotesPages bool
|
|
exportNotesInMargin bool
|
|
convertOooTargetToPdfTarget bool
|
|
exportLinksRelativeFsys bool
|
|
exportHiddenSlides bool
|
|
skipEmptyPages bool
|
|
addOriginalDocumentAsStream bool
|
|
singlePageSheets bool
|
|
losslessImageCompression bool
|
|
quality int
|
|
reduceImageResolution bool
|
|
maxImageResolution int
|
|
nativeWatermarkText string
|
|
nativeWatermarkColor int
|
|
initialView int
|
|
initialPage int
|
|
magnification int
|
|
zoom int
|
|
pageLayout int
|
|
firstPageOnLeft bool
|
|
resizeWindowToInitialPage bool
|
|
centerWindow bool
|
|
openInFullScreenMode bool
|
|
displayPDFDocumentTitle bool
|
|
hideViewerMenubar bool
|
|
hideViewerToolbar bool
|
|
hideViewerWindowControls bool
|
|
useTransitionEffects bool
|
|
openBookmarkLevels int
|
|
nativeWatermarkFontHeight int
|
|
nativeWatermarkRotateAngle int
|
|
nativeWatermarkFontName string
|
|
nativeTiledWatermarkText string
|
|
nativePdfFormats bool
|
|
merge bool
|
|
flatten bool
|
|
)
|
|
|
|
err := form.
|
|
MandatoryPaths(libreOffice.Extensions(), &inputPaths).
|
|
String("password", &password, defaultOptions.Password).
|
|
Bool("landscape", &landscape, defaultOptions.Landscape).
|
|
String("nativePageRanges", &nativePageRanges, defaultOptions.PageRanges).
|
|
Bool("updateIndexes", &updateIndexes, defaultOptions.UpdateIndexes).
|
|
Bool("exportFormFields", &exportFormFields, defaultOptions.ExportFormFields).
|
|
Bool("allowDuplicateFieldNames", &allowDuplicateFieldNames, defaultOptions.AllowDuplicateFieldNames).
|
|
Bool("exportBookmarks", &exportBookmarks, defaultOptions.ExportBookmarks).
|
|
Bool("exportBookmarksToPdfDestination", &exportBookmarksToPdfDestination, defaultOptions.ExportBookmarksToPdfDestination).
|
|
Bool("exportPlaceholders", &exportPlaceholders, defaultOptions.ExportPlaceholders).
|
|
Bool("exportNotes", &exportNotes, defaultOptions.ExportNotes).
|
|
Bool("exportNotesPages", &exportNotesPages, defaultOptions.ExportNotesPages).
|
|
Bool("exportOnlyNotesPages", &exportOnlyNotesPages, defaultOptions.ExportOnlyNotesPages).
|
|
Bool("exportNotesInMargin", &exportNotesInMargin, defaultOptions.ExportNotesInMargin).
|
|
Bool("convertOooTargetToPdfTarget", &convertOooTargetToPdfTarget, defaultOptions.ConvertOooTargetToPdfTarget).
|
|
Bool("exportLinksRelativeFsys", &exportLinksRelativeFsys, defaultOptions.ExportLinksRelativeFsys).
|
|
Bool("exportHiddenSlides", &exportHiddenSlides, defaultOptions.ExportHiddenSlides).
|
|
Bool("skipEmptyPages", &skipEmptyPages, defaultOptions.SkipEmptyPages).
|
|
Bool("addOriginalDocumentAsStream", &addOriginalDocumentAsStream, defaultOptions.AddOriginalDocumentAsStream).
|
|
Bool("singlePageSheets", &singlePageSheets, defaultOptions.SinglePageSheets).
|
|
Custom("initialView", func(value string) error {
|
|
if value == "" {
|
|
initialView = defaultOptions.InitialView
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains([]int{0, 1, 2}, intValue) {
|
|
return errors.New("value is not 0, 1 or 2")
|
|
}
|
|
initialView = intValue
|
|
return nil
|
|
}).
|
|
Custom("initialPage", func(value string) error {
|
|
if value == "" {
|
|
initialPage = defaultOptions.InitialPage
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if intValue < 1 {
|
|
return errors.New("value is inferior to 1")
|
|
}
|
|
initialPage = intValue
|
|
return nil
|
|
}).
|
|
Custom("magnification", func(value string) error {
|
|
if value == "" {
|
|
magnification = defaultOptions.Magnification
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains([]int{0, 1, 2, 3, 4}, intValue) {
|
|
return errors.New("value is not 0, 1, 2, 3 or 4")
|
|
}
|
|
magnification = intValue
|
|
return nil
|
|
}).
|
|
Custom("zoom", func(value string) error {
|
|
if value == "" {
|
|
zoom = defaultOptions.Zoom
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if intValue < 1 {
|
|
return errors.New("value is inferior to 1")
|
|
}
|
|
zoom = intValue
|
|
return nil
|
|
}).
|
|
Custom("pageLayout", func(value string) error {
|
|
if value == "" {
|
|
pageLayout = defaultOptions.PageLayout
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if !slices.Contains([]int{0, 1, 2, 3}, intValue) {
|
|
return errors.New("value is not 0, 1, 2 or 3")
|
|
}
|
|
pageLayout = intValue
|
|
return nil
|
|
}).
|
|
Bool("firstPageOnLeft", &firstPageOnLeft, defaultOptions.FirstPageOnLeft).
|
|
Bool("resizeWindowToInitialPage", &resizeWindowToInitialPage, defaultOptions.ResizeWindowToInitialPage).
|
|
Bool("centerWindow", ¢erWindow, defaultOptions.CenterWindow).
|
|
Bool("openInFullScreenMode", &openInFullScreenMode, defaultOptions.OpenInFullScreenMode).
|
|
Bool("displayPDFDocumentTitle", &displayPDFDocumentTitle, defaultOptions.DisplayPDFDocumentTitle).
|
|
Bool("hideViewerMenubar", &hideViewerMenubar, defaultOptions.HideViewerMenubar).
|
|
Bool("hideViewerToolbar", &hideViewerToolbar, defaultOptions.HideViewerToolbar).
|
|
Bool("hideViewerWindowControls", &hideViewerWindowControls, defaultOptions.HideViewerWindowControls).
|
|
Bool("useTransitionEffects", &useTransitionEffects, defaultOptions.UseTransitionEffects).
|
|
Custom("openBookmarkLevels", func(value string) error {
|
|
if value == "" {
|
|
openBookmarkLevels = defaultOptions.OpenBookmarkLevels
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if intValue != -1 && (intValue < 1 || intValue > 10) {
|
|
return errors.New("value is not -1 or between 1 and 10")
|
|
}
|
|
openBookmarkLevels = intValue
|
|
return nil
|
|
}).
|
|
Bool("losslessImageCompression", &losslessImageCompression, defaultOptions.LosslessImageCompression).
|
|
Custom("quality", func(value string) error {
|
|
if value == "" {
|
|
quality = defaultOptions.Quality
|
|
return nil
|
|
}
|
|
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if intValue < 1 {
|
|
return errors.New("value is inferior to 1")
|
|
}
|
|
|
|
if intValue > 100 {
|
|
return errors.New("value is superior to 100")
|
|
}
|
|
|
|
quality = intValue
|
|
return nil
|
|
}).
|
|
Bool("reduceImageResolution", &reduceImageResolution, defaultOptions.ReduceImageResolution).
|
|
Custom("maxImageResolution", func(value string) error {
|
|
if value == "" {
|
|
maxImageResolution = defaultOptions.MaxImageResolution
|
|
return nil
|
|
}
|
|
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if !slices.Contains([]int{75, 150, 300, 600, 1200}, intValue) {
|
|
return errors.New("value is not 75, 150, 300, 600 or 1200")
|
|
}
|
|
|
|
maxImageResolution = intValue
|
|
return nil
|
|
}).
|
|
String("nativeWatermarkText", &nativeWatermarkText, defaultOptions.NativeWatermarkText).
|
|
Custom("nativeWatermarkColor", func(value string) error {
|
|
if value == "" {
|
|
nativeWatermarkColor = defaultOptions.NativeWatermarkColor
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nativeWatermarkColor = intValue
|
|
return nil
|
|
}).
|
|
Custom("nativeWatermarkFontHeight", func(value string) error {
|
|
if value == "" {
|
|
nativeWatermarkFontHeight = defaultOptions.NativeWatermarkFontHeight
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if intValue < 0 {
|
|
return errors.New("value is inferior to 0")
|
|
}
|
|
nativeWatermarkFontHeight = intValue
|
|
return nil
|
|
}).
|
|
Custom("nativeWatermarkRotateAngle", func(value string) error {
|
|
if value == "" {
|
|
nativeWatermarkRotateAngle = defaultOptions.NativeWatermarkRotateAngle
|
|
return nil
|
|
}
|
|
intValue, err := strconv.Atoi(value)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
nativeWatermarkRotateAngle = intValue
|
|
return nil
|
|
}).
|
|
String("nativeWatermarkFontName", &nativeWatermarkFontName, defaultOptions.NativeWatermarkFontName).
|
|
String("nativeTiledWatermarkText", &nativeTiledWatermarkText, defaultOptions.NativeTiledWatermarkText).
|
|
Bool("nativePdfFormats", &nativePdfFormats, true).
|
|
Bool("merge", &merge, false).
|
|
Bool("flatten", &flatten, false).
|
|
Validate()
|
|
if err != nil {
|
|
return fmt.Errorf("validate form data: %w", err)
|
|
}
|
|
|
|
err = pdfengines.EnsureWatermarkFile(&watermark, watermarkFile)
|
|
if err != nil {
|
|
return fmt.Errorf("validate watermark: %w", err)
|
|
}
|
|
err = pdfengines.EnsureStampFile(&stamp, stampFile)
|
|
if err != nil {
|
|
return fmt.Errorf("validate stamp: %w", err)
|
|
}
|
|
|
|
err = pdfengines.ValidatePdfFormatsCompat(pdfFormats, encrypt.UserPassword, embedPaths)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = pdfengines.ValidatePdfEncryptCompat(encrypt)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
err = pdfengines.ValidateFacturXCompat(facturX, facturxXmlPath, pdfFormats)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Factur-X requires PDF/A-3; default to PDF/A-3b when no format was
|
|
// requested. The conversion runs as a post-processing step below.
|
|
pdfFormats = pdfengines.FacturXPdfFormats(ctx, engine, facturX, pdfFormats, true, nil)
|
|
|
|
hasPostProcessing := watermark.Source != "" || stamp.Source != "" || angle != 0 ||
|
|
len(embedPaths) > 0 || len(metadata) > 0 || flatten || facturX.ConformanceLevel != ""
|
|
|
|
outputPaths := make([]string, len(inputPaths))
|
|
for i, inputPath := range inputPaths {
|
|
outputPaths[i] = ctx.GeneratePath(".pdf")
|
|
options := libreofficeapi.Options{
|
|
Password: password,
|
|
Landscape: landscape,
|
|
PageRanges: nativePageRanges,
|
|
UpdateIndexes: updateIndexes,
|
|
ExportFormFields: exportFormFields,
|
|
AllowDuplicateFieldNames: allowDuplicateFieldNames,
|
|
ExportBookmarks: exportBookmarks,
|
|
ExportBookmarksToPdfDestination: exportBookmarksToPdfDestination,
|
|
ExportPlaceholders: exportPlaceholders,
|
|
ExportNotes: exportNotes,
|
|
ExportNotesPages: exportNotesPages,
|
|
ExportOnlyNotesPages: exportOnlyNotesPages,
|
|
ExportNotesInMargin: exportNotesInMargin,
|
|
ConvertOooTargetToPdfTarget: convertOooTargetToPdfTarget,
|
|
ExportLinksRelativeFsys: exportLinksRelativeFsys,
|
|
ExportHiddenSlides: exportHiddenSlides,
|
|
SkipEmptyPages: skipEmptyPages,
|
|
AddOriginalDocumentAsStream: addOriginalDocumentAsStream,
|
|
SinglePageSheets: singlePageSheets,
|
|
InitialView: initialView,
|
|
InitialPage: initialPage,
|
|
Magnification: magnification,
|
|
Zoom: zoom,
|
|
PageLayout: pageLayout,
|
|
FirstPageOnLeft: firstPageOnLeft,
|
|
ResizeWindowToInitialPage: resizeWindowToInitialPage,
|
|
CenterWindow: centerWindow,
|
|
OpenInFullScreenMode: openInFullScreenMode,
|
|
DisplayPDFDocumentTitle: displayPDFDocumentTitle,
|
|
HideViewerMenubar: hideViewerMenubar,
|
|
HideViewerToolbar: hideViewerToolbar,
|
|
HideViewerWindowControls: hideViewerWindowControls,
|
|
UseTransitionEffects: useTransitionEffects,
|
|
OpenBookmarkLevels: openBookmarkLevels,
|
|
LosslessImageCompression: losslessImageCompression,
|
|
Quality: quality,
|
|
ReduceImageResolution: reduceImageResolution,
|
|
MaxImageResolution: maxImageResolution,
|
|
NativeWatermarkText: nativeWatermarkText,
|
|
NativeWatermarkColor: nativeWatermarkColor,
|
|
NativeWatermarkFontHeight: nativeWatermarkFontHeight,
|
|
NativeWatermarkRotateAngle: nativeWatermarkRotateAngle,
|
|
NativeWatermarkFontName: nativeWatermarkFontName,
|
|
NativeTiledWatermarkText: nativeTiledWatermarkText,
|
|
}
|
|
|
|
if nativePdfFormats && splitMode == zeroValuedSplitMode && !hasPostProcessing {
|
|
// Only natively apply given PDF formats if we're not
|
|
// splitting the PDF later and no post-processing features
|
|
// are enabled (as they would degrade compliance).
|
|
options.PdfFormats = pdfFormats
|
|
}
|
|
|
|
err = libreOffice.Pdf(ctx, ctx.Log(), inputPath, outputPaths[i], options)
|
|
if err != nil {
|
|
if errors.Is(err, libreofficeapi.ErrInvalidPdfFormats) {
|
|
return api.WrapError(
|
|
fmt.Errorf("convert to PDF: %w", err),
|
|
api.NewSentinelHttpError(
|
|
http.StatusBadRequest,
|
|
fmt.Sprintf("The PDF format '%s' is not supported. Valid formats include PDF/A-1b, PDF/A-2b, PDF/A-3b, and PDF/UA.", pdfFormats.PdfA),
|
|
),
|
|
)
|
|
}
|
|
|
|
if errors.Is(err, libreofficeapi.ErrUnoException) {
|
|
return api.WrapError(
|
|
fmt.Errorf("convert to PDF: %w", err),
|
|
api.NewSentinelHttpError(http.StatusBadRequest, fmt.Sprintf("LibreOffice failed to process a document: possible causes include malformed page ranges '%s' (nativePageRanges), or, if a password has been provided, it may not be required. In any case, the exact cause is uncertain.", options.PageRanges)),
|
|
)
|
|
}
|
|
|
|
if errors.Is(err, libreofficeapi.ErrRuntimeException) {
|
|
return api.WrapError(
|
|
fmt.Errorf("convert to PDF: %w", err),
|
|
api.NewSentinelHttpError(http.StatusBadRequest, "LibreOffice failed to process a document: a password may be required, or, if one has been given, it is invalid. In any case, the exact cause is uncertain."),
|
|
)
|
|
}
|
|
|
|
return fmt.Errorf("convert to PDF: %w", err)
|
|
}
|
|
}
|
|
|
|
if merge {
|
|
outputPath, err := pdfengines.MergeStub(ctx, engine, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("merge PDFs: %w", err)
|
|
}
|
|
|
|
// Only one output path.
|
|
outputPaths = []string{outputPath}
|
|
}
|
|
|
|
if splitMode != zeroValuedSplitMode {
|
|
if !merge {
|
|
// document.docx -> document.docx.pdf, so that split naming
|
|
// document.docx_0.pdf, etc.
|
|
for i, inputPath := range inputPaths {
|
|
originalName := ctx.OriginalFilename(inputPath)
|
|
outputPath := ctx.GeneratePathFromFilename(originalName + ".pdf")
|
|
|
|
err = ctx.Rename(outputPaths[i], outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("rename output path: %w", err)
|
|
}
|
|
|
|
outputPaths[i] = outputPath
|
|
}
|
|
}
|
|
|
|
outputPaths, err = pdfengines.SplitPdfStub(ctx, engine, splitMode, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("split PDFs: %w", err)
|
|
}
|
|
}
|
|
|
|
err = pdfengines.WatermarkStub(ctx, engine, watermark, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("watermark PDFs: %w", err)
|
|
}
|
|
|
|
err = pdfengines.StampStub(ctx, engine, stamp, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("stamp PDFs: %w", err)
|
|
}
|
|
|
|
err = pdfengines.RotateStub(ctx, engine, angle, rotatePages, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("rotate PDFs: %w", err)
|
|
}
|
|
|
|
if flatten {
|
|
err = pdfengines.FlattenStub(ctx, engine, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("flatten PDFs: %w", err)
|
|
}
|
|
}
|
|
|
|
needsConvertStub := !nativePdfFormats ||
|
|
(nativePdfFormats && splitMode != zeroValuedSplitMode) ||
|
|
(nativePdfFormats && hasPostProcessing)
|
|
if needsConvertStub {
|
|
convertOutputPaths, err := pdfengines.ConvertStub(ctx, engine, pdfFormats, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("convert PDFs: %w", err)
|
|
}
|
|
|
|
if splitMode != zeroValuedSplitMode {
|
|
// The PDF has been split and split parts have been converted to
|
|
// specific formats. We want to keep the split naming.
|
|
for i, convertOutputPath := range convertOutputPaths {
|
|
err = ctx.Rename(convertOutputPath, outputPaths[i])
|
|
if err != nil {
|
|
return fmt.Errorf("rename output path: %w", err)
|
|
}
|
|
}
|
|
} else {
|
|
outputPaths = convertOutputPaths
|
|
}
|
|
}
|
|
|
|
// Metadata, embeds are written after Convert, as LibreOffice
|
|
// strips them during PDF/A conversion.
|
|
err = pdfengines.WriteMetadataStub(ctx, engine, metadata, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("write metadata: %w", err)
|
|
}
|
|
|
|
err = pdfengines.EmbedFilesStub(ctx, engine, embedPaths, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("embed files into PDFs: %w", err)
|
|
}
|
|
|
|
err = pdfengines.EmbedFilesMetadataStub(ctx, engine, embedsMetadata, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("set embeds metadata: %w", err)
|
|
}
|
|
|
|
err = pdfengines.ApplyFacturXStub(ctx, engine, facturX, facturxXmlPath, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("apply Factur-X: %w", err)
|
|
}
|
|
|
|
err = pdfengines.EncryptPdfStub(ctx, engine, encrypt, outputPaths)
|
|
if err != nil {
|
|
return fmt.Errorf("encrypt PDFs: %w", err)
|
|
}
|
|
|
|
if len(outputPaths) > 1 && splitMode == zeroValuedSplitMode {
|
|
// If .zip archive, document.docx -> document.docx.pdf.
|
|
for i, inputPath := range inputPaths {
|
|
originalName := ctx.OriginalFilename(inputPath)
|
|
outputPath := ctx.GeneratePathFromFilename(originalName + ".pdf")
|
|
|
|
err = ctx.Rename(outputPaths[i], outputPath)
|
|
if err != nil {
|
|
return fmt.Errorf("rename output path: %w", err)
|
|
}
|
|
|
|
outputPaths[i] = outputPath
|
|
}
|
|
}
|
|
|
|
err = ctx.AddOutputPaths(outputPaths...)
|
|
if err != nil {
|
|
return fmt.Errorf("add output paths: %w", err)
|
|
}
|
|
|
|
return nil
|
|
},
|
|
}
|
|
}
|