还原漫画图片

master
pan 3 years ago
commit cbc9a26b7d
  1. 115
      .gitignore
  2. 3
      README.md
  3. 125
      build.gradle.kts
  4. 12
      gradle.properties
  5. 5
      gradle/wrapper/gradle-wrapper.properties
  6. 185
      gradlew
  7. 89
      gradlew.bat
  8. 3
      settings.gradle.kts
  9. 37
      src/commonMain/kotlin/Data.kt
  10. 21
      src/jsMain/kotlin/client.kt
  11. 308
      src/jsMain/kotlin/image.kt
  12. 153
      src/jsMain/kotlin/welcome.kt
  13. 45
      src/jsTest/kotlin/Test.kt
  14. 19
      src/jvmMain/kotlin/plugins/HTTP.kt
  15. 14
      src/jvmMain/kotlin/plugins/Monitoring.kt
  16. 157
      src/jvmMain/kotlin/plugins/Routing.kt
  17. 11
      src/jvmMain/kotlin/plugins/Serialization.kt
  18. 8
      src/jvmMain/kotlin/plugins/WebSockets.kt
  19. 47
      src/jvmMain/kotlin/server.kt
  20. 22
      src/jvmMain/resources/application.conf
  21. 14
      src/jvmMain/resources/logback.xml
  22. 0
      src/jvmMain/resources/static/image/.gitkeep
  23. 78
      src/jvmTest/kotlin/ApplicationTest.kt

115
.gitignore vendored

@ -0,0 +1,115 @@
### JetBrains template
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
# User-specific stuff
.idea/**/workspace.xml
.idea/**/tasks.xml
.idea/**/usage.statistics.xml
.idea/**/dictionaries
.idea/**/shelf
# Generated files
.idea/**/contentModel.xml
# Sensitive or high-churn files
.idea/**/dataSources/
.idea/**/dataSources.ids
.idea/**/dataSources.local.xml
.idea/**/sqlDataSources.xml
.idea/**/dynamic.xml
.idea/**/uiDesigner.xml
.idea/**/dbnavigator.xml
# Gradle
.idea/**/gradle.xml
.idea/**/libraries
# Gradle and Maven with auto-import
# When using Gradle or Maven with auto-import, you should exclude module files,
# since they will be recreated, and may cause churn. Uncomment if using
# auto-import.
# .idea/artifacts
# .idea/compiler.xml
# .idea/jarRepositories.xml
# .idea/modules.xml
# .idea/*.iml
# .idea/modules
# *.iml
# *.ipr
# CMake
cmake-build-*/
# Mongo Explorer plugin
.idea/**/mongoSettings.xml
# File-based project format
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Cursive Clojure plugin
.idea/replstate.xml
# Crashlytics plugin (for Android Studio and IntelliJ)
com_crashlytics_export_strings.xml
crashlytics.properties
crashlytics-build.properties
fabric.properties
# Editor-based Rest Client
.idea/httpRequests
# Android studio 3.1+ serialized cache file
.idea/caches/build_file_checksums.ser
### Gradle template
.gradle
**/build/
!src/**/build/
# Ignore Gradle GUI config
gradle-app.setting
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar
# Cache of project
.gradletasknamecache
# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898
# gradle/wrapper/gradle-wrapper.properties
### Kotlin template
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Mobile Tools for Java (J2ME)
.mtj.tmp/
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
.idea

@ -0,0 +1,3 @@
# 爬取[takeshobo](https://gammaplus.takeshobo.co.jp/) 网站上的漫画
## 服务端:[ktor](https://ktor.io/)
## 客户端:[Kotlin/JS+React](https://kotlinlang.org/docs/js-get-started.html)

