Skip to content

Commit

Permalink
custom css and is ready condition
Browse files Browse the repository at this point in the history
  • Loading branch information
klahap committed Nov 23, 2024
1 parent 95db02e commit e8e7e6f
Show file tree
Hide file tree
Showing 6 changed files with 115 additions and 11 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The easiest way to get `web2pdf` up and running is by using the provided Docker

2. Run the container:
```bash
docker run -d -p 8080:8080 goquati/web2pdf:latest
docker run -p 8080:8080 goquati/web2pdf:latest
```

This will start the `web2pdf` service on port `8080` of your local machine.
Expand Down
14 changes: 13 additions & 1 deletion src/main/kotlin/io/github/goquati/api/Web2PdfController.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package io.github.goquati.api

import de.smart.nexus.orchestrator.api.Web2pdfApi
import de.smart.nexus.orchestrator.oas_model.IsReadyConditionDto
import de.smart.nexus.orchestrator.oas_model.JsConditionDto
import de.smart.nexus.orchestrator.oas_model.PdfPrintOptionsDto
import de.smart.nexus.orchestrator.oas_model.Web2PdfRequestDto
import io.github.goquati.LoggerDelegate
import io.github.goquati.LoggerDelegate.Companion.measureExecutionTime
import io.github.goquati.service.Web2PdfService
import io.github.goquati.service.Web2PdfService.Companion.host
import io.ktor.http.*
import org.springframework.core.io.ByteArrayResource
import org.springframework.core.io.Resource
import org.springframework.http.ResponseEntity
Expand All @@ -21,12 +24,18 @@ class Web2PdfController(
url: String,
header: Map<String, String>?,
cookie: Map<String, String>?,
acceptLanguage: String?,
customCss: String?,
condition: IsReadyConditionDto?,
options: PdfPrintOptionsDto?,
serverHttpRequest: ServerHttpRequest,
): ResponseEntity<Resource> = Web2PdfRequestDto(
url = url,
headers = serverHttpRequest.extractQueryParams(regexHeaderKey),
cookies = serverHttpRequest.extractQueryParams(regexCookieKey),
acceptLanguage = acceptLanguage,
customCss = customCss,
condition = condition,
options = options
).toPdfResponse()

