拼接漫画图片

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 websiteTitle="朴实无华的漫画解析工具"
const val websiteTitle="朴实无华的takeshobo漫画解析工具"

@ -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<Promise<Image>>()
//speedbinb.js?dmy=016301:formatted:8766
private fun callback(t:un): List<n> {
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<Image>(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()
}
}

@ -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<WelcomeProps, WelcomeState>(prop
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()
}
ImageLoader(urlResult = urlResult).rebuild()
}else{
val task=Json.decodeFromString<ParseTask>(data)
state.result="解析进度:${task.percentage}%"
state.allowInput=(state.percentage==100F)
setState(state)
}
}
@ -72,9 +70,9 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(prop
href="https://gammaplus.takeshobo.co.jp"
target="_blank"
}
+"漫画"
+"takeshobo"
}
+"解析工具"
+"漫画解析工具"
}
styledDiv{
@ -93,6 +91,7 @@ class Welcome(props: WelcomeProps) : RComponent<WelcomeProps, WelcomeState>(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<WelcomeProps, WelcomeState>(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<MessageResponse>(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)
}
}
+"清空消息"
+"取消解析任务"
}
}
}
}

@ -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<ParseTask>()
val urlResultChannel = Channel<UrlResult>()
val htmlChannel= Channel<UrlParam>()
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}")

Loading…
Cancel
Save