@ -0,0 +1,125 @@
import org.jetbrains.kotlin.gradle.targets.js.webpack.KotlinWebpack
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
val kotlin_version="1.5.20"
kotlin("multiplatform") version kotlin_version
kotlin("plugin.serialization") version kotlin_version
application
}
val serialization_version:String by project
val kotlin_version:String by project
val logback_version:String by project
val ktor_version:String by project
val `kotlin-react-version`:String by project
val `kotlin-styled-version`:String by project
group = "jp.co.takeshobo"
version = "1.0-SNAPSHOT"
repositories {
jcenter()
mavenCentral()
maven { url = uri("https://maven.pkg.jetbrains.space/kotlin/p/kotlin/kotlin-js-wrappers") }
maven { url = uri("https://maven.pkg.jetbrains.space/public/p/kotlinx-html/maven") }
}
kotlin {
jvm {
compilations.all {
kotlinOptions.jvmTarget = "1.8"
}
testRuns["test"].executionTask.configure {
useJUnit()
}
withJava()
}
js(LEGACY) {
binaries.executable()
browser {
commonWebpackConfig {
cssSupport.enabled = true
}
testTask {
useKarma {
useChrome()
}
}
}
}
sourceSets {
val commonMain by getting{
dependencies {
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlin_version")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$serialization_version")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting {
dependencies {
implementation("io.ktor:ktor-server-netty:$ktor_version")
implementation("io.ktor:ktor-client-core:$ktor_version")
implementation("io.ktor:ktor-client-cio:$ktor_version")
implementation("io.ktor:ktor-html-builder:$ktor_version")
implementation("io.ktor:ktor-serialization:$ktor_version")
implementation("io.ktor:ktor-websockets:$ktor_version")
implementation("ch.qos.logback:logback-classic:$logback_version")
}
}
val jvmTest by getting{
dependencies {
implementation("io.ktor:ktor-server-tests:$ktor_version")
}
}
val jsMain by getting {
dependencies {
implementation("org.jetbrains.kotlin-wrappers:kotlin-react:${`kotlin-react-version`}")
implementation("org.jetbrains.kotlin-wrappers:kotlin-react-dom:${`kotlin-react-version`}")
implementation("org.jetbrains.kotlin-wrappers:kotlin-styled:${`kotlin-styled-version`}")
}
}
val jsTest by getting
}
}
application{
mainClass.set("ServerKt")
}
tasks {
"run"(JavaExec::class) {
environment("CHROME_BIN","F:\\ChromeUpdater\\chrome.exe")
}
}
tasks.getByName<KotlinWebpack>("jsBrowserProductionWebpack") {
outputFileName = "js.js"
}
tasks.getByName<Jar>("jvmJar") {
dependsOn(tasks.getByName("jsBrowserProductionWebpack"))
val jsBrowserProductionWebpack = tasks.getByName<KotlinWebpack>("jsBrowserProductionWebpack")
from(File(jsBrowserProductionWebpack.destinationDirectory, jsBrowserProductionWebpack.outputFileName))
}
tasks.getByName<JavaExec>("run") {
dependsOn(tasks.getByName<Jar>("jvmJar"))
classpath(tasks.getByName<Jar>("jvmJar"))
}
tasks.withType<KotlinCompile> {
kotlinOptions {
freeCompilerArgs = listOf("-Xopt-in=kotlin.RequiresOptIn")
}
}
tasks.withType<ProcessResources>{
duplicatesStrategy = DuplicatesStrategy.INCLUDE
}

@ -0,0 +1,12 @@
kotlin.code.style=official
kotlin.mpp.enableGranularSourceSetsMetadata=true
kotlin.native.enableDependencyPropagation=false
kotlin.js.generate.executable.default=false
kotlin.mpp.stability.nowarn=true
serialization_version=1.2.1
kotlin_version=1.5.20
logback_version=1.2.3
ktor_version=1.6.0
kotlin-react-version=17.0.2-pre.213-kotlin-1.5.20
kotlin-styled-version=5.3.0-pre.213-kotlin-1.5.20
systemProp.file.encoding=UTF-8

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

185
gradlew vendored

@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"

89
gradlew.bat vendored

@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

@ -0,0 +1,3 @@
rootProject.name = "manga"

@ -0,0 +1,37 @@
import kotlinx.serialization.Contextual
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class i(val src: String, val width: Int, val height: Int)
@Serializable
data class resources(val i: i)
@Serializable
data class area(val href: String, val left: Int, val top: Int, val right: Int, val bottom: Int)
@Serializable
data class view(val width: Int, val height: Int, val coords: List<String>, val areas: List<area>? = null)
@Serializable
data class t(@SerialName("ptimg-version") val ptimg_version: Int,
val resources: resources, val views: List<view>)
@Serializable
data class ApiResponse<T>(val message:String,@Contextual val body:T?=null)
@Serializable
data class MessageResponse(val message: String)
@Serializable
data class UrlResult(val originImagePath:String,val serverImagePath:String, val t:t)
@Serializable
data class ParseTask(val total:Int,val finish:Int,val percentage:Float)
data class UrlParam(val url:String,val html:String)
const val websocketPath="/webSocket"
const val websiteTitle="朴实无华的漫画解析工具"

@ -0,0 +1,21 @@
import react.dom.render
import kotlinx.browser.document
import kotlinx.browser.window
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
fun main() {
window.onload = {
val webSocket=WebSocket("ws://localhost:8080${websocketPath}")
webSocket.onopen={event: Event -> console.info("打开连接:${event}") }
webSocket.onclose={event: Event -> console.info("关闭连接:${event}") }
webSocket.onerror={event: Event -> console.error("发生错误:${event}") }
render(document.getElementById("root")) {
child(Welcome::class) {
attrs {
this.webSocket=webSocket
}
}
}
}
}

@ -0,0 +1,308 @@
import kotlinext.js.getOwnPropertyNames
import kotlinx.browser.document
import kotlinx.css.head
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.set
import org.w3c.dom.CanvasRenderingContext2D
import org.w3c.dom.HTMLCanvasElement
import org.w3c.dom.Image
import org.w3c.dom.events.Event
import org.w3c.dom.url.URL
import org.w3c.files.Blob
import org.w3c.files.BlobPropertyBag
import kotlin.js.Promise
import kotlin.math.ceil
import kotlin.math.max
import kotlin.math.min
import kotlin.math.round
data class un(val width: Int, val height: Int)
data class coord(
val resid: String,
val xsrc: Int,
val ysrc: Int,
val width: Int,
val height: Int,
val xdest: Int,
val ydest: Int
)
data class formatview(val width: Int, val height: Int, val coords: List<coord>, val areas: List<area>? = null)
data class transfer(val index:Int,val coords: List<coord>)
data class n(val width: Int,val height: Int,val transfers:List<transfer>)
data class N(
val left: Int, val top: Int, val width: Int, val height: Int,
val bottom: Int = top + height, val right: Int = left + width
)
object p {
//speedbinb.js?dmy=016301:formatted:3998
fun Rectangle(t: Int, i: Int, n: Int, r: Int): N {
return N(left = t, top = i, width = n, height = r)
}
//speedbinb.js?dmy=016301:formatted:3966
fun intersect(t:N,i:N):N?{
val n = t.left
val r = t.left + t.width
val e = t.top
val s = t.top + t.height
val h = i.left
val u = i.left + i.width
val o = i.top
val a = i.top + i.height
if (n < u && h < r && e < a && o < s) {
val f = max(n, h)
val c = max(e, o)
return N(f,c,min(r, u) - f,min(s, a) - c)
}
return null
}
}
class e(val Xs: Int = 3, private val Ws: Int = 8, private val Ys: Int = 4, val un: un) {
//speedbinb.js?dmy=016301:formatted:8848
fun Us(t: Int): N {
val i = this.un.height
val n = ceil( (i + this.Ys * (this.Xs - 1)) / this.Ws.toDouble()).toInt()
val r = ceil(t * n / this.Xs.toDouble()).toInt() * this.Ws
val e = ceil((t + 1) * n / this.Xs.toDouble()).toInt() * this.Ws
val s = n * this.Ws
val h = r * i / s
val u = e * i / s
val o = e - r
val a = if (1 == this.Xs) 0 else round(h + (u - h - o) * t / (this.Xs - 1).toDouble()).toInt()
return p.Rectangle(t = 0, i = a, n = this.un.width, r = o)
}
}
object d {
data class e(val resources: resources,val views: List<formatview>)
lateinit var urlResult: UrlResult
//speedbinb.js?dmy=016301:formatted:8699
private fun RS(t: resources, i: String): coord {
val n = (Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire(i)
?: throw IllegalArgumentException("Invalid format for Image Transfer : $i")).groupValues
val r = n[1]
if (r !in t.getOwnPropertyNames())
throw IllegalArgumentException("resid $r not found.")
return coord(
resid = r, xsrc = n[2].toInt(10), ysrc = n[3].toInt(10), width = n[4].toInt(10),
height = n[5].toInt(10), xdest = n[6].toInt(10), ydest = n[7].toInt(10)
)
}
//speedbinb.js?dmy=016301:formatted:8632
private fun FS():e {
val t=urlResult.t
val n =
t.resources.copy(i = t.resources.i.copy(src =urlResult.originImagePath))
return e(resources = n,views = t.views.map { it ->
formatview(width = it.width, height = it.height, coords = it.coords.map {
RS(t = n, i = it)
}, areas = it.areas)
})
}
//speedbinb.js?dmy=016301:formatted:8604
fun Gs():n{
val e= FS()
val u=e.views[0]
return n(width = u.width,height = u.height,transfers = listOf(transfer(index = 0,coords = u.coords)))
}
}
class ImageLoader(val urlResult: UrlResult){
private val canvasHtml=createCanvas()
private var imageHeight=0.0
//speedbinb.js?dmy=016301:formatted:8766
private fun callback(t:un): List<n> {
d.urlResult=urlResult
val n=d.Gs()
// console.info("Gs:")
// console.info(n)
val u=e(un=un(width = n.width,height = n.height))
val s=n.transfers[0].coords
val h= mutableListOf<n>()
repeat(u.Xs){
val r=u.Us(t=it)
// console.info("r:")
// console.info(r)
val e= mutableListOf<coord>()
s.forEach {
t->
// console.info("-------------------")
// console.info("t:")
// console.info(t)
val _i=p.Rectangle(t=t.xdest,i=t.ydest,n=t.width,r=t.height)
// console.info("p.Rectangle(i):")
// console.info(_i)
p.intersect(t=r,i=_i)?.let {
n->
// console.info("p.Rectangle.intersect(n):")
// console.info(n)
// console.info("-------------------")
e.add(coord(resid = t.resid,
xsrc = t.xsrc+(n.left-t.xdest),
ysrc= t.ysrc + (n.top - t.ydest),
width= n.width,
height= n.height,
xdest= n.left - r.left,
ydest = n.top - r.top))
}
}
// console.info("e.size=${e.size}")
h.add(n(width = r.width,height = r.height,transfers = listOf(transfer(index = 0,coords = e))))
}
// console.info(h)
return h.toList()
}
private fun createCanvas(): HTMLCanvasElement {
return document.createElement("canvas") as HTMLCanvasElement
}
//speedbinb.js?dmy=016301:formatted:7976
private fun us(t:n,image:Image){
console.info("开始绘制漫画页")
val canvas:HTMLCanvasElement= createCanvas()
canvas.apply {
width=t.width
height=t.height
val ctx:CanvasRenderingContext2D= getContext("2d") as CanvasRenderingContext2D
ctx.clearRect(0.0, 0.0, width.toDouble(), height.toDouble())
t.transfers.forEach {
i->
i.coords.forEach {
t->
ctx.drawImage(image = image,sx=t.xsrc.toDouble(),sy=t.ysrc.toDouble(),sw=t.width.toDouble(),sh=t.height.toDouble(),
dx=t.xdest.toDouble(),dy=t.ydest.toDouble(),dw=t.width.toDouble(),dh=t.height.toDouble())
}
}
}
canvasToBlob(t=canvas,{blob: Blob ->
console.info("blob.size:${blob.size}")
val i=URL.createObjectURL(blob=blob)
console.info("漫画URL:${i}")
Image().apply {
onload={event: Event ->
canvasHtml.let {
canvas->
val ctx:CanvasRenderingContext2D= canvas.getContext("2d") as CanvasRenderingContext2D
ctx.drawImage(this,0.0,imageHeight,naturalWidth.toDouble(),naturalHeight.toDouble().apply {
imageHeight+=this
console.info("拼接图片imageHeight:${imageHeight}")
})
}
}
src=i
}
})
}
fun create(){
Image().apply {
onload={event: Event ->
console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}")
}
src=canvasHtml.toDataURL()
}
}
//speedbinb.js?dmy=016301:formatted:3798
fun canvasToBlob(
t:HTMLCanvasElement, callback:(t:Blob)->Unit, n:String="image/jpeg", r: Double =.9){
val i=t.toDataURL(type=n,quality = r).split(",")[1]
console.info("url length:${i.length}")
val blobTransfer=w(t=i)
// console.info("w[i]:")
// console.info(blobTransfer)
val blob= Blob(blobParts=arrayOf(blobTransfer),options = BlobPropertyBag(type = n))
console.info("blob size:${blob.size}")
callback(blob)
}
//speedbinb.js?dmy=016301:formatted:3602
fun _m(t: String): Array<Int> {
val i= arrayOf<Int>()
repeat(t.length){
i[t[it].code]=it
}
return i
}
val m=_m("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")
//speedbinb.js?dmy=016301:formatted:3608
fun w(t:String): Uint8Array {
val e=t.length
val s = t.slice(IntRange(e-2,e-1)).split("=").count() - 1
val h = 3 * ((e + 3) / 4) - s
val u = Uint8Array(h)
var i=0
var n=i
while (n<e){
val r=m[t[n].code] shl 18 or (m[t[n + 1].code] shl 12) or (m[t[n + 2].code] shl 6) or m[t[n + 3].code]
u[i]=(r shr 16 and 255).toByte()
u[i+1]=(r shr 8 and 255).toByte()
u[i+2]=(255 and r).toByte()
i+=3
n+=4
}
return u
}
//speedbinb.js?dmy=016301:formatted:8020
fun rebuild(){
hs().then {
console.info("image(naturalWidth:${it.naturalWidth},naturalHeight=${it.naturalHeight})")
val f=un(width = it.naturalWidth,height = it.naturalHeight)
callback(t=f).map {
t->
// val n=t(width = t.width,height = t.height)
us(t=t,image=it)
}
}
}
//speedbinb.js?dmy=016301:formatted:7949
private fun hs(): Promise<Image> {
return Promise(executor = { resolve, reject ->
val e=Image()
// e.crossOrigin="anonymous"
val t=urlResult.serverImagePath
e.onload={
resolve(e)
}
e.onerror = { _: dynamic, _: String, _: Int, _: Int, _: Any? ->
reject(Error("Failed to load image. : $t"))
}
e.onabort = {
reject(Error("Failed to load image. : $t"))
}
e.src=t.apply { console.info("img path:${this}") }
})
}
}