Expand All @@ -39,9 +48,12 @@ class Web2PdfController(
private suspend fun Web2PdfRequestDto.toPdfResponse(): ResponseEntity<Resource> =
log.measureExecutionTime("generated PDF ($host)") {
web2PdfService.generatePdf(
url = url,
url = runCatching { Url(url) }.getOrElse { throw Exception("invalid url") },
headers = headers,
cookies = cookies,
acceptLanguage = acceptLanguage,
customCss = customCss,
condition = condition,
options = options,
)
}.let { ResponseEntity.ok(ByteArrayResource(it)) }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.github.goquati.service

import io.ktor.http.*
import org.springframework.core.env.Environment
import org.springframework.stereotype.Service

Expand All @@ -11,5 +12,5 @@ class EnvironmentService(

val isDevMode get() = environment.activeProfiles.contains("dev")

fun getLocalServerUrl(path: String) = "http://$host:8080/$path"
fun getLocalServerUrl(path: String) = Url("http://$host:8080/$path")
}
66 changes: 59 additions & 7 deletions src/main/kotlin/io/github/goquati/service/Web2PdfService.kt
Original file line number Diff line number Diff line change
@@ -1,17 +1,23 @@
package io.github.goquati.service

import de.smart.nexus.orchestrator.oas_model.IsReadyConditionDto
import de.smart.nexus.orchestrator.oas_model.PdfPrintOptionsDto
import de.smart.nexus.orchestrator.oas_model.Web2PdfRequestDto
import io.github.goquati.Web2PdfException
import io.ktor.http.*
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.coroutines.delay
import kotlinx.serialization.json.*
import org.hildan.chrome.devtools.domains.emulation.EmulationDomain
import org.hildan.chrome.devtools.domains.emulation.SetUserAgentOverrideRequest
import org.hildan.chrome.devtools.domains.network.CookieParam
import org.hildan.chrome.devtools.domains.network.NetworkDomain
import org.hildan.chrome.devtools.domains.page.PrintToPDFRequest
import org.hildan.chrome.devtools.domains.runtime.RuntimeDomain
import org.hildan.chrome.devtools.protocol.ExperimentalChromeApi
import org.hildan.chrome.devtools.sessions.PageSession
import org.hildan.chrome.devtools.sessions.goto
import org.hildan.chrome.devtools.sessions.use
import org.intellij.lang.annotations.Language
import org.springframework.stereotype.Service
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
Expand All @@ -21,24 +27,70 @@ class Web2PdfService(
private val browserSessionService: BrowserSessionService,
) {
suspend fun generatePdf(
url: String,
url: Url,
headers: Map<String, String>? = null,
cookies: Map<String, String>? = null,
@Language("css") customCss: String? = null,
condition: IsReadyConditionDto? = null,
acceptLanguage: String? = null,
options: PdfPrintOptionsDto?,
): ByteArray = runCatching {
val url = runCatching { Url(url) }.getOrElse { throw Exception("invalid url") }
browserSessionService.getPageSession().use { pageSession ->
pageSession.network.addHeaders(headers ?: emptyMap())
pageSession.network.addCookies(cookies ?: emptyMap(), host = url.host)
pageSession.network.apply {
addHeaders(headers ?: emptyMap())
addCookies(cookies ?: emptyMap(), host = url.host)
}
if (acceptLanguage != null)
pageSession.emulation.setAcceptLanguage(acceptLanguage)

runCatching {
pageSession.goto(url.toString())
}.getOrElse { throw Exception("Navigation to '${url.host}' failed. The full URL is hidden for security reasons. Please ensure the URL is correct and reachable.") }
}.getOrElse { error("Navigation to '${url.host}' failed. The full URL is hidden for security reasons. Please ensure the URL is correct and reachable.") }

if (customCss != null)
pageSession.setCustomCss(customCss)
if (condition != null)
pageSession.runtime.waitForReady(condition)

pageSession.page.printToPDF(options?.pdfOptions ?: PrintToPDFRequest())
.let { @OptIn(ExperimentalEncodingApi::class) Base64.decode(it.data) }
}
}.getOrElse { throw Web2PdfException(it.message ?: "") }

@OptIn(ExperimentalChromeApi::class)
private suspend fun PageSession.setCustomCss(data: String) {
dom.enable()
val frameId = page.getFrameTree().frameTree.frame.id
css.apply {
enable()
val styleSheetId = createStyleSheet(frameId = frameId).styleSheetId
setStyleSheetText(styleSheetId = styleSheetId, text = data)
}
}

@OptIn(ExperimentalChromeApi::class)
private suspend fun EmulationDomain.setAcceptLanguage(language: String) {
setUserAgentOverride(
SetUserAgentOverrideRequest(
userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/116.0.0.0 Safari/537.36",
acceptLanguage = language
)
)
}

private suspend fun RuntimeDomain.waitForReady(condition: IsReadyConditionDto) {
enable()
var isLoaded = false
for (i in 0..<condition.maxTries) {
isLoaded = evaluate(expression = condition.expression)
.result.value?.let { it as? JsonPrimitive }?.booleanOrNull
?: error("invalid response of custom JS condition")
if (isLoaded) break
delay(condition.delayInMilliSeconds.toLong())
}
if (!isLoaded)
error("timeout, custom JS condition not full filled")
}

private suspend fun NetworkDomain.addHeaders(data: Map<String, String>) {
if (data.isEmpty()) return
Expand Down
2 changes: 1 addition & 1 deletion src/main/resources/application.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
logging:
pattern:
console: "%d{yyyy-MM-dd HH:mm:ss.SSSXXX} %clr(%5p) [%15thread] %clr(%-40.40logger{39}){cyan} : %msg%n"
console: "%d{yyyy-MM-dd HH:mm:ss.SSSXXX} %clr(%5p) [%15.15thread] %clr(%-40.40logger){cyan} : %msg%n"
39 changes: 39 additions & 0 deletions src/main/resources/oas.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,22 @@ paths:
explode: true
example:
my-cookie: my-cookie-value
- in: query
name: acceptLanguage
required: false
schema:
type: string
- in: query
name: customCss
required: false
schema:
type: string
- in: query
name: condition
schema:
$ref: '#/components/schemas/IsReadyCondition'
style: form
explode: true
- in: query
name: options
required: false
Expand Down Expand Up @@ -149,6 +165,12 @@ components:
type: string
example:
my-cookie: my-cookie-value
acceptLanguage:
type: string
customCss:
type: string
condition:
$ref: '#/components/schemas/IsReadyCondition'
options:
$ref: '#/components/schemas/PdfPrintOptions'
Html2PdfRequest:
Expand Down Expand Up @@ -182,6 +204,23 @@ components:
# Hello World
options:
$ref: '#/components/schemas/PdfPrintOptions'
IsReadyCondition:
type: object
required:
- expression
- maxTries
- delayInMilliSeconds
properties:
expression:
type: string
maxTries:
type: integer
minimum: 1
example: 10
delayInMilliSeconds:
type: integer
minimum: 0
example: 1000
PdfPrintOptions:
type: object
properties:
Expand Down

0 comments on commit e8e7e6f

Please sign in to comment.