拼接漫画图片

master
pan 3 years ago
parent cbc9a26b7d
commit 93bf0c9730
  1. 2
      src/commonMain/kotlin/Data.kt
  2. 92
      src/jsMain/kotlin/image.kt
  3. 39
      src/jsMain/kotlin/welcome.kt
  4. 120
      src/jvmMain/kotlin/plugins/Routing.kt

@ -34,4 +34,4 @@ data class UrlParam(val url:String,val html:String)
const val websocketPath="/webSocket" const val websocketPath="/webSocket"
const val websiteTitle="朴实无华的漫画解析工具" const val websiteTitle="朴实无华的takeshobo漫画解析工具"

@ -1,6 +1,8 @@
import kotlinext.js.getOwnPropertyNames import kotlinext.js.getOwnPropertyNames
import kotlinx.browser.document import kotlinx.browser.document
import kotlinx.css.head import kotlinx.browser.window
import org.khronos.webgl.ArrayBuffer
import org.khronos.webgl.DataView
import org.khronos.webgl.Uint8Array import org.khronos.webgl.Uint8Array
import org.khronos.webgl.set import org.khronos.webgl.set
import org.w3c.dom.CanvasRenderingContext2D import org.w3c.dom.CanvasRenderingContext2D
@ -15,7 +17,7 @@ import kotlin.math.ceil
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.math.round import kotlin.math.round
import org.w3c.dom.WindowOrWorkerGlobalScope
data class un(val width: Int, val height: Int) data class un(val width: Int, val height: Int)
@ -130,6 +132,8 @@ class ImageLoader(val urlResult: UrlResult){
private var imageHeight=0.0 private var imageHeight=0.0
private val tasks= mutableListOf<Promise<Image>>()
//speedbinb.js?dmy=016301:formatted:8766 //speedbinb.js?dmy=016301:formatted:8766
private fun callback(t:un): List<n> { private fun callback(t:un): List<n> {
d.urlResult=urlResult d.urlResult=urlResult
@ -198,39 +202,76 @@ class ImageLoader(val urlResult: UrlResult){
} }
} }
canvasToBlob(t=canvas,{blob: Blob ->
console.info("blob.size:${blob.size}") Promise<Image>(executor = { resolve, _ ->
val i=URL.createObjectURL(blob=blob) canvasToBlob(t=canvas,{blob: Blob ->
console.info("漫画URL:${i}") console.info("blob.size:${blob.size}")
Image().apply { val i=URL.createObjectURL(blob=blob)
onload={event: Event -> console.info("加载图片dataUrl:\n${i}")
canvasHtml.let { Image().apply {
canvas-> onload={event: Event ->
val ctx:CanvasRenderingContext2D= canvas.getContext("2d") as CanvasRenderingContext2D imageHeight+=this.naturalHeight
ctx.drawImage(this,0.0,imageHeight,naturalWidth.toDouble(),naturalHeight.toDouble().apply { resolve(this)
imageHeight+=this
console.info("拼接图片imageHeight:${imageHeight}")
})
} }
src=i
} }
src=i })
} }).apply {
}) tasks.add(this)
}
} }
fun create(){ fun dataURItoBlob(dataURI:String): Blob {
Image().apply { // convert base64 to raw binary data held in a string
onload={event: Event -> val byteString = window.atob(dataURI.split(',')[1])
console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}")
} // separate out the mime component
src=canvasHtml.toDataURL() val mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]
// write the bytes of the string to an ArrayBuffer
val arrayBuffer = ArrayBuffer(byteString.length)
val _ia = Uint8Array(arrayBuffer)
byteString.withIndex().forEach {
_ia[it.index] = it.value.code.toByte()
} }
val dataView = DataView(arrayBuffer);
val blob = Blob(arrayOf(dataView),options = BlobPropertyBag(type = mimeString));
return blob
} }
private fun create(){
Promise.all(promise = tasks.toTypedArray()).then {
canvasHtml.let {
canvas->
canvas.width=it.first().naturalWidth
canvas.height=imageHeight.toInt()
val ctx:CanvasRenderingContext2D= canvas.getContext("2d") as CanvasRenderingContext2D
ctx.clearRect(0.0, 0.0, canvas.width.toDouble(), canvas.height.toDouble())
var dy=0.0
it.forEach {
ctx.drawImage(image=it,dx=0.0,dy=dy,
dw=it.naturalWidth.toDouble(),dh=it.naturalHeight.toDouble().apply {
dy+=this.toInt()-1
})
}
Image().apply {
onload={
console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}")
}
src=canvas.toDataURL().let { URL.createObjectURL(dataURItoBlob(it)) }.apply {
console.info("拼接图片url:\n$this")
}
}
}
}
}
//speedbinb.js?dmy=016301:formatted:3798 //speedbinb.js?dmy=016301:formatted:3798
fun canvasToBlob( private fun canvasToBlob(
t:HTMLCanvasElement, callback:(t:Blob)->Unit, n:String="image/jpeg", r: Double =.9){ t:HTMLCanvasElement, callback:(t:Blob)->Unit, n:String="image/jpeg", r: Double =.9){
val i=t.toDataURL(type=n,quality = r).split(",")[1] val i=t.toDataURL(type=n,quality = r).split(",")[1]
console.info("url length:${i.length}") console.info("url length:${i.length}")
@ -282,6 +323,7 @@ class ImageLoader(val urlResult: UrlResult){
// val n=t(width = t.width,height = t.height) // val n=t(width = t.width,height = t.height)
us(t=t,image=it) us(t=t,image=it)
} }
create()
} }
} }

