Compare commits
13 Commits
| Author | SHA1 | Date |
|---|---|---|
|
|
24026d7fbe | |
|
|
e2c13c2503 | |
|
|
86f3f66d50 | |
|
|
0e1ae654a7 | |
|
|
f645477ded | |
|
|
92b53529f6 | |
|
|
3036ba243c | |
|
|
17931ea1ca | |
|
|
cb8867af2e | |
|
|
8d4ba39793 | |
|
|
0a97b37244 | |
|
|
24fca8c62c | |
|
|
8dbe0c59fa |
|
|
@ -0,0 +1,41 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="DocViewer KORGE" type="GradleRunConfiguration" factoryName="Gradle">
|
||||||
|
<ExternalSystemSettings>
|
||||||
|
<option name="env">
|
||||||
|
<map>
|
||||||
|
<entry key="GRADLE_USER_HOME" value="$USER_HOME$/.gradle" />
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
<option name="executionName" />
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="externalSystemIdString" value="GRADLE" />
|
||||||
|
<option name="scriptParameters" value="" />
|
||||||
|
<option name="taskDescriptions">
|
||||||
|
<list />
|
||||||
|
</option>
|
||||||
|
<option name="taskNames">
|
||||||
|
<list>
|
||||||
|
<option value=":apps:documentViewerKorge:runJvmAutoreload" />
|
||||||
|
</list>
|
||||||
|
</option>
|
||||||
|
<option name="vmOptions" />
|
||||||
|
</ExternalSystemSettings>
|
||||||
|
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||||
|
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||||
|
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||||
|
<extension name="net.ashald.envfile">
|
||||||
|
<option name="IS_ENABLED" value="false" />
|
||||||
|
<option name="IS_SUBST" value="false" />
|
||||||
|
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||||
|
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||||
|
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||||
|
<ENTRIES>
|
||||||
|
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||||
|
</ENTRIES>
|
||||||
|
</extension>
|
||||||
|
</EXTENSION>
|
||||||
|
<DebugAllEnabled>false</DebugAllEnabled>
|
||||||
|
<RunAsTest>false</RunAsTest>
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
<component name="ProjectRunConfigurationManager">
|
||||||
|
<configuration default="false" name="DocumentViewerKt" type="JetRunConfigurationType" nameIsGenerated="true">
|
||||||
|
<option name="MAIN_CLASS_NAME" value="de.itkl.documentViewer.DocumentViewerKt" />
|
||||||
|
<module name="docthor.apps.documentViewer.main" />
|
||||||
|
<shortenClasspath name="NONE" />
|
||||||
|
<extension name="net.ashald.envfile">
|
||||||
|
<option name="IS_ENABLED" value="false" />
|
||||||
|
<option name="IS_SUBST" value="false" />
|
||||||
|
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||||
|
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||||
|
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||||
|
<ENTRIES>
|
||||||
|
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||||
|
</ENTRIES>
|
||||||
|
</extension>
|
||||||
|
<extension name="software.aws.toolkits.jetbrains.core.execution.JavaAwsConnectionExtension">
|
||||||
|
<option name="credential" />
|
||||||
|
<option name="region" />
|
||||||
|
<option name="useCurrentConnection" value="false" />
|
||||||
|
</extension>
|
||||||
|
<method v="2">
|
||||||
|
<option name="Make" enabled="true" />
|
||||||
|
</method>
|
||||||
|
</configuration>
|
||||||
|
</component>
|
||||||
|
|
@ -164,28 +164,6 @@ fun <T> ZoomedImage(
|
||||||
fun loadImageBitmap(file: File): ImageBitmap =
|
fun loadImageBitmap(file: File): ImageBitmap =
|
||||||
file.inputStream().buffered().use(::loadImageBitmap)
|
file.inputStream().buffered().use(::loadImageBitmap)
|
||||||
|
|
||||||
data class PointConverter(
|
|
||||||
val docWidth: Int,
|
|
||||||
val docHeight: Int,
|
|
||||||
val canvasWidth: Float,
|
|
||||||
val canvasHeight: Float
|
|
||||||
) {
|
|
||||||
fun convertX(x: Int): Float {
|
|
||||||
val xf = x.toFloat()
|
|
||||||
val relXf = docWidth / xf
|
|
||||||
val scaledXf = canvasWidth * relXf
|
|
||||||
// println("X: $scaledXf")
|
|
||||||
return scaledXf
|
|
||||||
}
|
|
||||||
fun convertY(y: Int): Float {
|
|
||||||
val yf = y.toFloat()
|
|
||||||
val relYf = docHeight / yf
|
|
||||||
val scaledYf = canvasHeight * relYf
|
|
||||||
// println("Y: $scaledYf")
|
|
||||||
return scaledYf
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalFoundationApi::class)
|
@OptIn(ExperimentalFoundationApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun shapes(zoomableState: ZoomableState) {
|
fun shapes(zoomableState: ZoomableState) {
|
||||||
|
|
@ -215,13 +193,6 @@ fun canvas(zoomableState: ZoomableState, first: OcrPage) {
|
||||||
// }
|
// }
|
||||||
)
|
)
|
||||||
{
|
{
|
||||||
val converter = PointConverter(
|
|
||||||
docWidth = 2481,
|
|
||||||
docHeight = 3507,
|
|
||||||
canvasWidth = this.size.width,
|
|
||||||
canvasHeight = this.size.height
|
|
||||||
)
|
|
||||||
|
|
||||||
first.words.forEach { word ->
|
first.words.forEach { word ->
|
||||||
val rect = word.rectangle
|
val rect = word.rectangle
|
||||||
drawRect(
|
drawRect(
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import korlibs.korge.gradle.korge
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.soywiz.korge") version "5.3.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
jvmMainImplementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
|
jvmMainImplementation(project(":libraries:docthor-core"))
|
||||||
|
}
|
||||||
|
|
||||||
|
korge {
|
||||||
|
targetJvm()
|
||||||
|
serializationJson()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,2 @@
|
||||||
|
dependencies:
|
||||||
|
- https://github.com/korlibs/korge-box2d/tree/v0.1.2/korge-box2d##b1737ab3985c0bd3e2f002346ff2ac43ca1ebf48
|
||||||
|
|
@ -0,0 +1,108 @@
|
||||||
|
import de.itkl.docthor.core.DocumentViewer
|
||||||
|
import de.itkl.textprocessing.Document
|
||||||
|
import korlibs.event.Key
|
||||||
|
import korlibs.image.bitmap.context2d
|
||||||
|
import korlibs.korge.*
|
||||||
|
import korlibs.korge.scene.*
|
||||||
|
import korlibs.korge.view.*
|
||||||
|
import korlibs.image.color.*
|
||||||
|
import korlibs.image.format.*
|
||||||
|
import korlibs.io.file.std.*
|
||||||
|
import korlibs.korge.input.*
|
||||||
|
import korlibs.korge.tween.get
|
||||||
|
import korlibs.korge.tween.tween
|
||||||
|
import korlibs.korge.ui.tooltip
|
||||||
|
import korlibs.korge.ui.uiCheckBox
|
||||||
|
import korlibs.logger.AnsiEscape
|
||||||
|
import korlibs.math.geom.*
|
||||||
|
import korlibs.math.geom.shape.toShape2D
|
||||||
|
import korlibs.math.geom.shape.toShape2d
|
||||||
|
import korlibs.math.geom.vector.VectorPath
|
||||||
|
import korlibs.math.interpolation.Easing
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
|
suspend fun main() {
|
||||||
|
val document = DocumentViewer().loadTestDocument(Paths.get("assets/xs-reg"), "00001.jpg")
|
||||||
|
Korge(windowSize = Size(512, 512), backgroundColor = Colors["#2b2b2b"]) {
|
||||||
|
val sceneContainer = sceneContainer()
|
||||||
|
sceneContainer.changeTo { ViewDocument(document) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ViewDocument(private val document: Document) : Scene() {
|
||||||
|
override suspend fun SContainer.sceneMain() {
|
||||||
|
var scaleFactor = 0.5
|
||||||
|
var offset = Point(0.0,0.0)
|
||||||
|
val moveFactor = 50.0
|
||||||
|
var rotationFactor = 0.0
|
||||||
|
scale(scaleFactor)
|
||||||
|
|
||||||
|
suspend fun zoom(amount: Double) {
|
||||||
|
scaleFactor += amount
|
||||||
|
tween(
|
||||||
|
this@sceneMain::scale[scaleFactor],
|
||||||
|
time = 0.1.seconds,
|
||||||
|
easing = Easing.EASE_IN_OUT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun moveX(amount: Double) {
|
||||||
|
offset = offset.copy(x = offset.x + amount)
|
||||||
|
tween(
|
||||||
|
this@sceneMain::x[offset.x],
|
||||||
|
time = 0.1.seconds,
|
||||||
|
easing = Easing.EASE_IN_OUT)
|
||||||
|
}
|
||||||
|
suspend fun moveY(amount: Double) {
|
||||||
|
offset = offset.copy(y = offset.y + amount)
|
||||||
|
tween(
|
||||||
|
this@sceneMain::y[offset.y],
|
||||||
|
time = 0.1.seconds,
|
||||||
|
easing = Easing.EASE_IN_OUT)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun rotateBy(amount: Double){
|
||||||
|
rotationFactor += amount
|
||||||
|
tween(
|
||||||
|
this@sceneMain::rotation[rotationFactor.degrees],
|
||||||
|
time = 0.1.seconds,
|
||||||
|
easing = Easing.EASE_IN_OUT
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
keys {
|
||||||
|
down { key ->
|
||||||
|
when (key.key) {
|
||||||
|
Key.EQUAL -> zoom(0.05)
|
||||||
|
Key.MINUS -> zoom(-0.05)
|
||||||
|
Key.UP -> moveY(moveFactor)
|
||||||
|
Key.DOWN -> moveY(-moveFactor)
|
||||||
|
Key.RIGHT -> moveX(moveFactor)
|
||||||
|
Key.LEFT -> moveX(-moveFactor)
|
||||||
|
Key.E -> rotateBy(10.0)
|
||||||
|
Key.R -> rotateBy(-10.0)
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
val imageFile = localCurrentDirVfs["assets/xs-reg/00001.jpg"].readBitmap()
|
||||||
|
image(imageFile)
|
||||||
|
|
||||||
|
uiCheckBox { text = "foo" }
|
||||||
|
|
||||||
|
document.retrieveOcrPages().first().words.forEach { word ->
|
||||||
|
solidRect(
|
||||||
|
width = word.rectangle.width,
|
||||||
|
height = word.rectangle.height,
|
||||||
|
color = Colors.AQUAMARINE.withA(128)
|
||||||
|
) {
|
||||||
|
x = word.rectangle.x
|
||||||
|
y = word.rectangle.y
|
||||||
|
}.onClick {
|
||||||
|
println(word.text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
|
|
@ -0,0 +1,3 @@
|
||||||
|
dependencies {
|
||||||
|
implementation(project(":libraries:httpClient"))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,68 @@
|
||||||
|
import com.github.ajalt.clikt.core.CliktCommand
|
||||||
|
import com.github.ajalt.clikt.core.subcommands
|
||||||
|
import com.github.ajalt.clikt.parameters.options.convert
|
||||||
|
import com.github.ajalt.clikt.parameters.options.option
|
||||||
|
import com.github.ajalt.clikt.parameters.options.required
|
||||||
|
import com.github.ajalt.clikt.parameters.types.int
|
||||||
|
import de.itkl.httpClient.clients.TaskWaitStatus
|
||||||
|
import de.itkl.httpClient.clients.XsClient
|
||||||
|
import de.itkl.httpClient.httpClientModule
|
||||||
|
import kotlinx.coroutines.*
|
||||||
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
|
import java.util.Random
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.koin.core.context.GlobalContext.startKoin
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
class Cli : CliktCommand() {
|
||||||
|
override fun run() {}
|
||||||
|
|
||||||
|
inner class StressAnalyse : CliktCommand(name = "stress-analyse") {
|
||||||
|
val inputDirectory: File by option(help="Input directory path").convert { File(it) }.required()
|
||||||
|
val tasks: Int by option(help="Number of tasks").int().required()
|
||||||
|
|
||||||
|
override fun run() {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
subcommands(StressAnalyse())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class XsCli : KoinComponent {
|
||||||
|
private val xsClient: XsClient by inject()
|
||||||
|
|
||||||
|
suspend fun run(tasks: Int, inputDirectory: Path) = coroutineScope {
|
||||||
|
val files = inputDirectory.toFile()
|
||||||
|
.listFiles()!!
|
||||||
|
.toList()
|
||||||
|
.filter { it.isFile }.filter { it.extension in listOf("pdf", "ttf", "jpg", "jpeg") }
|
||||||
|
.filter { !it.name.startsWith(".") }
|
||||||
|
val random = Random()
|
||||||
|
|
||||||
|
val (success, error) = (0..<tasks)
|
||||||
|
.map {
|
||||||
|
val file = files[random.nextInt(files.size)]
|
||||||
|
async {
|
||||||
|
val taskRef = xsClient.analyse(file.toPath())
|
||||||
|
xsClient.waitFor(taskRef)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.awaitAll()
|
||||||
|
.partition { it.status == TaskWaitStatus.SUCCESS }
|
||||||
|
|
||||||
|
println("Summary: ${success.size + error.size}: ${success.size}/${error.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun main(args: Array<String>) {
|
||||||
|
startKoin {
|
||||||
|
modules(httpClientModule)
|
||||||
|
}
|
||||||
|
XsCli().run(tasks = 100, inputDirectory = Paths.get("assets/xs-reg"))
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
import korlibs.korge.gradle.korge
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
id("com.soywiz.korge") version "5.3.0"
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
jvmMainImplementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
|
jvmMainImplementation(project(":libraries:docthor-core"))
|
||||||
|
}
|
||||||
|
|
||||||
|
korge {
|
||||||
|
targetJvm()
|
||||||
|
serializationJson()
|
||||||
|
}
|
||||||
|
|
@ -3,5 +3,8 @@ project(":libraries").subprojects {
|
||||||
}
|
}
|
||||||
|
|
||||||
project(":apps").subprojects {
|
project(":apps").subprojects {
|
||||||
|
if(name != "documentViewerKorge" && name != "xsViewer") {
|
||||||
apply(plugin = "docthor.kotlin-application-conventions")
|
apply(plugin = "docthor.kotlin-application-conventions")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
|
import gradle.kotlin.dsl.accessors._6ebf0b5d05ec1eff67605a516c5db18b.implementation
|
||||||
import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.api
|
import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.api
|
||||||
|
import gradle.kotlin.dsl.accessors._d9dcfd1a467b0b6fe90c5571a57aa558.testImplementation
|
||||||
import org.gradle.api.plugins.jvm.JvmTestSuite
|
import org.gradle.api.plugins.jvm.JvmTestSuite
|
||||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||||
|
|
||||||
|
|
@ -13,13 +15,15 @@ repositories {
|
||||||
dependencies {
|
dependencies {
|
||||||
val koin_version = "3.5.3"
|
val koin_version = "3.5.3"
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
|
||||||
implementation("io.insert-koin:koin-core:$koin_version")
|
api("io.insert-koin:koin-core:$koin_version")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.5.0")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
|
||||||
|
|
||||||
|
|
||||||
api("io.github.oshai:kotlin-logging-jvm:5.1.0")
|
api("io.github.oshai:kotlin-logging-jvm:5.1.0")
|
||||||
testImplementation("io.insert-koin:koin-test:$koin_version")
|
testImplementation("io.insert-koin:koin-test:$koin_version")
|
||||||
|
testImplementation("ch.qos.logback:logback-classic:1.4.14")
|
||||||
|
testImplementation("com.willowtreeapps.assertk:assertk:0.28.0")
|
||||||
}
|
}
|
||||||
|
|
||||||
java {
|
java {
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,9 @@ batik = "1.17"
|
||||||
versionchecker = "0.50.0"
|
versionchecker = "0.50.0"
|
||||||
mavenpublish = "0.25.3"
|
mavenpublish = "0.25.3"
|
||||||
|
|
||||||
|
[plugins]
|
||||||
|
korge = { id = "com.soywiz.korge", version = "5.3.0" }
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
compose-desktop = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose" }
|
compose-desktop = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "compose" }
|
||||||
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
kotlin-gradlePlugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" }
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
dependencies {
|
||||||
|
fun addProjects(vararg names: String) {
|
||||||
|
names.forEach {
|
||||||
|
api(project(":libraries:$it"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addProjects(
|
||||||
|
"assetmanager",
|
||||||
|
"core-api",
|
||||||
|
"textprocessing",
|
||||||
|
"httpClient",
|
||||||
|
"tui",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
package de.itkl.docthor.core
|
||||||
|
|
||||||
|
import de.itkl.assetmanager.assetManagerModule
|
||||||
|
import de.itkl.core_api.coreApiModule
|
||||||
|
import de.itkl.httpClient.clients.MsOcr
|
||||||
|
import de.itkl.httpClient.httpClientModule
|
||||||
|
import de.itkl.textprocessing.CorpusFactory
|
||||||
|
import de.itkl.textprocessing.Document
|
||||||
|
import de.itkl.textprocessing.textProcessingModule
|
||||||
|
import de.itkl.tui.tuiModule
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
class DocumentViewer : KoinComponent {
|
||||||
|
|
||||||
|
init {
|
||||||
|
startKoin {
|
||||||
|
modules(
|
||||||
|
coreApiModule,
|
||||||
|
textProcessingModule,
|
||||||
|
tuiModule,
|
||||||
|
assetManagerModule,
|
||||||
|
httpClientModule
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suspend fun loadTestDocument(corpusPath: Path, documentName: String): Document {
|
||||||
|
val corpus = CorpusFactory().load(corpusPath.toAbsolutePath().toString())
|
||||||
|
val document = corpus.document(documentName)
|
||||||
|
val ocrExtractor: MsOcr by inject()
|
||||||
|
document.process(ocrExtractor)
|
||||||
|
return document
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -12,4 +12,7 @@ dependencies {
|
||||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||||
|
implementation("com.akuleshov7:ktoml-core:0.5.1")
|
||||||
|
implementation("com.akuleshov7:ktoml-file:0.5.1")
|
||||||
|
implementation("com.github.ben-manes.caffeine:caffeine:3.1.8")
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
package de.itkl.httpClient
|
||||||
|
|
||||||
|
class BearerTokenCache {
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
package de.itkl.httpClient
|
||||||
|
|
||||||
|
import de.itkl.httpClient.implementation.SmartCloudAuthStrategy
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.ktor.client.plugins.api.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.util.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
|
private val Log = KotlinLogging.logger { }
|
||||||
|
val smartCloudAuthPlugin = createClientPlugin("SmartCloudAuth") {
|
||||||
|
val smartCloudAuthStrategy = SmartCloudAuthStrategy()
|
||||||
|
on(Send) { request ->
|
||||||
|
smartCloudAuthStrategy.login(client, request)?.let { token ->
|
||||||
|
request.headers {
|
||||||
|
set(HttpHeaders.Authorization, token.toBearer())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proceed(request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
package de.itkl.httpClient
|
||||||
|
|
||||||
|
import java.security.cert.X509Certificate
|
||||||
|
import javax.net.ssl.X509TrustManager
|
||||||
|
|
||||||
|
class TrustAllX509TrustManager : X509TrustManager {
|
||||||
|
override fun getAcceptedIssuers(): Array<X509Certificate?> = arrayOfNulls(0)
|
||||||
|
|
||||||
|
override fun checkClientTrusted(certs: Array<X509Certificate?>?, authType: String?) {}
|
||||||
|
|
||||||
|
override fun checkServerTrusted(certs: Array<X509Certificate?>?, authType: String?) {}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
|
||||||
|
interface AuthStrategy {
|
||||||
|
suspend fun login(
|
||||||
|
httpClient: HttpClient,
|
||||||
|
request: HttpRequestBuilder
|
||||||
|
): AuthenticationToken?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,23 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
import kotlinx.datetime.Instant
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
sealed class AuthenticationToken {
|
||||||
|
abstract val expires: Expires
|
||||||
|
abstract fun toBearer(): String
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class BearerToken(val token: String, val validUntil: Instant) : AuthenticationToken() {
|
||||||
|
override val expires: Expires
|
||||||
|
get() = Expires.ExpiresAt(validUntil)
|
||||||
|
|
||||||
|
override fun toBearer(): String {
|
||||||
|
return "Bearer $token"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sealed class Expires {
|
||||||
|
data object Never : Expires()
|
||||||
|
data class ExpiresAt(val instant: Instant) : Expires()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
import io.ktor.http.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
|
||||||
|
typealias UrlMatcher = (Url) -> Boolean
|
||||||
|
class Authenticator : KoinComponent {
|
||||||
|
fun addStrategy(strategy: AuthStrategy, urlMatcher: UrlMatcher) {}
|
||||||
|
|
||||||
|
fun requiresAuthentication(url: Url): Boolean {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
suspend fun authenticate(url: Url, username: String): AuthenticationToken? {
|
||||||
|
TODO()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
sealed class Credentials {
|
||||||
|
@Serializable
|
||||||
|
data class LoginAndPassword(val username: String, val password: String) : Credentials()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
package de.itkl.httpClient.auth
|
||||||
|
|
||||||
|
interface CredentialsProvider {
|
||||||
|
suspend fun lookupByUsername(username: String): Credentials?
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
package de.itkl.httpClient.clients
|
||||||
|
|
||||||
|
import io.ktor.client.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
|
||||||
|
abstract class RestClient : KoinComponent {
|
||||||
|
private val httpClient by inject<HttpClient>()
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,134 @@
|
||||||
|
package de.itkl.httpClient.clients
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.request.forms.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.client.utils.EmptyContent.contentType
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.http.content.*
|
||||||
|
import io.ktor.util.*
|
||||||
|
import io.ktor.util.cio.*
|
||||||
|
import io.ktor.utils.io.*
|
||||||
|
import io.ktor.utils.io.streams.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.awt.SystemColor.info
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
import java.util.UUID
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.io.path.isRegularFile
|
||||||
|
import kotlin.io.path.name
|
||||||
|
|
||||||
|
private val Log = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
class XsClient : KoinComponent {
|
||||||
|
private val httpClient by inject<HttpClient>()
|
||||||
|
|
||||||
|
suspend fun waitFor(task: XsTask): WaitForResponse {
|
||||||
|
Log.info { "Wait for competition: $task" }
|
||||||
|
val response = httpClient.get {
|
||||||
|
url("http://localhost:8080/api/v1/analyse/tasks/wait/${task.xsTaskId.taskId}")
|
||||||
|
user = "xs.dev.klara"
|
||||||
|
}
|
||||||
|
val result = response.body<WaitForResponse>()
|
||||||
|
Log.info { "Waiting done for task $task: ${response.status}: $result" }
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun analyseResult(task: XsTask) {
|
||||||
|
val response = httpClient.get {
|
||||||
|
url("http://localhost:8080/api/v1/analyse-async-result/${task.xsTaskId.taskId}")
|
||||||
|
user = "xs.dev.klara"
|
||||||
|
}
|
||||||
|
check(response.status.isSuccess()) {
|
||||||
|
"HTTP Error"
|
||||||
|
}
|
||||||
|
val text = response.bodyAsText()
|
||||||
|
println(text)
|
||||||
|
}
|
||||||
|
suspend fun analyse(image: Path): TaskReference {
|
||||||
|
Log.info { "Starting analysis for image at path: $image" }
|
||||||
|
|
||||||
|
check(image.isRegularFile()) {
|
||||||
|
"The provided path $image is not a file"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info { "Submitting form with binary data to http://localhost:8080/api/v1/analyse-async" }
|
||||||
|
val response = httpClient.submitFormWithBinaryData(
|
||||||
|
url = "http://localhost:8080/api/v1/analyse-async",
|
||||||
|
formData {
|
||||||
|
append(
|
||||||
|
"image",
|
||||||
|
image.toChannelProvider(),
|
||||||
|
Headers.build {
|
||||||
|
append(HttpHeaders.ContentType, ContentType.defaultForFile(image))
|
||||||
|
append(HttpHeaders.ContentDisposition, """filename="${image.name}"""")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
user = "xs.dev.klara"
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.info { "Received the response from the server." }
|
||||||
|
|
||||||
|
if(response.status.isSuccess()) {
|
||||||
|
Log.info { "Successful response with status: ${response.status}" }
|
||||||
|
return response.body()
|
||||||
|
} else {
|
||||||
|
val responseText = response.bodyAsText()
|
||||||
|
Log.warn { "Error creating analyse task: ${response.status}: $responseText" }
|
||||||
|
error("Could not create analyse task: $responseText")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
var HttpRequestBuilder.user: String?
|
||||||
|
get() = this.attributes[AttributeKey(("username"))]
|
||||||
|
set(value) = value?.let { this.attributes.put(AttributeKey("username"), it) } ?: this.attributes.remove(
|
||||||
|
AttributeKey("username")
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
interface XsTask {
|
||||||
|
val xsTaskId: XsTaskId
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class TaskReference(
|
||||||
|
override val xsTaskId: XsTaskId
|
||||||
|
) : XsTask {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "Task($xsTaskId)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class XsTaskId(val tenantId: String, val taskId: String) {
|
||||||
|
override fun toString(): String {
|
||||||
|
return "$tenantId/$taskId"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class WaitForResponse(
|
||||||
|
val xsTaskId: XsTaskId,
|
||||||
|
val status: TaskWaitStatus,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
enum class TaskWaitStatus {
|
||||||
|
ERROR,
|
||||||
|
SUCCESS
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Path.toChannelProvider(): ChannelProvider {
|
||||||
|
val file = toFile()
|
||||||
|
return ChannelProvider(file.length()) { file.readChannel() }
|
||||||
|
}
|
||||||
|
|
@ -1,14 +1,41 @@
|
||||||
package de.itkl.httpClient
|
package de.itkl.httpClient
|
||||||
|
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.engine.*
|
||||||
import io.ktor.client.engine.cio.*
|
import io.ktor.client.engine.cio.*
|
||||||
|
import io.ktor.client.plugins.*
|
||||||
import io.ktor.client.plugins.contentnegotiation.*
|
import io.ktor.client.plugins.contentnegotiation.*
|
||||||
|
import io.ktor.http.*
|
||||||
import io.ktor.serialization.kotlinx.json.*
|
import io.ktor.serialization.kotlinx.json.*
|
||||||
|
import java.net.InetSocketAddress
|
||||||
|
import java.net.Socket
|
||||||
|
import kotlin.time.Duration.Companion.minutes
|
||||||
|
|
||||||
|
private val Log = KotlinLogging.logger { }
|
||||||
fun createHttpClient(): HttpClient {
|
fun createHttpClient(): HttpClient {
|
||||||
return HttpClient(CIO) {
|
return HttpClient(CIO) {
|
||||||
install(ContentNegotiation) {
|
install(ContentNegotiation) {
|
||||||
json()
|
json()
|
||||||
}
|
}
|
||||||
|
install(smartCloudAuthPlugin)
|
||||||
|
install(HttpTimeout) {
|
||||||
|
requestTimeoutMillis = 30.minutes.inWholeMilliseconds
|
||||||
|
}
|
||||||
|
|
||||||
|
engine {
|
||||||
|
val isPortOpen = try {
|
||||||
|
Socket().use { it.connect(InetSocketAddress("localhost", 9999), 200) }
|
||||||
|
true
|
||||||
|
} catch (ex: Exception) {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
if (isPortOpen) {
|
||||||
|
proxy = ProxyBuilder.http(Url("http://localhost:9999"))
|
||||||
|
}
|
||||||
|
https {
|
||||||
|
trustManager = TrustAllX509TrustManager()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
package de.itkl.httpClient
|
package de.itkl.httpClient
|
||||||
|
|
||||||
|
import de.itkl.httpClient.auth.CredentialsProvider
|
||||||
import de.itkl.httpClient.clients.MsOcr
|
import de.itkl.httpClient.clients.MsOcr
|
||||||
|
import de.itkl.httpClient.clients.XsClient
|
||||||
|
import de.itkl.httpClient.implementation.StaticCredentialsProvider
|
||||||
import io.ktor.client.*
|
import io.ktor.client.*
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
val httpClientModule = module {
|
val httpClientModule = module {
|
||||||
single<HttpClient> { createHttpClient() }
|
single<HttpClient> { createHttpClient() }
|
||||||
single<MsOcr> { MsOcr() }
|
single<MsOcr> { MsOcr() }
|
||||||
|
single<CredentialsProvider> {
|
||||||
|
val homeDirectory = System.getProperty("user.home")
|
||||||
|
val credentialsFilePath = Paths.get(homeDirectory, ".auth.toml")
|
||||||
|
StaticCredentialsProvider.load(credentialsFilePath)
|
||||||
|
}
|
||||||
|
single<XsClient> { XsClient() }
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
package de.itkl.httpClient.implementation
|
||||||
|
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine
|
||||||
|
import com.github.benmanes.caffeine.cache.Cache
|
||||||
|
import de.itkl.httpClient.auth.*
|
||||||
|
import de.itkl.httpClient.clients.user
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import io.ktor.client.*
|
||||||
|
import io.ktor.client.call.*
|
||||||
|
import io.ktor.client.request.*
|
||||||
|
import io.ktor.client.statement.*
|
||||||
|
import io.ktor.http.*
|
||||||
|
import io.ktor.util.*
|
||||||
|
import org.koin.core.component.KoinComponent
|
||||||
|
import org.koin.core.component.inject
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
private val Log = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
class SmartCloudAuthStrategy : AuthStrategy, KoinComponent {
|
||||||
|
private val credentialsProvider: CredentialsProvider by inject()
|
||||||
|
|
||||||
|
private val loginMutex = Mutex()
|
||||||
|
|
||||||
|
// Cache Helpers
|
||||||
|
private val tokenCache: Cache<String, AuthenticationToken> = Caffeine.newBuilder()
|
||||||
|
.expireAfterWrite(1, TimeUnit.HOURS)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override suspend fun login(httpClient: HttpClient, request: HttpRequestBuilder): AuthenticationToken? {
|
||||||
|
Log.debug { "Attempting login..." }
|
||||||
|
val user = request.attributes.getOrNull(AttributeKey<String>("username")) ?: run {
|
||||||
|
Log.info { "No username is specified for this request" }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenCache.getIfPresent(user)?.let {
|
||||||
|
Log.info { "Returning cached token for user: $user" }
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
return loginMutex.withLock {
|
||||||
|
tokenCache.getIfPresent(user)?.let {
|
||||||
|
Log.info { "Returning cached token for user: $user" }
|
||||||
|
return it
|
||||||
|
}
|
||||||
|
|
||||||
|
val credentials: Credentials = credentialsProvider.lookupByUsername(user) ?: error("No credentials found for user: $user")
|
||||||
|
val loginAndPassword: Credentials.LoginAndPassword = credentials as? Credentials.LoginAndPassword ?: error("Only username and password is supported by smartcloud auth login")
|
||||||
|
|
||||||
|
Log.debug { "User: $user, using smartcloud auth login" }
|
||||||
|
|
||||||
|
val response = httpClient.post {
|
||||||
|
url("https://api.internal.insiders.cloud/1/rest/accounts/authentication/requesttoken")
|
||||||
|
contentType(ContentType.Application.Json)
|
||||||
|
setBody(loginAndPassword)
|
||||||
|
}
|
||||||
|
check(response.status.isSuccess()) {
|
||||||
|
"could not login into smart cloud: ${response.bodyAsText()}"
|
||||||
|
}
|
||||||
|
val token = response.body<BearerToken>()
|
||||||
|
Log.info { "Login successful. Valid until: ${token.validUntil}" }
|
||||||
|
|
||||||
|
// Cache the token after successful login
|
||||||
|
tokenCache.put(user, token)
|
||||||
|
|
||||||
|
token
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
package de.itkl.httpClient.implementation
|
||||||
|
|
||||||
|
import com.akuleshov7.ktoml.Toml
|
||||||
|
import de.itkl.httpClient.auth.Credentials
|
||||||
|
import de.itkl.httpClient.auth.CredentialsProvider
|
||||||
|
import io.github.oshai.kotlinlogging.KotlinLogging
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import java.nio.file.Files
|
||||||
|
import java.nio.file.Path
|
||||||
|
|
||||||
|
private val Log = KotlinLogging.logger { }
|
||||||
|
|
||||||
|
class StaticCredentialsProvider(private val credentials: Map<String, Credentials.LoginAndPassword>) : CredentialsProvider {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun load(path: Path): StaticCredentialsProvider {
|
||||||
|
Log.info { "Loading credentials from path: $path" }
|
||||||
|
val content = Files.readString(path)
|
||||||
|
val credentials = Toml.decodeFromString<CredentialsTable>(content).toCredentialsMap()
|
||||||
|
Log.info { "Credentials loaded successfully" }
|
||||||
|
return StaticCredentialsProvider(credentials)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun lookupByUsername(username: String): Credentials? {
|
||||||
|
Log.info { "Looking up credentials by username: $username" }
|
||||||
|
val credentialsEntry = credentials[username]
|
||||||
|
if (credentialsEntry != null) {
|
||||||
|
Log.info { "Credentials found for username: $username" }
|
||||||
|
return credentialsEntry
|
||||||
|
}
|
||||||
|
Log.info { "No credentials found for username: $username" }
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class CredentialsTable(
|
||||||
|
val credentials: Map<String, Map<String, String>>
|
||||||
|
) {
|
||||||
|
fun toCredentialsMap(): Map<String, Credentials.LoginAndPassword> {
|
||||||
|
return credentials.mapValues {
|
||||||
|
val login = it.value["Login"] ?: throw IllegalArgumentException("Missing login")
|
||||||
|
val password = it.value["Password"] ?: throw IllegalArgumentException("Missing password")
|
||||||
|
Credentials.LoginAndPassword(login, password)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
package de.itkl.httpClient.implementation
|
||||||
|
|
||||||
|
import assertk.assertThat
|
||||||
|
import assertk.assertions.isDataClassEqualTo
|
||||||
|
import assertk.assertions.isNotNull
|
||||||
|
import de.itkl.httpClient.auth.Credentials
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.junit.jupiter.api.Test
|
||||||
|
import java.nio.file.Paths
|
||||||
|
|
||||||
|
|
||||||
|
class StaticCredentialsProviderTest {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `can load credentials file`() = runBlocking {
|
||||||
|
val target = StaticCredentialsProvider.load(Paths.get("../../assets/credentials.toml"))
|
||||||
|
val credentials = target.lookupByUsername("test user")
|
||||||
|
assertThat(credentials)
|
||||||
|
.isNotNull()
|
||||||
|
.isDataClassEqualTo(Credentials.LoginAndPassword("TestiTest", "Secret"))
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,3 +1,11 @@
|
||||||
|
|
||||||
|
pluginManagement {
|
||||||
|
repositories {
|
||||||
|
mavenCentral();
|
||||||
|
google();
|
||||||
|
gradlePluginPortal() }
|
||||||
|
}
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0"
|
id("org.gradle.toolchains.foojay-resolver-convention") version "0.4.0"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue