From 93bf0c9730be97ff491a2cb60605878b3d4ee736 Mon Sep 17 00:00:00 2001 From: pan <1029559041@qq.com> Date: Sun, 27 Jun 2021 02:43:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8B=BC=E6=8E=A5=E6=BC=AB=E7=94=BB=E5=9B=BE?= =?UTF-8?q?=E7=89=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/commonMain/kotlin/Data.kt | 2 +- src/jsMain/kotlin/image.kt | 92 ++++++++++++++------ src/jsMain/kotlin/welcome.kt | 39 +++++---- src/jvmMain/kotlin/plugins/Routing.kt | 120 +++++++++++++++----------- 4 files changed, 159 insertions(+), 94 deletions(-) diff --git a/src/commonMain/kotlin/Data.kt b/src/commonMain/kotlin/Data.kt index b1999d4..07475ee 100644 --- a/src/commonMain/kotlin/Data.kt +++ b/src/commonMain/kotlin/Data.kt @@ -34,4 +34,4 @@ data class UrlParam(val url:String,val html:String) const val websocketPath="/webSocket" -const val websiteTitle="朴实无华的漫画解析工具" \ No newline at end of file +const val websiteTitle="朴实无华的takeshobo漫画解析工具" \ No newline at end of file diff --git a/src/jsMain/kotlin/image.kt b/src/jsMain/kotlin/image.kt index 2f44867..d6604d5 100644 --- a/src/jsMain/kotlin/image.kt +++ b/src/jsMain/kotlin/image.kt @@ -1,6 +1,8 @@ import kotlinext.js.getOwnPropertyNames 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.set import org.w3c.dom.CanvasRenderingContext2D @@ -15,7 +17,7 @@ import kotlin.math.ceil import kotlin.math.max import kotlin.math.min import kotlin.math.round - +import org.w3c.dom.WindowOrWorkerGlobalScope data class un(val width: Int, val height: Int) @@ -130,6 +132,8 @@ class ImageLoader(val urlResult: UrlResult){ private var imageHeight=0.0 + private val tasks= mutableListOf>() + //speedbinb.js?dmy=016301:formatted:8766 private fun callback(t:un): List { d.urlResult=urlResult @@ -198,39 +202,76 @@ class ImageLoader(val urlResult: UrlResult){ } } - 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}") - }) + + Promise(executor = { resolve, _ -> + canvasToBlob(t=canvas,{blob: Blob -> + console.info("blob.size:${blob.size}") + val i=URL.createObjectURL(blob=blob) + console.info("加载图片dataUrl:\n${i}") + Image().apply { + onload={event: Event -> + imageHeight+=this.naturalHeight + resolve(this) } + src=i } - src=i - } - }) + }) + }).apply { + tasks.add(this) + } } - fun create(){ - Image().apply { - onload={event: Event -> - console.info("拼接图片大小naturalWidth:${naturalWidth},naturalHeight:${naturalHeight}") - } - src=canvasHtml.toDataURL() + fun dataURItoBlob(dataURI:String): Blob { + // convert base64 to raw binary data held in a string + val byteString = window.atob(dataURI.split(',')[1]) + + // separate out the mime component + 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 - fun canvasToBlob( + private 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}") @@ -282,6 +323,7 @@ class ImageLoader(val urlResult: UrlResult){ // val n=t(width = t.width,height = t.height) us(t=t,image=it) } + create() } } diff --git a/src/jsMain/kotlin/welcome.kt b/src/jsMain/kotlin/welcome.kt index e21dfea..b599a0a 100644 --- a/src/jsMain/kotlin/welcome.kt +++ b/src/jsMain/kotlin/welcome.kt @@ -26,7 +26,7 @@ external interface WelcomeProps : RProps { 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 Float.format(digits: Int): String = this.asDynamic().toFixed(digits) @@ -43,13 +43,11 @@ class Welcome(props: WelcomeProps) : RComponent(prop if(data.contains("ptimg-version")){ val urlResult=Json.decodeFromString(data) console.info("ptimg_version:${urlResult.t.ptimg_version}") - ImageLoader(urlResult = urlResult).apply { - rebuild() - create() - } + ImageLoader(urlResult = urlResult).rebuild() }else{ val task=Json.decodeFromString(data) state.result="解析进度:${task.percentage}%" + state.allowInput=(state.percentage==100F) setState(state) } } @@ -72,9 +70,9 @@ class Welcome(props: WelcomeProps) : RComponent(prop href="https://gammaplus.takeshobo.co.jp" target="_blank" } - +"漫画" + +"takeshobo" } - +"解析工具" + +"漫画解析工具" } styledDiv{ @@ -93,6 +91,7 @@ class Welcome(props: WelcomeProps) : RComponent(prop attrs{ type=InputType.text value = state.url + disabled = !state.allowInput onChangeFunction = { event -> (event.target as HTMLInputElement).let { console.info(it.value) @@ -117,36 +116,42 @@ class Welcome(props: WelcomeProps) : RComponent(prop button { attrs { + disabled=!state.allowInput onClickFunction={ - + state.allowInput=false + state.result="初始化解析任务请稍等" + setState(state) val formData=FormData() 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() }.then { console.info(it) + state.result=Json.decodeFromString(it).message + setState(state) } } } +"开始解析" } - if(state.result!=""){ - styledDiv { - +state.result - } + div { + +state.result + } + + if(state.percentage>0F&&state.percentage<100F){ button { attrs { onClickFunction={ - state.result="" + props.webSocket.send("cancel") + state=WelcomeState() setState(state) } } - +"清空消息" + +"取消解析任务" } } - - } } diff --git a/src/jvmMain/kotlin/plugins/Routing.kt b/src/jvmMain/kotlin/plugins/Routing.kt index 2f234a6..df6b49d 100644 --- a/src/jvmMain/kotlin/plugins/Routing.kt +++ b/src/jvmMain/kotlin/plugins/Routing.kt @@ -11,6 +11,8 @@ import io.ktor.request.* import io.ktor.response.* import io.ktor.routing.* import io.ktor.websocket.* +import kotlinx.coroutines.Job +import kotlinx.coroutines.cancel import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.launch import kotlinx.html.HTML @@ -25,6 +27,7 @@ const val website = "https://gammaplus.takeshobo.co.jp" var taskChannel = Channel() val urlResultChannel = Channel() val htmlChannel= Channel() +var currentJob: Job?=null fun Application.parse(){ log.info("初始化解析任务") @@ -35,57 +38,64 @@ fun Application.parse(){ 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() + currentJob?.let { + if(it.isActive) this.cancel() + } + + 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()) + .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}") } - 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()}") + 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())) } - } 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() { routing { - val uploadDir=environment.config.property("ktor.deployment.filePath").getString() + val uploadDir = environment.config.property("ktor.deployment.filePath").getString() static(uploadDir) { resources(uploadDir) } @@ -111,8 +121,8 @@ fun Application.configureRouting() { } } launch { - while (true){ - val task=taskChannel.receive() + while (true) { + val task = taskChannel.receive() outgoing.send(Frame.Text(Json.encodeToString(task))) } } @@ -120,11 +130,19 @@ fun Application.configureRouting() { 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")) + when { + "cancel" == text -> { + 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) if (response.status == HttpStatusCode.OK) { val resHtml = response.readText() - htmlChannel.send(UrlParam(url = urlParam,html = resHtml)) + htmlChannel.send(UrlParam(url = urlParam, html = resHtml)) call.respond(MessageResponse(message = "开始执行解析任务")) } else { log.warn("http code:${response.status}")