@ -0,0 +1,153 @@
import kotlinx.browser.document
import kotlinx.browser.window
import kotlinx.css.*
import kotlinx.html.InputType
import kotlinx.html.js.onChangeFunction
import kotlinx.html.js.onClickFunction
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import org.w3c.dom.HTMLInputElement
import org.w3c.dom.MessageEvent
import org.w3c.dom.WebSocket
import org.w3c.dom.events.Event
import org.w3c.fetch.RequestInit
import org.w3c.xhr.FormData
import react.RBuilder
import react.RComponent
import react.RProps
import react.RState
import react.dom.*
import styled.css
import styled.styledDiv
import styled.styledH1
import styled.styledInput
external interface WelcomeProps : RProps {
var webSocket:WebSocket
}
data class WelcomeState(var url:String="",var result:String="",var percentage:kotlin.Float=0F) : RState
fun Double.format(digits: Int): String = this.asDynamic().toFixed(digits)
fun Float.format(digits: Int): String = this.asDynamic().toFixed(digits)
@OptIn(ExperimentalJsExport::class)
@JsExport
class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(props) {
init {
state=WelcomeState()
props.webSocket.onmessage={messageEvent: MessageEvent ->
when(val data=messageEvent.data){
is String-> {
if(data.contains("ptimg-version")){
val urlResult=Json.decodeFromString<UrlResult>(data)
console.info("ptimg_version:${urlResult.t.ptimg_version}")
ImageLoader(urlResult = urlResult).apply {
rebuild()
create()
}
}else{
val task=Json.decodeFromString<ParseTask>(data)
state.result="解析进度:${task.percentage}%"
setState(state)
}
}
else->{
console.info("unknow data:${data}")
}
}
}
}
override fun RBuilder.render() {
styledH1 {
css{
textAlign=TextAlign.center
}
+"朴实无华的"
a {
attrs {
href="https://gammaplus.takeshobo.co.jp"
target="_blank"
}
+"漫画"
}
+"解析工具"
}
styledDiv{
css{
textAlign=TextAlign.center
}
styledDiv {
css {
width = LinearDimension.fitContent
margin="0 auto"
}
styledInput {
css {
width = 100.pct
}
attrs{
type=InputType.text
value = state.url
onChangeFunction = { event ->
(event.target as HTMLInputElement).let {
console.info(it.value)
state.url=it.value
setState(
state
)
}
}
}
}
styledDiv {
css{
visibility=Visibility.hidden
height=0.px
paddingLeft=10.px
}
+ state.url
}
}
button {
attrs {
onClickFunction={
val formData=FormData()
formData.append("url",state.url)
window.fetch("/api/json", RequestInit(method = "post",body = formData)).then {
it.text()
}.then {
console.info(it)
}
}
}
+"开始解析"
}
if(state.result!=""){
styledDiv {
+state.result
}
button {
attrs {
onClickFunction={
state.result=""
setState(state)
}
}
+"清空消息"
}
}
}
}
}

