From 06e8323f8300cfd86a464085886abdf10b34c499 Mon Sep 17 00:00:00 2001 From: Dennis Ruhe Date: Thu, 17 Aug 2017 17:17:40 +0200 Subject: [PATCH] Created first version of wkhtmltoimage --- .../io/github/cloudify/scala.spdf/Image.scala | 64 ++++++++++ .../cloudify/scala.spdf/ImageConfig.scala | 112 ++++++++++++++++++ .../cloudify/scala/spdf/ImageSpec.scala | 51 ++++++++ 3 files changed, 227 insertions(+) create mode 100644 src/main/scala/io/github/cloudify/scala.spdf/Image.scala create mode 100644 src/main/scala/io/github/cloudify/scala.spdf/ImageConfig.scala create mode 100644 src/test/scala/io/github/cloudify/scala/spdf/ImageSpec.scala diff --git a/src/main/scala/io/github/cloudify/scala.spdf/Image.scala b/src/main/scala/io/github/cloudify/scala.spdf/Image.scala new file mode 100644 index 0000000..a8539d9 --- /dev/null +++ b/src/main/scala/io/github/cloudify/scala.spdf/Image.scala @@ -0,0 +1,64 @@ +package io.github.cloudify.scala.spdf + +import scala.sys.process._ +import java.io.File + +class Image(executablePath: String, config: ImageConfig) { + validateExecutable_!(executablePath) + + /** + * Runs the conversion tool to convert sourceDocument HTML into + * destinationDocument image. + */ + def run[A, B](sourceDocument: A, destinationDocument: B)(implicit sourceDocumentLike: SourceDocumentLike[A], destinationDocumentLike: DestinationDocumentLike[B]): Int = { + val commandLine = toCommandLine(sourceDocument, destinationDocument) + val process = Process(commandLine) + def source = sourceDocumentLike.sourceFrom(sourceDocument) _ + def sink = destinationDocumentLike.sinkTo(destinationDocument) _ + + (sink compose source)(process).! + } + + /** + * Generates the command line needed to execute `wkhtmltoimage` + */ + private def toCommandLine[A: SourceDocumentLike, B: DestinationDocumentLike](source: A, destination: B): Seq[String] = + Seq(executablePath) ++ + ImageConfig.toParameters(config) ++ + Seq( + implicitly[SourceDocumentLike[A]].commandParameter(source), + implicitly[DestinationDocumentLike[B]].commandParameter(destination) + ) + + /** + * Check whether the executable is actually executable, if it isn't + * a NoExecutableException is thrown. + */ + private def validateExecutable_!(executablePath: String): Unit = { + val executableFile = new File(executablePath) + if(!executableFile.canExecute) throw new NoExecutableException(executableFile.getAbsolutePath) + } + +} + +object Image { + + /** + * Creates a new instance of Image with default configuration + * @return + */ + def apply(config: ImageConfig): Image = { + val executablePath: String = ImageConfig.findExecutable.getOrElse { + throw new NoExecutableException(System.getenv("PATH")) + } + + apply(executablePath, config) + } + + /** + * Creates a new instance of Image with the passed configuration + */ + def apply(executablePath: String, config: ImageConfig): Image = + new Image(executablePath, config) + +} diff --git a/src/main/scala/io/github/cloudify/scala.spdf/ImageConfig.scala b/src/main/scala/io/github/cloudify/scala.spdf/ImageConfig.scala new file mode 100644 index 0000000..b6528ad --- /dev/null +++ b/src/main/scala/io/github/cloudify/scala.spdf/ImageConfig.scala @@ -0,0 +1,112 @@ +package io.github.cloudify.scala.spdf + +import scala.sys.process._ +import ParamShow._ + +/** + * Holds the configuration parameters of Pdf Kit + */ +trait ImageConfig extends PdfConfig + +object ImageConfig { + + /** + * An instance of the default configuration + */ + object default extends ImageConfig + + /** + * Generates a sequence of command line parameters from a `PdfKitConfig` + */ + def toParameters(config: ImageConfig): Seq[String] = { + import config._ + Seq( + allow.toParameter, + background.toParameter, + defaultHeader.toParameter, + disableExternalLinks.toParameter, + disableInternalLinks.toParameter, + disableJavascript.toParameter, + noPdfCompression.toParameter, + disableSmartShrinking.toParameter, + javascriptDelay.toParameter, + enableForms.toParameter, + encoding.toParameter, + footerCenter.toParameter, + footerFontName.toParameter, + footerFontSize.toParameter, + footerHtml.toParameter, + footerLeft.toParameter, + footerLine.toParameter, + footerRight.toParameter, + footerSpacing.toParameter, + grayScale.toParameter, + headerCenter.toParameter, + headerFontName.toParameter, + headerFontSize.toParameter, + headerHtml.toParameter, + headerLeft.toParameter, + headerLine.toParameter, + headerRight.toParameter, + headerSpacing.toParameter, + lowQuality.toParameter, + marginBottom.toParameter, + marginLeft.toParameter, + marginRight.toParameter, + marginTop.toParameter, + minimumFontSize.toParameter, + orientation.toParameter, + outline.toParameter, + outlineDepth.toParameter, + pageHeight.toParameter, + pageOffset.toParameter, + pageSize.toParameter, + pageWidth.toParameter, + password.toParameter, + printMediaType.toParameter, + tableOfContent.toParameter, + tableOfContentDepth.toParameter, + tableOfContentDisableBackLinks.toParameter, + tableOfContentDisableLinks.toParameter, + tableOfContentFontName.toParameter, + tableOfContentHeaderFontName.toParameter, + tableOfContentHeaderFontSize.toParameter, + tableOfContentHeaderText.toParameter, + tableOfContentLevel1FontSize.toParameter, + tableOfContentLevel1Indentation.toParameter, + tableOfContentLevel2FontSize.toParameter, + tableOfContentLevel2Indentation.toParameter, + tableOfContentLevel3FontSize.toParameter, + tableOfContentLevel3Indentation.toParameter, + tableOfContentLevel4FontSize.toParameter, + tableOfContentLevel4Indentation.toParameter, + tableOfContentLevel5FontSize.toParameter, + tableOfContentLevel5Indentation.toParameter, + tableOfContentLevel6FontSize.toParameter, + tableOfContentLevel6Indentation.toParameter, + tableOfContentLevel7FontSize.toParameter, + tableOfContentLevel7Indentation.toParameter, + tableOfContentNoDots.toParameter, + title.toParameter, + userStyleSheet.toParameter, + username.toParameter, + useXServer.toParameter, + viewportSize.toParameter, + zoom.toParameter + ).flatten + } + + /** + * Attempts to find the `wkhtmltoimage` executable in the system path. + * @return + */ + def findExecutable: Option[String] = try { + val os = System.getProperty("os.name").toLowerCase + val cmd = if(os.contains("windows")) "where wkhtmltoimage" else "which wkhtmltoimage" + + Option(cmd.!!.trim).filter(_.nonEmpty) + } catch { + case _: RuntimeException => None + } + +} diff --git a/src/test/scala/io/github/cloudify/scala/spdf/ImageSpec.scala b/src/test/scala/io/github/cloudify/scala/spdf/ImageSpec.scala new file mode 100644 index 0000000..9b170c2 --- /dev/null +++ b/src/test/scala/io/github/cloudify/scala/spdf/ImageSpec.scala @@ -0,0 +1,51 @@ +package io.github.cloudify.scala.spdf + +import java.io.File +import scala.sys.process._ +import org.scalatest.Matchers +import org.scalatest.WordSpec + +class ImageSpec extends WordSpec with Matchers { + + "An Image" should { + + "require the executionPath config" in { + val file = new File("notexecutable") + val filePath = file.getAbsolutePath + + assertThrows[NoExecutableException] { + new Image(filePath, ImageConfig.default) + } + + assertThrows[NoExecutableException] { + Image(filePath, ImageConfig.default) + } + + } + + PdfConfig.findExecutable match { + case Some(_) => + "generate an image file from an HTML string" in { + + val page = + """ + |

Hello

+ """.stripMargin + + val file = File.createTempFile("scala.spdf", "pdf") + + val image = Image(ImageConfig.default) + + image.run(page, file) + + Seq("file", file.getAbsolutePath).!! should include("PDF document") + } + + case None => + "Skipping test, missing wkhtmltopdf binary" in { true should equal(true) } + } + + + } + +}