@ -26,7 +26,7 @@ external interface WelcomeProps : RProps {
var webSocket:WebSocket var webSocket:WebSocket
} }
data class WelcomeState(var url:String="",var result:String="",var percentage:kotlin.Float=0F) : RState data class WelcomeState(var url:String="",var result:String="",var percentage:kotlin.Float=0F,var allowInput:Boolean=true) : RState
fun Double.format(digits: Int): String = this.asDynamic().toFixed(digits) fun Double.format(digits: Int): String = this.asDynamic().toFixed(digits)
fun Float.format(digits: Int): String = this.asDynamic().toFixed(digits) fun Float.format(digits: Int): String = this.asDynamic().toFixed(digits)
@ -43,13 +43,11 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
if(data.contains("ptimg-version")){ if(data.contains("ptimg-version")){
val urlResult=Json.decodeFromString<UrlResult>(data) val urlResult=Json.decodeFromString<UrlResult>(data)
console.info("ptimg_version:${urlResult.t.ptimg_version}") console.info("ptimg_version:${urlResult.t.ptimg_version}")
ImageLoader(urlResult = urlResult).apply { ImageLoader(urlResult = urlResult).rebuild()
rebuild()
create()
}
}else{ }else{
val task=Json.decodeFromString<ParseTask>(data) val task=Json.decodeFromString<ParseTask>(data)
state.result="解析进度:${task.percentage}%" state.result="解析进度:${task.percentage}%"
state.allowInput=(state.percentage==100F)
setState(state) setState(state)
} }
} }
@ -72,9 +70,9 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
href="https://gammaplus.takeshobo.co.jp" href="https://gammaplus.takeshobo.co.jp"
target="_blank" target="_blank"
} }
+"漫画" +"takeshobo"
} }
+"解析工具" +"漫画解析工具"
} }
styledDiv{ styledDiv{
@ -93,6 +91,7 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
attrs{ attrs{
type=InputType.text type=InputType.text
value = state.url value = state.url
disabled = !state.allowInput
onChangeFunction = { event -> onChangeFunction = { event ->
(event.target as HTMLInputElement).let { (event.target as HTMLInputElement).let {
console.info(it.value) console.info(it.value)
@ -117,36 +116,42 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
button { button {
attrs { attrs {
disabled=!state.allowInput
onClickFunction={ onClickFunction={
state.allowInput=false
state.result="初始化解析任务请稍等"
setState(state)
val formData=FormData() val formData=FormData()
formData.append("url",state.url) formData.append("url",state.url)
window.fetch("/api/json", RequestInit(method = "post",body = formData)).then { window.fetch("/api/json", RequestInit(method = "post",body = formData))
.then {
it.text() it.text()
}.then { }.then {
console.info(it) console.info(it)
state.result=Json.decodeFromString<MessageResponse>(it).message
setState(state)
} }
} }
} }
+"开始解析" +"开始解析"
} }
if(state.result!=""){ div {
styledDiv { +state.result
+state.result }
}
if(state.percentage>0F&&state.percentage<100F){
button { button {
attrs { attrs {
onClickFunction={ onClickFunction={
state.result="" props.webSocket.send("cancel")
state=WelcomeState()
setState(state) setState(state)
} }
} }
+"清空消息" +"取消解析任务"
} }
} }
} }
} }

@ -11,6 +11,8 @@ import io.ktor.request.*
import io.ktor.response.* import io.ktor.response.*
import io.ktor.routing.* import io.ktor.routing.*
import io.ktor.websocket.* import io.ktor.websocket.*
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.html.HTML import kotlinx.html.HTML
@ -25,6 +27,7 @@ const val website = "https://gammaplus.takeshobo.co.jp"
var taskChannel = Channel<ParseTask>() var taskChannel = Channel<ParseTask>()
val urlResultChannel = Channel<UrlResult>() val urlResultChannel = Channel<UrlResult>()
val htmlChannel= Channel<UrlParam>() val htmlChannel= Channel<UrlParam>()
var currentJob: Job?=null
fun Application.parse(){ fun Application.parse(){
log.info("初始化解析任务") log.info("初始化解析任务")
@ -35,57 +38,64 @@ fun Application.parse(){
launch { launch {
while (true){ while (true){
val resHtml= htmlChannel.receive() val resHtml= htmlChannel.receive()
launch {
log.info("开始解析:${resHtml.url}")
val client = HttpClient(CIO)
val titlePrefix="title>" currentJob?.let {
val title=Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml.html)?.value?.replace(titlePrefix,"")?:throw IllegalArgumentException("无法解析漫画标题") if(it.isActive) this.cancel()
val romajiPrefix="/manga" }
val romajiTitle=Regex("$romajiPrefix/\\w+").find(resHtml.url)?.value?.replace(romajiPrefix,"")?:throw IllegalArgumentException("无法解析漫画罗马音标题")
log.info("解析漫画标题") launch {
val imageDir= File(filePath,romajiTitle).apply { if(!exists()) mkdir() } log.info("开始解析:${resHtml.url}")
log.info("漫画图片存储到:${imageDir.absolutePath}") val client = HttpClient(CIO)
Regex("data/.*.json").findAll(resHtml.html) val titlePrefix="title>"
.apply { val title=Regex("$titlePrefix[\\u4e00-\\u9fa5\\u0800-\\u4e00\\s\\d\\uff00-\\uffef]+").find(resHtml.html)?.value?.replace(titlePrefix,"")?:throw IllegalArgumentException("无法解析漫画标题")
withIndex() 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 { //TODO .forEach {
.first().let { .first().let {
log.info("开始解析:${it.value.value}") log.info("开始解析:${it.value.value}")
val urlPath = "${resHtml.url}/${it.value.value}" val urlPath = "${resHtml.url}/${it.value.value}"
val jsonRes: HttpResponse = client.get(urlPath) val jsonRes: HttpResponse = client.get(urlPath)
if (jsonRes.status == HttpStatusCode.OK) { if (jsonRes.status == HttpStatusCode.OK) {
log.info("url:${urlPath} request OK") log.info("url:${urlPath} request OK")
val t:t=Json.decodeFromString(jsonRes.readText()) val t:t=Json.decodeFromString(jsonRes.readText())
val originImagePath="/data/${t.resources.i.src}" val originImagePath="/data/${t.resources.i.src}"
val imageUrl="${resHtml.url}${originImagePath}" val imageUrl="${resHtml.url}${originImagePath}"
val imgRes:HttpResponse=client.get(imageUrl) val imgRes:HttpResponse=client.get(imageUrl)
if(imgRes.status==HttpStatusCode.OK&&imgRes.contentType()==ContentType.Image.JPEG){ if(imgRes.status==HttpStatusCode.OK&&imgRes.contentType()==ContentType.Image.JPEG){
val filename="${it.index}.jpg" val filename="${it.index}.jpg"
val file=File(imageDir,filename).apply { val file=File(imageDir,filename).apply {
writeBytes(imgRes.readBytes()) 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}")
} }
log.info("存储漫画图片:${file.absolutePath}") 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()))
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 {
currentJob=this
invokeOnCompletion {
log.info("${resHtml.url}解析完成")
} }
client.close()
}.apply {
invokeOnCompletion {
log.info("${resHtml.url}解析完成")
} }
}
} }
} }
@ -94,7 +104,7 @@ fun Application.parse(){
fun Application.configureRouting() { fun Application.configureRouting() {
routing { routing {
val uploadDir=environment.config.property("ktor.deployment.filePath").getString() val uploadDir = environment.config.property("ktor.deployment.filePath").getString()
static(uploadDir) { static(uploadDir) {
resources(uploadDir) resources(uploadDir)
} }
@ -111,8 +121,8 @@ fun Application.configureRouting() {
} }
} }
launch { launch {
while (true){ while (true) {
val task=taskChannel.receive() val task = taskChannel.receive()
outgoing.send(Frame.Text(Json.encodeToString(task))) outgoing.send(Frame.Text(Json.encodeToString(task)))
} }
} }
@ -120,11 +130,19 @@ fun Application.configureRouting() {
when (frame) { when (frame) {
is Frame.Text -> { is Frame.Text -> {
val text = frame.readText() val text = frame.readText()
outgoing.send(Frame.Text("YOU SAID: $text")) when {
if (text.equals("bye", ignoreCase = true)) { "cancel" == text -> {
close(CloseReason(CloseReason.Codes.NORMAL, "Client said BYE")) if (currentJob?.isActive == true) {
currentJob?.cancel()
outgoing.send(Frame.Text("当前服务器解析任务已停止"))
} else {
outgoing.send(Frame.Text("当前服务器没有正在运行的解析任务"))
}
}
"exit" == text -> close(CloseReason(CloseReason.Codes.NORMAL, "Client said exit"))
} }
} }
else -> log.warn("无法处理${frame.frameType}类型消息")
} }
} }
} }
@ -142,7 +160,7 @@ fun Application.configureRouting() {
val response: HttpResponse = client.get(urlParam) val response: HttpResponse = client.get(urlParam)
if (response.status == HttpStatusCode.OK) { if (response.status == HttpStatusCode.OK) {
val resHtml = response.readText() val resHtml = response.readText()
htmlChannel.send(UrlParam(url = urlParam,html = resHtml)) htmlChannel.send(UrlParam(url = urlParam, html = resHtml))
call.respond(MessageResponse(message = "开始执行解析任务")) call.respond(MessageResponse(message = "开始执行解析任务"))
} else { } else {
log.warn("http code:${response.status}") log.warn("http code:${response.status}")

Loading…
Cancel
Save