Skip to content
Snippets Groups Projects
Commit 4106f158 authored by Marius Dumitru Florea's avatar Marius Dumitru Florea
Browse files

XWIKI-19270: Add support for performing the PDF export using a browser running...

XWIKI-19270: Add support for performing the PDF export using a browser running in a Docker container
* Make sure we close the PDF input stream at the end and perform the cleanup (close the Chrome tab)
* Add configuration component
* Wait for the page to load before printing to PDF
* Rewrite the print preview URL to use the docker host domain (because localhost points to the docker container itself)

(cherry picked from commit b8fd6054)
parent 537f5b5a
No related branches found
No related tags found
No related merge requests found
Showing
with 468 additions and 66 deletions
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.export.pdf;
import org.xwiki.component.annotation.Role;
import org.xwiki.stability.Unstable;
/**
* PDF export configuration options.
*
* @version $Id$
* @since 14.4.1
* @since 14.5RC1
*/
@Role
@Unstable
public interface PDFExportConfiguration
{
/**
* @return the Docker image used to create the Docker container running the headless Chrome web browser
*/
String getChromeDockerImage();
/**
* @return the name of the Docker container running the headless Chrome web browser used to print web pages to PDF
*/
String getChromeDockerContainerName();
/**
* @return the domain used to access the host running the Docker container, from within the Docker container (i.e.
* by the headless Chrome web browser)
*/
String getChromeDockerHostName();
/**
* @return the port number used for communicating with the headless Chrome web browser
*/
int getChromeRemoteDebuggingPort();
}
......@@ -19,6 +19,7 @@
*/
package org.xwiki.export.pdf;
import java.io.IOException;
import java.io.InputStream;
import org.xwiki.component.annotation.Role;
......@@ -42,5 +43,5 @@ public interface PDFPrinter<T>
* @param input the data to be printed as PDF
* @return the PDF input stream
*/
InputStream print(T input);
InputStream print(T input) throws IOException;
}
......@@ -19,6 +19,7 @@
*/
package org.xwiki.export.pdf.internal.job;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.List;
......@@ -142,10 +143,11 @@ private boolean hasAccess(Right right, EntityReference reference)
|| this.authorization.hasAccess(right, this.request.getAuthorReference(), reference)));
}
private void saveAsPDF() throws Exception
private void saveAsPDF() throws IOException
{
URL printPreviewURL = (URL) this.request.getContext().get("request.url");
InputStream pdfContent = this.pdfPrinter.print(printPreviewURL);
this.temporaryResourceStore.createTemporaryFile(this.status.getPDFFileReference(), pdfContent);
try (InputStream pdfContent = this.pdfPrinter.print(printPreviewURL)) {
this.temporaryResourceStore.createTemporaryFile(this.status.getPDFFileReference(), pdfContent);
}
}
}
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.export.pdf.internal;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Singleton;
import org.xwiki.component.annotation.Component;
import org.xwiki.configuration.ConfigurationSource;
import org.xwiki.export.pdf.PDFExportConfiguration;
/**
* Default implementation of {@link PDFExportConfiguration}.
*
* @version $Id$
* @since 14.4.1
* @since 14.5RC1
*/
@Component
@Singleton
public class DefaultPDFExportConfiguration implements PDFExportConfiguration
{
private static final String PREFIX = "export.pdf.";
@Inject
@Named("xwikiproperties")
private ConfigurationSource configurationSource;
@Override
public String getChromeDockerImage()
{
return this.configurationSource.getProperty(PREFIX + "chromeDockerImage", "zenika/alpine-chrome:latest");
}
@Override
public String getChromeDockerContainerName()
{
return this.configurationSource.getProperty(PREFIX + "chromeDockerContainerName",
"headless-chrome-pdf-printer");
}
@Override
public String getChromeDockerHostName()
{
return this.configurationSource.getProperty(PREFIX + "chromeDockerHostName", "host.docker.internal");
}
@Override
public int getChromeRemoteDebuggingPort()
{
return this.configurationSource.getProperty(PREFIX + "chromeRemoteDebuggingPort", 9222);
}
}
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.export.pdf.internal.chrome;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeoutException;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import com.github.kklisura.cdt.protocol.commands.Runtime;
import com.github.kklisura.cdt.protocol.types.page.PrintToPDF;
import com.github.kklisura.cdt.protocol.types.page.PrintToPDFTransferMode;
import com.github.kklisura.cdt.protocol.types.runtime.Evaluate;
import com.github.kklisura.cdt.protocol.types.runtime.RemoteObject;
import com.github.kklisura.cdt.services.ChromeDevToolsService;
import com.github.kklisura.cdt.services.ChromeService;
import com.github.kklisura.cdt.services.impl.ChromeServiceImpl;
/**
* Help interact with the headless Chrome web browser.
*
* @version $Id$
* @since 14.4.1
* @since 14.5RC1
*/
@Component(roles = ChromeManager.class)
@Singleton
public class ChromeManager implements Initializable
{
/**
* The number of seconds to wait for Chrome remote debugging before giving up.
*/
private static final int REMOTE_DEBUGGING_TIMEOUT = 10;
@Inject
private Logger logger;
private ChromeService chromeService;
private String pageReadyPromise;
@Override
public void initialize() throws InitializationException
{
String filePath = "/pageReadyPromise.js";
try {
this.pageReadyPromise = IOUtils.toString(getClass().getResourceAsStream(filePath), StandardCharsets.UTF_8);
} catch (IOException e) {
throw new InitializationException(String.format("Failed to read the [%s] file.", filePath), e);
}
}
/**
* Connect to the headless Chrome web browser that runs on local host, behind the specified port.
*
* @param remoteDebuggingPort the port number to connect to
* @throws TimeoutException if the connection timeouts
*/
public void connect(int remoteDebuggingPort) throws TimeoutException
{
this.logger.debug("Connecting to the Chrome remote debugging service.");
this.chromeService = new ChromeServiceImpl(remoteDebuggingPort);
waitForChromeService(REMOTE_DEBUGGING_TIMEOUT);
}
/**
* @return the Chrome remote debugging service
*/
public ChromeService getChromeService()
{
return this.chromeService;
}
private void waitForChromeService(int timeoutSeconds) throws TimeoutException
{
this.logger.debug("Wait for Chrome to accept remote debugging connections.");
int timeoutMillis = timeoutSeconds * 1000;
long start = System.currentTimeMillis();
while (System.currentTimeMillis() - start < timeoutMillis) {
try {
this.chromeService.getVersion();
return;
} catch (Exception e) {
this.logger.debug("Chrome remote debugging not available. Retrying in 2s.");
try {
Thread.sleep(2000);
} catch (InterruptedException ie) {
this.logger.warn("Interrupted thread [{}]. Root cause: [{}].", Thread.currentThread().getName(),
ExceptionUtils.getRootCauseMessage(e));
// Restore the interrupted state.
Thread.currentThread().interrupt();
}
}
}
long waitTime = (System.currentTimeMillis() - start) / 1000;
throw new TimeoutException(String
.format("Timeout waiting for Chrome remote debugging to become available. Waited [%s] seconds.", waitTime));
}
/**
* Wait for a page to be ready.
*
* @param runtime the page runtime
*/
public void waitForPageReady(Runtime runtime) throws IOException
{
Evaluate evaluate = runtime.evaluate(/* expression */ this.pageReadyPromise, /* objectGroup */ null,
/* includeCommandLineAPI */ false, /* silent */ false, /* contextId */ null, /* returnByValue */ true,
/* generatePreview */ false, /* userGesture */ false, /* awaitPromise */ true,
/* throwOnSideEffect */ false, /* timeout */ REMOTE_DEBUGGING_TIMEOUT * 1000.0, /* disableBreaks */ true,
/* replMode */ false, /* allowUnsafeEvalBlockedByCSP */ false, /* uniqueContextId */ null);
if (evaluate.getExceptionDetails() != null) {
RemoteObject exception = evaluate.getExceptionDetails().getException();
Object cause = exception.getDescription();
if (cause == null) {
cause = exception.getValue();
}
throw new IOException("Failed to wait for page to be ready. Root cause: " + cause);
} else {
RemoteObject result = evaluate.getResult();
if (!"Page ready.".equals(result.getValue())) {
throw new IOException("Timeout waiting for page to be ready. Root cause: " + result.getValue());
}
}
}
/**
* Print the current page to PDF.
*
* @param devToolsService the developer tools service corresponding to the page to print
* @param cleanup the code to execute after the PDF was generated, useful for performing cleanup
* @return the PDF input stream
*/
public InputStream printToPDF(ChromeDevToolsService devToolsService, Runnable cleanup)
{
Boolean landscape = false;
Boolean displayHeaderFooter = false;
Boolean printBackground = false;
Double scale = 1d;
// A4 paper format
Double paperWidth = 8.27d;
Double paperHeight = 11.7d;
Double marginTop = 0d;
Double marginBottom = 0d;
Double marginLeft = 0d;
Double marginRight = 0d;
String pageRanges = "";
Boolean ignoreInvalidPageRanges = false;
String headerTemplate = "";
String footerTemplate = "";
Boolean preferCSSPageSize = false;
PrintToPDFTransferMode mode = PrintToPDFTransferMode.RETURN_AS_STREAM;
PrintToPDF printToPDF = devToolsService.getPage().printToPDF(landscape, displayHeaderFooter, printBackground,
scale, paperWidth, paperHeight, marginTop, marginBottom, marginLeft, marginRight, pageRanges,
ignoreInvalidPageRanges, headerTemplate, footerTemplate, preferCSSPageSize, mode);
return new PrintToPDFInputStream(devToolsService.getIO(), printToPDF.getStream(), cleanup);
}
}
......@@ -17,7 +17,7 @@
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.export.pdf.internal.docker;
package org.xwiki.export.pdf.internal.chrome;
import java.io.IOException;
import java.io.InputStream;
......@@ -46,16 +46,20 @@ public class PrintToPDFInputStream extends InputStream
private byte[] buffer = new byte[] {};
private Runnable closeCallback;
/**
* Creates a new instance for reading the specified PDF stream.
*
* @param io the service used to read the PDF data
* @param stream a handle of the stream that holds the PDF data
* @param closeCallback the code to execute when this input stream is closed
*/
public PrintToPDFInputStream(IO io, String stream)
public PrintToPDFInputStream(IO io, String stream, Runnable closeCallback)
{
this.io = io;
this.stream = stream;
this.closeCallback = closeCallback;
}
@Override
......@@ -65,7 +69,6 @@ public int read() throws IOException
this.bufferOffset = 0;
this.buffer = readBuffer();
if (this.buffer.length == 0) {
io.close(stream);
return -1;
}
}
......@@ -79,12 +82,22 @@ private byte[] readBuffer()
return new byte[] {};
}
Read read = io.read(stream);
Read read = this.io.read(this.stream);
this.finished = read.getEof() == Boolean.TRUE;
String data = read.getData();
if (read.getBase64Encoded() == Boolean.TRUE) {
return Base64.getDecoder().decode(read.getData());
} else {
return read.getData().getBytes(StandardCharsets.UTF_8);
data = new String(Base64.getDecoder().decode(data));
}
return data.getBytes(StandardCharsets.UTF_8);
}
@Override
public void close() throws IOException
{
this.io.close(this.stream);
if (this.closeCallback != null) {
this.closeCallback.run();
}
}
}
......@@ -30,6 +30,7 @@
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.export.pdf.PDFExportConfiguration;
import com.github.dockerjava.api.DockerClient;
import com.github.dockerjava.api.async.ResultCallbackTemplate;
......@@ -65,11 +66,15 @@ public class ContainerManager implements Initializable
@Inject
private Logger logger;
@Inject
private PDFExportConfiguration configuration;
private DockerClient client;
@Override
public void initialize() throws InitializationException
{
this.logger.debug("Initializing the Docker client.");
DockerClientConfig config = DefaultDockerClientConfig.createDefaultConfigBuilder().build();
DockerHttpClient httpClient = new ApacheDockerHttpClient.Builder().dockerHost(config.getDockerHost())
.sslConfig(config.getSSLConfig()).build();
......@@ -84,22 +89,29 @@ public void initialize() throws InitializationException
*/
public String maybeReuseContainerByName(String containerName)
{
this.logger.debug("Looking for an existing Docker container with name [{}].", containerName);
List<Container> containers =
this.client.listContainersCmd().withNameFilter(Arrays.asList(containerName)).withShowAll(true).exec();
if (containers.isEmpty()) {
this.logger.debug("Could not find any Docker container with name [{}].", containerName);
// There's no container with the specified name.
return null;
}
this.logger.debug("Inspecting the state of the Docker container [{}].", containers.get(0).getId());
InspectContainerResponse container = this.client.inspectContainerCmd(containers.get(0).getId()).exec();
if (container.getState().getDead() == Boolean.TRUE) {
this.logger.debug("Docker container [{}] is dead. Removing it.", container.getId());
// The container is not reusable. Try to remove it so it can be recreated.
this.client.removeContainerCmd(container.getId()).exec();
return null;
} else if (container.getState().getPaused() == Boolean.TRUE) {
this.logger.debug("Docker container [{}] is paused. Unpausing it.", container.getId());
this.client.unpauseContainerCmd(container.getId()).exec();
} else if (container.getState().getRunning() != Boolean.TRUE
&& container.getState().getRestarting() != Boolean.TRUE) {
this.logger.debug("Docker container [{}] is neither running nor restarting. Starting it.",
container.getId());
this.startContainer(container.getId());
}
......@@ -129,6 +141,7 @@ public boolean isLocalImagePresent(String imageName)
*/
public void pullImage(String imageName)
{
this.logger.debug("Pulling the Docker image [{}].", imageName);
wait(this.client.pullImageCmd(imageName).start());
}
......@@ -144,16 +157,23 @@ public void pullImage(String imageName)
public String createContainer(String imageName, String containerName, int remoteDebuggingPort,
List<String> parameters)
{
this.logger.debug("Creating a Docker container with name [{}] using image [{}], remote debugging port [{}]"
+ " and parameters [{}].", containerName, imageName, remoteDebuggingPort, parameters);
ExposedPort exposedPort = ExposedPort.tcp(remoteDebuggingPort);
Ports portBindings = new Ports();
portBindings.bind(exposedPort, Ports.Binding.bindPort(remoteDebuggingPort));
CreateContainerCmd command = this.client.createContainerCmd(imageName);
CreateContainerResponse container = command.withCmd(parameters).withExposedPorts(exposedPort)
.withHostConfig(HostConfig.newHostConfig().withBinds(
// Make sure it also works when XWiki is running in Docker.
new Bind(DOCKER_SOCK, new Volume(DOCKER_SOCK))).withPortBindings(portBindings))
// The extra host is needed in order to be able to access the XWiki instance running on the same machine as
// the Docker container itself.
.withHostConfig(HostConfig.newHostConfig()
.withExtraHosts(this.configuration.getChromeDockerHostName() + ":host-gateway").withBinds(
// Make sure it also works when XWiki is running in Docker.
new Bind(DOCKER_SOCK, new Volume(DOCKER_SOCK)))
.withPortBindings(portBindings))
.withName(containerName).exec();
this.logger.debug("Created the Docker container with id [{}].", container.getId());
return container.getId();
}
......@@ -164,6 +184,7 @@ public String createContainer(String imageName, String containerName, int remote
*/
public void startContainer(String containerId)
{
this.logger.debug("Starting the Docker container with id [{}].", containerId);
this.client.startContainerCmd(containerId).exec();
}
......@@ -175,9 +196,11 @@ public void startContainer(String containerId)
public void stopContainer(String containerId)
{
if (containerId != null) {
this.logger.debug("Stopping the Docker container with id [{}].", containerId);
this.client.stopContainerCmd(containerId).exec();
// Wait for the container to be fully stopped before continuing.
this.logger.debug("Wait for the Docker container [{}] to stop.", containerId);
wait(this.client.waitContainerCmd(containerId).start());
}
}
......@@ -187,9 +210,9 @@ private void wait(ResultCallbackTemplate<?, ?> template)
try {
template.awaitCompletion();
} catch (InterruptedException e) {
this.logger.warn("Interrupted thread [{}]. Root cause: [{}]", Thread.currentThread().getName(),
this.logger.warn("Interrupted thread [{}]. Root cause: [{}].", Thread.currentThread().getName(),
ExceptionUtils.getRootCauseMessage(e));
// Restore interrupted state to be a good citizen.
// Restore the interrupted state.
Thread.currentThread().interrupt();
}
}
......
......@@ -19,6 +19,7 @@
*/
package org.xwiki.export.pdf.internal.docker;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.Arrays;
......@@ -27,19 +28,20 @@
import javax.inject.Named;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.manager.ComponentLifecycleException;
import org.xwiki.component.phase.Disposable;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.export.pdf.PDFExportConfiguration;
import org.xwiki.export.pdf.PDFPrinter;
import org.xwiki.export.pdf.internal.chrome.ChromeManager;
import com.github.kklisura.cdt.protocol.commands.Page;
import com.github.kklisura.cdt.protocol.types.page.PrintToPDF;
import com.github.kklisura.cdt.protocol.types.page.PrintToPDFTransferMode;
import com.github.kklisura.cdt.protocol.commands.Runtime;
import com.github.kklisura.cdt.services.ChromeDevToolsService;
import com.github.kklisura.cdt.services.ChromeService;
import com.github.kklisura.cdt.services.impl.ChromeServiceImpl;
import com.github.kklisura.cdt.services.types.ChromeTab;
/**
......@@ -54,40 +56,44 @@
@Named("docker")
public class DockerURL2PDFPrinter implements PDFPrinter<URL>, Initializable, Disposable
{
private static final String CHROME_IMAGE = "zenika/alpine-chrome:latest";
@Inject
private Logger logger;
private static final String CONTAINER_NAME = "headless-chrome-pdf-printer";
@Inject
private PDFExportConfiguration configuration;
private static final int REMOTE_DEBUGGING_PORT = 9222;
@Inject
private ChromeManager chromeManager;
@Inject
private ContainerManager containerManager;
private String containerId;
private ChromeService chromeService;
@Override
public void initialize() throws InitializationException
{
initializeDockerContainer();
this.chromeService = new ChromeServiceImpl(REMOTE_DEBUGGING_PORT);
initializeChromeDockerContainer(this.configuration.getChromeDockerImage(),
this.configuration.getChromeDockerContainerName(), this.configuration.getChromeRemoteDebuggingPort());
initializeChromeService(this.configuration.getChromeRemoteDebuggingPort());
}
private void initializeDockerContainer() throws InitializationException
private void initializeChromeDockerContainer(String imageName, String containerName, int remoteDebuggingPort)
throws InitializationException
{
this.logger.debug("Initializing the Docker container running the headless Chrome web browser.");
try {
this.containerId = this.containerManager.maybeReuseContainerByName(CONTAINER_NAME);
this.containerId = this.containerManager.maybeReuseContainerByName(containerName);
if (this.containerId == null) {
// The container doesn't exist so we have to create it.
// But first we need to pull the image used to create the container, if we don't have it already.
if (!this.containerManager.isLocalImagePresent(CHROME_IMAGE)) {
this.containerManager.pullImage(CHROME_IMAGE);
if (!this.containerManager.isLocalImagePresent(imageName)) {
this.containerManager.pullImage(imageName);
}
this.containerId = this.containerManager.createContainer(CHROME_IMAGE, CONTAINER_NAME,
REMOTE_DEBUGGING_PORT, Arrays.asList("--no-sandbox", "--remote-debugging-address=0.0.0.0",
"--remote-debugging-port=" + REMOTE_DEBUGGING_PORT));
this.containerId = this.containerManager.createContainer(imageName, containerName, remoteDebuggingPort,
Arrays.asList("--no-sandbox", "--remote-debugging-address=0.0.0.0",
"--remote-debugging-port=" + remoteDebuggingPort));
this.containerManager.startContainer(containerId);
}
} catch (Exception e) {
......@@ -95,57 +101,52 @@ private void initializeDockerContainer() throws InitializationException
}
}
private void initializeChromeService(int remoteDebuggingPort) throws InitializationException
{
try {
this.chromeManager.connect(remoteDebuggingPort);
} catch (Exception e) {
throw new InitializationException("Failed to initialize the Chrome remote debugging service.", e);
}
}
@Override
public void dispose() throws ComponentLifecycleException
{
try {
this.containerManager.stopContainer(this.containerId);
} catch (Exception e) {
throw new ComponentLifecycleException("Failed to stop the Docker container used for PDF export.", e);
throw new ComponentLifecycleException(
String.format("Failed to stop the Docker container [%s] used for PDF export.", this.containerId), e);
}
}
@Override
public InputStream print(URL input)
public InputStream print(URL input) throws IOException
{
this.logger.debug("Printing [{}]", input);
// The headless Chrome web browser runs inside a Docker container where 'localhost' refers to the container
// itself. We have to update the domain from the given URL to point to the host running both the XWiki instance
// and the Docker container.
URL printPreviewURL =
new URL(input.toString().replace("://localhost", "://" + this.configuration.getChromeDockerHostName()));
ChromeService chromeService = this.chromeManager.getChromeService();
ChromeTab tab = chromeService.createTab();
ChromeDevToolsService devToolsService = chromeService.createDevToolsService(tab);
Page page = devToolsService.getPage();
page.enable();
page.navigate(input.toString());
InputStream[] pdfStreams = new InputStream[] {null};
page.onLoadEventFired(loadEventFired -> {
Boolean landscape = false;
Boolean displayHeaderFooter = false;
Boolean printBackground = false;
Double scale = 1d;
// A4 paper format
Double paperWidth = 8.27d;
Double paperHeight = 11.7d;
Double marginTop = 0d;
Double marginBottom = 0d;
Double marginLeft = 0d;
Double marginRight = 0d;
String pageRanges = "";
Boolean ignoreInvalidPageRanges = false;
String headerTemplate = "";
String footerTemplate = "";
Boolean preferCSSPageSize = false;
PrintToPDFTransferMode mode = PrintToPDFTransferMode.RETURN_AS_STREAM;
PrintToPDF printToPDF = devToolsService.getPage().printToPDF(landscape, displayHeaderFooter,
printBackground, scale, paperWidth, paperHeight, marginTop, marginBottom, marginLeft, marginRight,
pageRanges, ignoreInvalidPageRanges, headerTemplate, footerTemplate, preferCSSPageSize, mode);
pdfStreams[0] = new PrintToPDFInputStream(devToolsService.getIO(), printToPDF.getStream());
page.navigate(printPreviewURL.toString());
Runtime runtime = devToolsService.getRuntime();
runtime.enable();
this.chromeManager.waitForPageReady(runtime);
return this.chromeManager.printToPDF(devToolsService, () -> {
devToolsService.close();
chromeService.closeTab(tab);
});
devToolsService.waitUntilClosed();
return pdfStreams[0];
}
}
org.xwiki.export.pdf.internal.chrome.ChromeManager
org.xwiki.export.pdf.internal.docker.ContainerManager
org.xwiki.export.pdf.internal.docker.DockerURL2PDFPrinter
org.xwiki.export.pdf.internal.job.DefaultPDFExportJobRequestFactory
org.xwiki.export.pdf.internal.job.PDFExportContextStore
org.xwiki.export.pdf.internal.DefaultPDFExportConfiguration
org.xwiki.export.pdf.internal.DefaultRequiredSkinExtensionsRecorder
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
new Promise((resolve, reject) => {
const waitForPageReady = () => {
require(['xwiki-page-ready'], function(pageReady) {
pageReady.afterPageReady(() => {
// Print to PDF only after all page ready callbacks were executed,
// because the print preview is initialized as a page ready callback.
pageReady.afterPageReady(resolve.bind(null, 'Page ready.'));
});
}, reject.bind(null, 'Failed to load the xwiki-page-ready module.'));
};
let retryCount = 0;
const maybeWaitForPageReady = () => {
if (typeof require === 'function') {
waitForPageReady();
} else {
retryCount++;
if (retryCount > 10) {
reject('Timeout waiting for RequireJS to be available.');
} else {
setTimeout(maybeWaitForPageReady, 1000);
}
}
};
maybeWaitForPageReady();
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment