From eb054146d27aadbe8824e2f11c39bfaf030ecdbd Mon Sep 17 00:00:00 2001 From: Julien Neuhart Date: Sun, 17 Mar 2024 14:34:28 +0100 Subject: [PATCH] feat(chromium): support more units for paper size and margins --- pkg/modules/api/formdata.go | 86 ++++++++ pkg/modules/api/formdata_test.go | 328 +++++++++++++++++++++++++++++++ pkg/modules/chromium/routes.go | 12 +- 3 files changed, 420 insertions(+), 6 deletions(-) diff --git a/pkg/modules/api/formdata.go b/pkg/modules/api/formdata.go index d07c14c9..5483415d 100644 --- a/pkg/modules/api/formdata.go +++ b/pkg/modules/api/formdata.go @@ -2,6 +2,7 @@ package api import ( "fmt" + "math" "net/http" "os" "path/filepath" @@ -145,6 +146,91 @@ func (form *FormData) MandatoryDuration(key string, target *time.Duration) *Form return form.mustMandatoryField(key, target) } +// Inches binds a form field to a float64 variable. It populates an error +// if the value cannot be computed back to inches. +// +// var foo float64 +// +// ctx.FormData().Inches("foo", &foo, 2.0) +func (form *FormData) Inches(key string, target *float64, defaultValue float64) *FormData { + form.inches(key, target) + if *target == -math.MaxFloat64 { + *target = defaultValue + } + return form +} + +// MandatoryInches binds a form field to a float64 variable. It populates +// an error if the value cannot be computed back to inches, is empty, or the +// "key" does not exist. +// +// var foo float64 +// +// ctx.FormData().MandatoryInches("foo", &foo) +func (form *FormData) MandatoryInches(key string, target *float64) *FormData { + val, ok := form.values[key] + if !ok || val[0] == "" { + form.append( + fmt.Errorf("form field '%s' is required", key), + ) + return form + } + return form.inches(key, target) +} + +// inches tries to compute a string value to inches. +func (form *FormData) inches(key string, target *float64) *FormData { + var value string + form.mustValue(key, &value, "") + + if value == "" { + *target = -math.MaxFloat64 + return form + } + + for _, unit := range []string{"pt", "px", "in", "mm", "cm", "pc"} { + if !strings.HasSuffix(value, unit) { + continue + } + + val, err := strconv.ParseFloat(strings.TrimSuffix(value, unit), 64) + if err != nil { + form.append( + fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err), + ) + return form + } + + switch unit { + case "pt": + *target = val * (1.0 / 72.0) + case "px": + *target = val * (1.0 / 96.0) + case "in": + *target = val + case "mm": + *target = val * (1.0 / 25.4) + case "cm": + *target = val * (1.0 / 2.54) + case "pc": + *target = val * (1.0 / 6.0) + } + + return form + } + + val, err := strconv.ParseFloat(value, 64) + if err != nil { + form.append( + fmt.Errorf("form field '%s' is invalid (got '%s', resulting to %w)", key, value, err), + ) + return form + } + + *target = val + return form +} + // Custom helps to define a custom binding function for a form field. // // var foo map[string]string diff --git a/pkg/modules/api/formdata_test.go b/pkg/modules/api/formdata_test.go index c11706cd..5ebd0b2f 100644 --- a/pkg/modules/api/formdata_test.go +++ b/pkg/modules/api/formdata_test.go @@ -3,6 +3,7 @@ package api import ( "encoding/json" "errors" + "fmt" "reflect" "testing" "time" @@ -768,6 +769,333 @@ func TestFormData_MandatoryDuration(t *testing.T) { } } +func TestFormData_Inches(t *testing.T) { + for _, tc := range []struct { + scenario string + form *FormData + defaultValue float64 + expect float64 + expectError bool + }{ + { + scenario: "key does not exist, fallback to default zero value", + form: &FormData{}, + defaultValue: 0.0, + expect: 0.0, + expectError: false, + }, + { + scenario: "key does not exist, fallback to default value", + form: &FormData{}, + defaultValue: 2.5, + expect: 2.5, + expectError: false, + }, + { + scenario: "key does exist, but empty value, fallback to default value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "", + }, + }, + }, + defaultValue: 0.0, + expect: 0.0, + expectError: false, + }, + { + scenario: "key does exist, value has a unit, but the rest is not float64 compatible", + form: &FormData{ + values: map[string][]string{ + "foo": { + "foomm", + }, + }, + }, + defaultValue: 0.0, + expect: 0.0, + expectError: true, + }, + { + scenario: "key does exist, but value has no unit and is invalid", + form: &FormData{ + values: map[string][]string{ + "foo": { + "foo", + }, + }, + }, + defaultValue: 0.0, + expect: 0.0, + expectError: true, + }, + { + scenario: "key does exist with a pt value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "72pt", + }, + }, + }, + defaultValue: 0.0, + expect: 1.0, + expectError: false, + }, + { + scenario: "key does exist with a px value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "96px", + }, + }, + }, + defaultValue: 0.0, + expect: 1.0, + expectError: false, + }, + { + scenario: "key does exist with an in value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "1in", + }, + }, + }, + defaultValue: 0.0, + expect: 1.0, + expectError: false, + }, + { + scenario: "key does exist with a mm value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "25.4mm", + }, + }, + }, + defaultValue: 0.0, + expect: 1.0, + expectError: false, + }, + { + scenario: "key does exist with a cm value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "2.54cm", + }, + }, + }, + defaultValue: 0.0, + expect: 1.0, + expectError: false, + }, + { + scenario: "key does exist with a pc value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "6pc", + }, + }, + }, + defaultValue: 0.0, + expect: 1.0, + expectError: false, + }, + { + scenario: "key does exist with no unit in the value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "100", + }, + }, + }, + defaultValue: 0.0, + expect: 100, + expectError: false, + }, + } { + t.Run(tc.scenario, func(t *testing.T) { + var actual float64 + + tc.form.Inches("foo", &actual, tc.defaultValue) + + if fmt.Sprintf("%.1f", actual) != fmt.Sprintf("%.1f", tc.expect) { + t.Errorf("expected %.1f but got %.1f", tc.expect, actual) + } + + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } + + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) + } +} + +func TestFormData_MandatoryInches(t *testing.T) { + for _, tc := range []struct { + scenario string + form *FormData + expect float64 + expectError bool + }{ + { + scenario: "missing mandatory key", + form: &FormData{}, + expect: 0.0, + expectError: true, + }, + { + scenario: "mandatory value is empty", + form: &FormData{ + values: map[string][]string{ + "foo": { + "", + }, + }, + }, + expect: 0.0, + expectError: true, + }, + { + scenario: "mandatory value has a unit, but the rest is not float64 compatible", + form: &FormData{ + values: map[string][]string{ + "foo": { + "foomm", + }, + }, + }, + expect: 0.0, + expectError: true, + }, + { + scenario: "mandatory value has no unit and is invalid", + form: &FormData{ + values: map[string][]string{ + "foo": { + "foo", + }, + }, + }, + expect: 0.0, + expectError: true, + }, + { + scenario: "a pt mandatory value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "72pt", + }, + }, + }, + expect: 1.0, + expectError: false, + }, + { + scenario: "a px mandatory value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "96px", + }, + }, + }, + expect: 1.0, + expectError: false, + }, + { + scenario: "an in mandatory value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "1in", + }, + }, + }, + expect: 1.0, + expectError: false, + }, + { + scenario: "a mm mandatory value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "25.4mm", + }, + }, + }, + expect: 1.0, + expectError: false, + }, + { + scenario: "a cm mandatory value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "2.54cm", + }, + }, + }, + expect: 1.0, + expectError: false, + }, + { + scenario: "a pc mandatory value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "6pc", + }, + }, + }, + expect: 1.0, + expectError: false, + }, + { + scenario: "no unit in the mandatory value", + form: &FormData{ + values: map[string][]string{ + "foo": { + "100", + }, + }, + }, + expect: 100, + expectError: false, + }, + } { + t.Run(tc.scenario, func(t *testing.T) { + var actual float64 + + tc.form.MandatoryInches("foo", &actual) + + if fmt.Sprintf("%.1f", actual) != fmt.Sprintf("%.1f", tc.expect) { + t.Errorf("expected %.1f but got %.1f", tc.expect, actual) + } + + if tc.expectError && tc.form.errors == nil { + t.Fatal("expected error but got none", tc.form.errors) + } + + if !tc.expectError && tc.form.errors != nil { + t.Fatalf("expected no error but got: %v", tc.form.errors) + } + }) + } +} + func TestFormData_Custom(t *testing.T) { for _, tc := range []struct { scenario string diff --git a/pkg/modules/chromium/routes.go b/pkg/modules/chromium/routes.go index 94667f50..e9f0f65b 100644 --- a/pkg/modules/chromium/routes.go +++ b/pkg/modules/chromium/routes.go @@ -122,12 +122,12 @@ func FormDataChromiumPdfOptions(ctx *api.Context) (*api.FormData, PdfOptions) { Bool("printBackground", &printBackground, defaultPdfOptions.PrintBackground). Float64("scale", &scale, defaultPdfOptions.Scale). Bool("singlePage", &singlePage, defaultPdfOptions.SinglePage). - Float64("paperWidth", &paperWidth, defaultPdfOptions.PaperWidth). - Float64("paperHeight", &paperHeight, defaultPdfOptions.PaperHeight). - Float64("marginTop", &marginTop, defaultPdfOptions.MarginTop). - Float64("marginBottom", &marginBottom, defaultPdfOptions.MarginBottom). - Float64("marginLeft", &marginLeft, defaultPdfOptions.MarginLeft). - Float64("marginRight", &marginRight, defaultPdfOptions.MarginRight). + Inches("paperWidth", &paperWidth, defaultPdfOptions.PaperWidth). + Inches("paperHeight", &paperHeight, defaultPdfOptions.PaperHeight). + Inches("marginTop", &marginTop, defaultPdfOptions.MarginTop). + Inches("marginBottom", &marginBottom, defaultPdfOptions.MarginBottom). + Inches("marginLeft", &marginLeft, defaultPdfOptions.MarginLeft). + Inches("marginRight", &marginRight, defaultPdfOptions.MarginRight). String("nativePageRanges", &pageRanges, defaultPdfOptions.PageRanges). Content("header.html", &headerTemplate, defaultPdfOptions.HeaderTemplate). Content("footer.html", &footerTemplate, defaultPdfOptions.FooterTemplate).