@ -0,0 +1,45 @@
import kotlinx.css.p
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.khronos.webgl.Uint8Array
import org.khronos.webgl.set
import kotlin.test.Test
class JsTest {
@Serializable
data class FF(@SerialName("ptimg-version") val c:String)
@Test
fun testParse(){
val c=Json.decodeFromString<t>("{\"ptimg-version\":1,\"resources\":{\"i\":{\"src\":\"0012.jpg\",\"width\":908,\"height\":1264}},\"views\":[{\"width\":844,\"height\":1200,\"coords\":[\"i:4,4+106,150>420,1050\",\"i:118,4+106,150>530,450\",\"i:232,4+106,150>738,0\",\"i:346,4+106,150>526,750\",\"i:460,4+106,150>420,750\",\"i:574,4+106,150>106,300\",\"i:688,4+106,150>318,600\",\"i:802,4+102,150>742,600\",\"i:4,162+106,150>106,450\",\"i:118,162+106,150>526,1050\",\"i:232,162+106,150>0,900\",\"i:346,162+106,150>636,150\",\"i:460,162+102,150>424,900\",\"i:570,162+106,150>0,1050\",\"i:684,162+106,150>106,1050\",\"i:798,162+106,150>0,600\",\"i:4,320+106,150>738,1050\",\"i:118,320+106,150>106,150\",\"i:232,320+106,150>424,600\",\"i:346,320+106,150>420,300\",\"i:460,320+106,150>530,150\",\"i:574,320+106,150>424,0\",\"i:688,320+102,150>742,150\",\"i:798,320+106,150>0,450\",\"i:4,478+106,150>738,750\",\"i:118,478+106,150>632,1050\",\"i:232,478+106,150>106,900\",\"i:346,478+102,150>106,750\",\"i:456,478+106,150>106,600\",\"i:570,478+106,150>424,450\",\"i:684,478+106,150>212,900\",\"i:798,478+106,150>318,450\",\"i:4,636+106,150>632,300\",\"i:118,636+106,150>314,1050\",\"i:232,636+106,150>106,0\",\"i:346,636+106,150>318,0\",\"i:460,636+106,150>314,750\",\"i:574,636+102,150>636,0\",\"i:684,636+106,150>632,900\",\"i:798,636+106,150>738,900\",\"i:4,794+106,150>318,150\",\"i:118,794+106,150>530,600\",\"i:232,794+106,150>212,0\",\"i:346,794+106,150>212,450\",\"i:460,794+106,150>0,150\",\"i:574,794+102,150>636,450\",\"i:684,794+106,150>738,300\",\"i:798,794+106,150>0,750\",\"i:4,952+106,150>208,750\",\"i:118,952+106,150>212,150\",\"i:232,952+106,150>636,600\",\"i:346,952+106,150>212,300\",\"i:460,952+106,150>0,300\",\"i:574,952+102,150>212,1050\",\"i:684,952+106,150>526,900\",\"i:798,952+106,150>424,150\",\"i:4,1110+106,150>530,0\",\"i:118,1110+106,150>526,300\",\"i:232,1110+106,150>632,750\",\"i:346,1110+106,150>738,450\",\"i:460,1110+106,150>212,600\",\"i:574,1110+106,150>0,0\",\"i:688,1110+102,150>318,300\",\"i:798,1110+106,150>318,900\"]}]}")
println(c.ptimg_version)
println(c.resources.i)
println(c.views)
}
@Test
fun testRS(){
val f=Regex("^([^:]+):(\\d+),(\\d+)\\+(\\d+),(\\d+)>(\\d+),(\\d+)\$").matchEntire("i:574,4+106,150>106,600")
println(f?.groupValues)
}
@Test
fun testUnitArray(){
ImageLoader(UrlResult(originImagePath = "",serverImagePath = "",
t=t(ptimg_version = 0,resources = resources(i = i(src = "",width = 0,height = 0)),views = listOf())))
.apply {
m.forEach {
println("m;${it}")
}
}
}
}

