From bb91259829a048c7e4c2ec716cc5cb8d3344e2fd Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Fri, 27 Mar 2026 19:40:42 +0100 Subject: [PATCH] feat(libreoffice): add viewer preference form fields --- .bruno/LibreOffice/Convert to PDF.bru | 15 ++ pkg/modules/libreoffice/api/api.go | 75 ++++++++++ pkg/modules/libreoffice/api/libreoffice.go | 15 ++ pkg/modules/libreoffice/routes.go | 129 ++++++++++++++++++ .../features/libreoffice_convert.feature | 17 ++- 5 files changed, 250 insertions(+), 1 deletion(-) diff --git a/.bruno/LibreOffice/Convert to PDF.bru b/.bruno/LibreOffice/Convert to PDF.bru index 1e757cf1..2ca99670 100644 --- a/.bruno/LibreOffice/Convert to PDF.bru +++ b/.bruno/LibreOffice/Convert to PDF.bru @@ -31,6 +31,21 @@ body:multipart-form { ~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 diff --git a/pkg/modules/libreoffice/api/api.go b/pkg/modules/libreoffice/api/api.go index 396395f0..b2d4bdfd 100644 --- a/pkg/modules/libreoffice/api/api.go +++ b/pkg/modules/libreoffice/api/api.go @@ -140,6 +140,66 @@ type Options struct { // 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. @@ -208,6 +268,21 @@ func DefaultOptions() Options { 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, diff --git a/pkg/modules/libreoffice/api/libreoffice.go b/pkg/modules/libreoffice/api/libreoffice.go index 759f4872..8b945899 100644 --- a/pkg/modules/libreoffice/api/libreoffice.go +++ b/pkg/modules/libreoffice/api/libreoffice.go @@ -295,6 +295,21 @@ func (p *libreOfficeProcess) pdf(ctx context.Context, logger *slog.Logger, input args = append(args, "--export", fmt.Sprintf("IsSkipEmptyPages=%t", options.SkipEmptyPages)) args = append(args, "--export", fmt.Sprintf("IsAddStream=%t", options.AddOriginalDocumentAsStream)) args = append(args, "--export", fmt.Sprintf("SinglePageSheets=%t", options.SinglePageSheets)) + args = append(args, "--export", fmt.Sprintf("InitialView=%d", options.InitialView)) + args = append(args, "--export", fmt.Sprintf("InitialPage=%d", options.InitialPage)) + args = append(args, "--export", fmt.Sprintf("Magnification=%d", options.Magnification)) + args = append(args, "--export", fmt.Sprintf("Zoom=%d", options.Zoom)) + args = append(args, "--export", fmt.Sprintf("PageLayout=%d", options.PageLayout)) + args = append(args, "--export", fmt.Sprintf("FirstPageOnLeft=%t", options.FirstPageOnLeft)) + args = append(args, "--export", fmt.Sprintf("ResizeWindowToInitialPage=%t", options.ResizeWindowToInitialPage)) + args = append(args, "--export", fmt.Sprintf("CenterWindow=%t", options.CenterWindow)) + args = append(args, "--export", fmt.Sprintf("OpenInFullScreenMode=%t", options.OpenInFullScreenMode)) + args = append(args, "--export", fmt.Sprintf("DisplayPDFDocumentTitle=%t", options.DisplayPDFDocumentTitle)) + args = append(args, "--export", fmt.Sprintf("HideViewerMenubar=%t", options.HideViewerMenubar)) + args = append(args, "--export", fmt.Sprintf("HideViewerToolbar=%t", options.HideViewerToolbar)) + args = append(args, "--export", fmt.Sprintf("HideViewerWindowControls=%t", options.HideViewerWindowControls)) + args = append(args, "--export", fmt.Sprintf("UseTransitionEffects=%t", options.UseTransitionEffects)) + args = append(args, "--export", fmt.Sprintf("OpenBookmarkLevels=%d", options.OpenBookmarkLevels)) args = append(args, "--export", fmt.Sprintf("UseLosslessCompression=%t", options.LosslessImageCompression)) args = append(args, "--export", fmt.Sprintf("Quality=%d", options.Quality)) args = append(args, "--export", fmt.Sprintf("ReduceImageResolution=%t", options.ReduceImageResolution)) diff --git a/pkg/modules/libreoffice/routes.go b/pkg/modules/libreoffice/routes.go index 5fc20bf3..fcc0b331 100644 --- a/pkg/modules/libreoffice/routes.go +++ b/pkg/modules/libreoffice/routes.go @@ -67,6 +67,21 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap 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 @@ -97,6 +112,105 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap 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 == "" { @@ -227,6 +341,21 @@ func convertRoute(libreOffice libreofficeapi.Uno, engine gotenberg.PdfEngine) ap 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, diff --git a/test/integration/features/libreoffice_convert.feature b/test/integration/features/libreoffice_convert.feature index 7e1e6bd7..60c478bc 100644 --- a/test/integration/features/libreoffice_convert.feature +++ b/test/integration/features/libreoffice_convert.feature @@ -169,6 +169,21 @@ Feature: /forms/libreoffice/convert | skipEmptyPages | foo | field | | addOriginalDocumentAsStream | foo | field | | singlePageSheets | foo | field | + | initialView | 5 | field | + | initialPage | -1 | field | + | magnification | 9 | field | + | zoom | -1 | field | + | pageLayout | 7 | field | + | firstPageOnLeft | foo | field | + | resizeWindowToInitialPage | foo | field | + | centerWindow | foo | field | + | openInFullScreenMode | foo | field | + | displayPDFDocumentTitle | foo | field | + | hideViewerMenubar | foo | field | + | hideViewerToolbar | foo | field | + | hideViewerWindowControls | foo | field | + | useTransitionEffects | foo | field | + | openBookmarkLevels | 15 | field | | losslessImageCompression | foo | field | | quality | -1 | field | | reduceImageResolution | foo | field | @@ -177,7 +192,7 @@ Feature: /forms/libreoffice/convert Then the response header "Content-Type" should be "text/plain; charset=UTF-8" Then the response body should match string: """ - Invalid form data: no form file found for extensions: [.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]; form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportFormFields' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'allowDuplicateFieldNames' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportBookmarks' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportBookmarksToPdfDestination' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportPlaceholders' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotes' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotesPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportOnlyNotesPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotesInMargin' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'convertOooTargetToPdfTarget' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportLinksRelativeFsys' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportHiddenSlides' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'skipEmptyPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'addOriginalDocumentAsStream' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'singlePageSheets' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'losslessImageCompression' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'quality' is invalid (got '-1', resulting to value is inferior to 1); form field 'reduceImageResolution' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'maxImageResolution' is invalid (got '10', resulting to value is not 75, 150, 300, 600 or 1200) + Invalid form data: no form file found for extensions: [.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]; form field 'landscape' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportFormFields' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'allowDuplicateFieldNames' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportBookmarks' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportBookmarksToPdfDestination' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportPlaceholders' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotes' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotesPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportOnlyNotesPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportNotesInMargin' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'convertOooTargetToPdfTarget' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportLinksRelativeFsys' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'exportHiddenSlides' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'skipEmptyPages' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'addOriginalDocumentAsStream' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'singlePageSheets' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'initialView' is invalid (got '5', resulting to value is not 0, 1 or 2); form field 'initialPage' is invalid (got '-1', resulting to value is inferior to 1); form field 'magnification' is invalid (got '9', resulting to value is not 0, 1, 2, 3 or 4); form field 'zoom' is invalid (got '-1', resulting to value is inferior to 1); form field 'pageLayout' is invalid (got '7', resulting to value is not 0, 1, 2 or 3); form field 'firstPageOnLeft' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'resizeWindowToInitialPage' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'centerWindow' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'openInFullScreenMode' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'displayPDFDocumentTitle' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'hideViewerMenubar' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'hideViewerToolbar' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'hideViewerWindowControls' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'useTransitionEffects' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'openBookmarkLevels' is invalid (got '15', resulting to value is not -1 or between 1 and 10); form field 'losslessImageCompression' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'quality' is invalid (got '-1', resulting to value is inferior to 1); form field 'reduceImageResolution' is invalid (got 'foo', resulting to strconv.ParseBool: parsing "foo": invalid syntax); form field 'maxImageResolution' is invalid (got '10', resulting to value is not 75, 150, 300, 600 or 1200) """ When I make a "POST" request to Gotenberg at the "/forms/libreoffice/convert" endpoint with the following form data and header(s): | files | testdata/page_1.docx | file |