diff --git a/pkg/modules/api/context.go b/pkg/modules/api/context.go
index b5336d34..a4c76925 100644
--- a/pkg/modules/api/context.go
+++ b/pkg/modules/api/context.go
@@ -473,6 +473,23 @@ func newContext(echoCtx echo.Context, logger *slog.Logger, fs *gotenberg.FileSys
}
}
+ // Create symlinks from original filenames to UUID-based disk names
+ // so that relative asset references (e.g.,
)
+ // resolve correctly when Chromium navigates to a file:// URL.
+ // Symlink creation is best-effort: it may fail for filenames that
+ // exceed the filesystem NAME_MAX limit (the reason UUIDs were
+ // introduced in the first place).
+ for originalName, diskPath := range ctx.files {
+ symlinkPath := fmt.Sprintf("%s/%s", ctx.dirPath, originalName)
+ if symlinkPath == diskPath {
+ continue
+ }
+ err = os.Symlink(filepath.Base(diskPath), symlinkPath)
+ if err != nil {
+ logger.DebugContext(context.Background(), fmt.Sprintf("skip symlink for '%s': %s", originalName, err))
+ }
+ }
+
ctx.Log().DebugContext(ctx, fmt.Sprintf("form fields: %+v", ctx.values))
ctx.Log().DebugContext(ctx, fmt.Sprintf("form files: %+v", ctx.files))
ctx.Log().DebugContext(ctx, fmt.Sprintf("form files by field: %+v", ctx.filesByField))
diff --git a/test/integration/features/chromium_convert_html.feature b/test/integration/features/chromium_convert_html.feature
index 74099998..a4567f8a 100644
--- a/test/integration/features/chromium_convert_html.feature
+++ b/test/integration/features/chromium_convert_html.feature
@@ -1175,6 +1175,19 @@ Feature: /forms/chromium/convert/html
Then the response status code should be 200
Then the response header "Content-Type" should be "application/pdf"
+ # See: https://github.com/gotenberg/gotenberg/issues/1505.
+ Scenario: POST /forms/chromium/convert/html (Asset)
+ Given I have a default Gotenberg container
+ When I make a "POST" request to Gotenberg at the "/forms/chromium/convert/html" endpoint with the following form data and header(s):
+ | files | testdata/html-with-asset/index.html | file |
+ | files | testdata/html-with-asset/image.png | file |
+ | Gotenberg-Output-Filename | foo | header |
+ Then the response status code should be 200
+ Then the response header "Content-Type" should be "application/pdf"
+ Then there should be 1 PDF(s) in the response
+ Then the "foo.pdf" PDF should have 1 page(s)
+ Then the "foo.pdf" PDF should have 1 image(s)
+
# See: https://github.com/gotenberg/gotenberg/issues/1500.
Scenario: POST /forms/chromium/convert/html (Long Filename)
Given I have a default Gotenberg container
diff --git a/test/integration/scenario/scenario.go b/test/integration/scenario/scenario.go
index 4a66238d..95dd136a 100644
--- a/test/integration/scenario/scenario.go
+++ b/test/integration/scenario/scenario.go
@@ -952,6 +952,39 @@ func (s *scenario) thePdfShouldHavePages(ctx context.Context, name string, pages
return nil
}
+func (s *scenario) thePdfShouldHaveImages(ctx context.Context, name string, images int) error {
+ path := fmt.Sprintf("%s/%s/%s", s.workdir, s.resp.Header().Get("Gotenberg-Trace"), name)
+
+ _, err := os.Stat(path)
+ if os.IsNotExist(err) {
+ return fmt.Errorf("PDF %q does not exist", path)
+ }
+
+ cmd := []string{
+ "pdfimages",
+ "-list",
+ filepath.Base(path),
+ }
+
+ output, err := execCommandInIntegrationToolsContainer(ctx, cmd, path)
+ if err != nil {
+ return fmt.Errorf("exec %q: %w", cmd, err)
+ }
+
+ // pdfimages -list outputs a header (2 lines) then one line per image.
+ lines := strings.Split(strings.TrimSpace(output), "\n")
+ actual := 0
+ if len(lines) > 2 {
+ actual = len(lines) - 2
+ }
+
+ if actual != images {
+ return fmt.Errorf("expected %d image(s), but actual is %d", images, actual)
+ }
+
+ return nil
+}
+
func (s *scenario) thePdfShouldBeSetToLandscapeOrientation(ctx context.Context, name string, kind string) error {
var path string
if !strings.HasPrefix(name, "*_") {
@@ -1270,6 +1303,7 @@ func InitializeScenario(ctx *godog.ScenarioContext) {
ctx.Then(`^the "([^"]*)" PDF should have (\d+) page\(s\)$`, s.thePdfShouldHavePages)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) be set to landscape orientation$`, s.thePdfShouldBeSetToLandscapeOrientation)
ctx.Then(`^the "([^"]*)" PDF (should|should NOT) have the following content at page (\d+):$`, s.thePdfShouldHaveTheFollowingContentAtPage)
+ ctx.Then(`^the "([^"]*)" PDF should have (\d+) image\(s\)$`, s.thePdfShouldHaveImages)
ctx.After(func(ctx context.Context, sc *godog.Scenario, err error) (context.Context, error) {
if s.gotenbergContainer != nil {
errTerminate := s.gotenbergContainer.Terminate(ctx, testcontainers.StopTimeout(0))
diff --git a/test/integration/testdata/html-with-asset/image.png b/test/integration/testdata/html-with-asset/image.png
new file mode 100644
index 00000000..62a5f8f4
Binary files /dev/null and b/test/integration/testdata/html-with-asset/image.png differ
diff --git a/test/integration/testdata/html-with-asset/index.html b/test/integration/testdata/html-with-asset/index.html
new file mode 100644
index 00000000..0fc90bbc
--- /dev/null
+++ b/test/integration/testdata/html-with-asset/index.html
@@ -0,0 +1,11 @@
+
+
+
Asset test
+
+
+