@ -0,0 +1,19 @@
package cool.kirito.bili.live.server.plugins
import io.ktor.http.*
import io.ktor.application.*
import io.ktor.features.*
fun Application.configureHTTP() {
install(CORS) {
method(HttpMethod.Options)
method(HttpMethod.Put)
method(HttpMethod.Delete)
method(HttpMethod.Patch)
header(HttpHeaders.Authorization)
header("MyCustomHeader")
allowCredentials = true
anyHost() // @TODO: Don't do this in production if possible. Try to limit it.
}
}

@ -0,0 +1,14 @@
package cool.kirito.bili.live.server.plugins
import io.ktor.features.*
import org.slf4j.event.*
import io.ktor.application.*
import io.ktor.request.*
fun Application.configureMonitoring() {
install(CallLogging) {
level = Level.INFO
filter { call -> call.request.path().startsWith("/") }
}
}

@ -0,0 +1,157 @@
import io.ktor.application.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.html.*
import io.ktor.http.*
import io.ktor.http.cio.websocket.*
import io.ktor.http.content.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.websocket.*
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch
import kotlinx.html.HTML
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
const val website = "https://gammaplus.takeshobo.co.jp"
var taskChannel = Channel<ParseTask>()
val urlResultChannel = Channel<UrlResult>()
val htmlChannel= Channel<UrlParam>()
fun Application.parse(){
log.info("初始化解析任务")
val uploadDir = environment.config.property("ktor.deployment.filePath").getString()
val filePath = environment.classLoader.getResource(uploadDir)?.path.apply { log.info("图片存储目录:${this}") }?:throw IllegalArgumentException("图片存储目录初始化失败")
launch {
while (true){
val resHtml= htmlChannel.receive()
launch {
log.info("开始解析:${resHtml.url}")
val client = HttpClient(CIO)
val titlePrefix="title>"
val title=Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml.html)?.value?.replace(titlePrefix,"")?:throw IllegalArgumentException("无法解析漫画标题")
val romajiPrefix="/manga"
val romajiTitle=Regex("$romajiPrefix/\\w+").find(resHtml.url)?.value?.replace(romajiPrefix,"")?:throw IllegalArgumentException("无法解析漫画罗马音标题")
log.info("解析漫画标题")
val imageDir= File(filePath,romajiTitle).apply { if(!exists()) mkdir() }
log.info("漫画图片存储到:${imageDir.absolutePath}")
Regex("data/.*.json").findAll(resHtml.html)
.apply {
withIndex()
//TODO .forEach {
.first().let {
log.info("开始解析:${it.value.value}")
val urlPath = "${resHtml.url}/${it.value.value}"
val jsonRes: HttpResponse = client.get(urlPath)
if (jsonRes.status == HttpStatusCode.OK) {
log.info("url:${urlPath} request OK")
val t:t=Json.decodeFromString(jsonRes.readText())
val originImagePath="/data/${t.resources.i.src}"
val imageUrl="${resHtml.url}${originImagePath}"
val imgRes:HttpResponse=client.get(imageUrl)
if(imgRes.status==HttpStatusCode.OK&&imgRes.contentType()==ContentType.Image.JPEG){
val filename="${it.index}.jpg"
val file=File(imageDir,filename).apply {
writeBytes(imgRes.readBytes())
}
log.info("存储漫画图片:${file.absolutePath}")
val serverImagePath="/${uploadDir}/${romajiTitle}/${filename}"
log.info("serverImagePath:${serverImagePath}")
urlResultChannel.send(UrlResult(originImagePath = originImagePath, t = t,
serverImagePath = serverImagePath))
}else{
log.warn("image url:${imageUrl} 响应码${jsonRes.status} 响应类型${imgRes.contentType()}")
}
} else {
log.warn("json url:${urlPath} 响应码:${jsonRes.status}")
}
taskChannel.send(ParseTask(total = count(), finish = it.index + 1,percentage = if(count()==it.index+1) 100F else String.format("%.2f",(it.index+1)*100F/count()).toFloat()))
}
}
client.close()
}.apply {
invokeOnCompletion {
log.info("${resHtml.url}解析完成")
}
}
}
}
}
fun Application.configureRouting() {
routing {
val uploadDir=environment.config.property("ktor.deployment.filePath").getString()
static(uploadDir) {
resources(uploadDir)
}
static("/static") {
resources()
}
webSocket(websocketPath) { // websocketSession
launch {
while (true) {
val urlResult = urlResultChannel.receive()
outgoing.send(Frame.Text(Json.encodeToString(urlResult)))
}
}
launch {
while (true){
val task=taskChannel.receive()
outgoing.send(Frame.Text(Json.encodeToString(task)))
}
}
for (frame in incoming) {
when (frame) {
is Frame.Text -> {
val text = frame.readText()
outgoing.send(Frame.Text("YOU SAID: $text"))
if (text.equals("bye", ignoreCase = true)) {
close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE"))
}
}
}
}
}
get("/") {
call.respondHtml(HttpStatusCode.OK, HTML::index)
}
post("/api/json") {
val formParameters = call.receiveParameters()
val urlParam = formParameters["url"] ?: ""
log.info("urlParam:${urlParam}")
if (urlParam.startsWith(website)) {
val client = HttpClient(CIO)
val response: HttpResponse = client.get(urlParam)
if (response.status == HttpStatusCode.OK) {
val resHtml = response.readText()
htmlChannel.send(UrlParam(url = urlParam,html = resHtml))
call.respond(MessageResponse(message = "开始执行解析任务"))
} else {
log.warn("http code:${response.status}")
call.respond(MessageResponse(message = "请求失败"))
}
client.close()
} else {
call.respond(MessageResponse(message = "${urlParam}:非法漫画地址"))
}
}
}
}

@ -0,0 +1,11 @@
package cool.kirito.bili.live.server.plugins
import io.ktor.features.*
import io.ktor.application.*
import io.ktor.serialization.*
fun Application.configureSerialization() {
install(ContentNegotiation) {
json()
}
}

@ -0,0 +1,8 @@
package plugins
import io.ktor.application.*
import io.ktor.websocket.*
fun Application.configureWebSockets() {
install(WebSockets)
}

@ -0,0 +1,47 @@
import cool.kirito.bili.live.server.plugins.configureHTTP
import cool.kirito.bili.live.server.plugins.configureMonitoring
import cool.kirito.bili.live.server.plugins.configureSerialization
import io.ktor.application.*
import io.ktor.client.*
import io.ktor.client.engine.cio.*
import io.ktor.client.features.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.features.*
import io.ktor.html.respondHtml
import io.ktor.http.*
import io.ktor.routing.get
import io.ktor.routing.routing
import io.ktor.server.engine.embeddedServer
import io.ktor.server.netty.Netty
import io.ktor.http.content.resources
import io.ktor.http.content.static
import io.ktor.response.*
import io.ktor.serialization.*
import kotlinx.html.*
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import plugins.configureWebSockets
fun HTML.index() {
head {
title(websiteTitle)
}
body {
div {
id = "root"
}
script(src = "/static/js.js") {}
}
}
fun main(args: Array<String>): Unit = io.ktor.server.netty.EngineMain.main(args)
fun Application.module(testing: Boolean = false) {
configureWebSockets()
configureRouting()
configureHTTP()
configureMonitoring()
configureSerialization()
parse()
}

@ -0,0 +1,22 @@
ktor {
#开发模式
development = true
deployment {
port = 8080
port = ${?PORT}
mysql = {
username = root
password = 123456
jdbcUrl = "jdbc:mysql://localhost:3306/csams?serverTimezone=Asia/Shanghai"
driverClassName = "com.mysql.cj.jdbc.Driver"
}
filePath = static/image
#免重启自动重载classes目录
watch = [ classes ]
}
application {
modules = [
ServerKt.module,
]
}
}

@ -0,0 +1,14 @@
<configuration>
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{YYYY-MM-dd HH:mm:ss} [%thread] %-5level - %C:%L - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<root level="trace">
<appender-ref ref="STDOUT"/>
</root>
<logger name="org.eclipse.jetty" level="INFO"/>
<logger name="io.netty" level="INFO"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
</configuration>

@ -0,0 +1,78 @@
import cool.kirito.bili.live.server.plugins.configureSerialization
import io.ktor.http.*
import io.ktor.config.*
import kotlin.test.*
import io.ktor.server.testing.*
import kotlinx.coroutines.channels.Channel
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import plugins.configureWebSockets
class ApplicationTest {
@Test
fun testRoot() {
withTestApplication({
configureWebSockets()
configureRouting()
configureSerialization()
}) {
handleRequest(HttpMethod.Post, "/").apply {
// assertEquals(HttpStatusCode.OK, response.status())
environment.log.info("返回!!!!!!!!!!!!!!!:${response.content}")
}
}
}
@Test
fun testParse(){
withTestApplication({
configureWebSockets()
configureRouting()
configureSerialization()
}) {
handleRequest(HttpMethod.Post,"/api/json"){
addHeader(HttpHeaders.ContentType, ContentType.Application.FormUrlEncoded.toString())
setBody("url=https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")
}
.apply {
assertEquals(HttpStatusCode.OK,response.status())
environment.log.info("json:${response.content}")
}
}
}
@Test
fun testRegex(){
val s="<div id=\"content\" class=\"pages ptbinb-container\" data-binbsp-direction=\"rtl\" data-binbsp-toc=\"toc\" data-binbsp-recommend=\"../recommend_01/index.html#more[next] ../recommend2/[next]\">\n" +
"\t\t\t<div data-ptimg=\"data/0001.ptimg.json\" data-binbsp-spread=\"center\" data-binbsp-anchors=\"L_book_000\"></div>\n" +
"\t\t\t<div data-ptimg=\"data/0002.ptimg.json\" data-binbsp-spread=\"right\"></div>\n" +
"\t\t\t<div data-ptimg=\"data/0003.ptimg.json\" data-binbsp-spread=\"left\"></div>\n" +
"\t\t\t<div data-ptimg=\"data/0004.ptimg.json\" data-binbsp-spread=\"right\"></div>\n" +
"\t\t\t<div data-ptimg=\"data/0005.ptimg.json\" data-binbsp-spread=\"left\"></div>"
Regex("data/.*.json").findAll(s).forEach {
println(it.value)
}
}
@Test
fun testNumber(){
println(String.format("%.2f",3*100/22F))
}
@Test
fun testUrl(){
println(Regex("/manga/\\w+")
.find("https://gammaplus.takeshobo.co.jp/manga/kobito_recipe/_files/02_1/")?.value)
}
@Test
fun testSlice(){
val t="sdfsdfsdfsd=="
println(t.slice(IntRange(t.length-2,t.length-1)))
}
}
Loading…
Cancel
Save