Fix Webtoons & Tapas Author's Notes (#13304)
* Bump Webtoons extension version * Fix author's note API * Bump Tapas extension version * Fix author's note API * Tapas: Render author's note image locally Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com> * Tapas: Add new dependency to gradle * Tapas: revert dependency addition 94473a2 * Tapas: StaticLayout.draw without dependency * Webtoons: Render author's note image locally Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com> Co-authored-by: stevenyomi <95685115+stevenyomi@users.noreply.github.com>
This commit is contained in:
parent
8f0a4c8903
commit
356849909c
@ -2,7 +2,14 @@ package eu.kanade.tachiyomi.multisrc.webtoons
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
import eu.kanade.tachiyomi.network.GET
|
||||
@ -25,20 +32,24 @@ import okhttp3.CookieJar
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import rx.Observable
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.ParseException
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Calendar
|
||||
import java.util.Locale
|
||||
import kotlin.math.ceil
|
||||
|
||||
open class Webtoons(
|
||||
override val name: String,
|
||||
@ -71,6 +82,7 @@ open class Webtoons(
|
||||
}
|
||||
}
|
||||
)
|
||||
.addInterceptor(TextInterceptor)
|
||||
.build()
|
||||
|
||||
private val day: String
|
||||
@ -291,32 +303,6 @@ open class Webtoons(
|
||||
|
||||
override fun chapterListRequest(manga: SManga) = GET("https://m.webtoons.com" + manga.url, mobileHeaders)
|
||||
|
||||
private fun wordwrap(t: String, lineWidth: Int) = buildString {
|
||||
// TODO: Split off into library file or something, because Tapastic is using the exact same wordwrap and toImage functions
|
||||
// src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt
|
||||
val text = t.replace("\n", "\n ")
|
||||
var charCount = 0
|
||||
text.split(" ").forEach { w ->
|
||||
if (w.contains("\n")) {
|
||||
charCount = 0
|
||||
}
|
||||
if (charCount > lineWidth) {
|
||||
append("\n")
|
||||
charCount = 0
|
||||
}
|
||||
append("$w ")
|
||||
charCount += w.length + 1
|
||||
}
|
||||
}
|
||||
|
||||
private fun toImage(t: String, fontSize: Int, bold: Boolean = false): String {
|
||||
val text = wordwrap(t.replace("&", "&").replace("\\s*<br>\\s*".toRegex(), "\n"), 65)
|
||||
val imgHeight = (text.lines().size + 2) * fontSize * 1.3
|
||||
return "https://placehold.jp/" + fontSize + "/ffffff/000000/1500x" + ceil(imgHeight).toInt() + ".png?" +
|
||||
"css=%7B%22text-align%22%3A%22%20left%22%2C%22padding-left%22%3A%22%203%25%22" + (if (bold) "%2C%22font-weight%22%3A%22%20600%22" else "") + "%7D&" +
|
||||
"text=" + Uri.encode(text)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
|
||||
|
||||
@ -324,13 +310,13 @@ open class Webtoons(
|
||||
val note = document.select("div.comment_area div.info_area p").text()
|
||||
|
||||
if (note.isNotEmpty()) {
|
||||
val noteImage = toImage(note, 42)
|
||||
|
||||
val creator = document.select("div.creator_note span.author a").text().trim()
|
||||
val creatorImage = toImage("Author's Notes from $creator", 43, true)
|
||||
|
||||
pages = pages + Page(pages.size, "", creatorImage)
|
||||
pages = pages + Page(pages.size, "", noteImage)
|
||||
pages = pages + Page(
|
||||
pages.size, "",
|
||||
"http://note/" + Uri.encode(creator) + "/" + Uri.encode(note)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -359,4 +345,100 @@ open class Webtoons(
|
||||
const val URL_SEARCH_PREFIX = "url:"
|
||||
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
||||
}
|
||||
|
||||
// TODO: Split off into library file or something, because Webtoons is using the exact same TextInterceptor
|
||||
// src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt
|
||||
object TextInterceptor : Interceptor {
|
||||
// With help from:
|
||||
// https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
|
||||
// https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
|
||||
|
||||
// Designer values:
|
||||
private const val WIDTH: Int = 1000
|
||||
private const val X_PADDING: Float = 50f
|
||||
private const val Y_PADDING: Float = 25f
|
||||
private const val HEADING_FONT_SIZE: Float = 36f
|
||||
private const val BODY_FONT_SIZE: Float = 30f
|
||||
private const val SPACING_MULT: Float = 1.1f
|
||||
private const val SPACING_ADD: Float = 2f
|
||||
|
||||
// No need to touch this one:
|
||||
private const val HOST = "note"
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val url = request.url
|
||||
if (url.host != HOST) return chain.proceed(request)
|
||||
|
||||
val creator = textFixer("Author's Notes from ${url.pathSegments[0]}")
|
||||
val story = textFixer(url.pathSegments[1])
|
||||
|
||||
// Heading
|
||||
val paintHeading = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = HEADING_FONT_SIZE
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val heading: StaticLayout = StaticLayout(
|
||||
creator, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||
)
|
||||
|
||||
// Body
|
||||
val paintBody = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = BODY_FONT_SIZE
|
||||
typeface = Typeface.DEFAULT
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val body: StaticLayout = StaticLayout(
|
||||
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||
)
|
||||
|
||||
// Image building
|
||||
val imgHeight: Int = (heading.height + body.height + 2 * Y_PADDING).toInt()
|
||||
val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas: Canvas = Canvas(bitmap)
|
||||
|
||||
// Image drawing
|
||||
canvas.drawColor(Color.WHITE)
|
||||
heading.draw(canvas, X_PADDING, Y_PADDING)
|
||||
body.draw(canvas, X_PADDING, Y_PADDING + heading.height.toFloat())
|
||||
|
||||
// Image converting & returning
|
||||
val stream: ByteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 0, stream)
|
||||
val responseBody = stream.toByteArray().toResponseBody("image/png".toMediaType())
|
||||
return Response.Builder()
|
||||
.request(request)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(200)
|
||||
.message("OK")
|
||||
.body(responseBody)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun textFixer(t: String): String {
|
||||
return t
|
||||
.replace("&", "&")
|
||||
.replace("'", "'")
|
||||
.replace(""", "\"")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\\s*<br>\\s*".toRegex(), "\n")
|
||||
}
|
||||
|
||||
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
|
||||
canvas.save()
|
||||
canvas.translate(x, y)
|
||||
this.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ class WebtoonsGenerator : ThemeSourceGenerator {
|
||||
override val baseVersionCode: Int = 2
|
||||
|
||||
override val sources = listOf(
|
||||
MultiLang("Webtoons.com", "https://www.webtoons.com", listOf("en", "fr", "es", "id", "th", "zh-Hant", "de"), className = "WebtoonsFactory", pkgName = "webtoons", overrideVersionCode = 33),
|
||||
MultiLang("Webtoons.com", "https://www.webtoons.com", listOf("en", "fr", "es", "id", "th", "zh-Hant", "de"), className = "WebtoonsFactory", pkgName = "webtoons", overrideVersionCode = 34),
|
||||
SingleLang("Dongman Manhua", "https://www.dongmanmanhua.cn", "zh")
|
||||
)
|
||||
|
||||
|
@ -6,7 +6,7 @@ ext {
|
||||
extName = 'Tapas'
|
||||
pkgNameSuffix = 'en.tapastic'
|
||||
extClass = '.Tapastic'
|
||||
extVersionCode = 16
|
||||
extVersionCode = 17
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
@ -2,7 +2,15 @@ package eu.kanade.tachiyomi.extension.en.tapastic
|
||||
|
||||
import android.app.Application
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Bitmap.CompressFormat
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Color
|
||||
import android.graphics.Typeface
|
||||
import android.net.Uri
|
||||
import android.text.Layout
|
||||
import android.text.StaticLayout
|
||||
import android.text.TextPaint
|
||||
import android.webkit.CookieManager
|
||||
import androidx.preference.PreferenceScreen
|
||||
import androidx.preference.SwitchPreferenceCompat
|
||||
@ -25,18 +33,22 @@ import okhttp3.CookieJar
|
||||
import okhttp3.Headers
|
||||
import okhttp3.HttpUrl
|
||||
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import okhttp3.Protocol
|
||||
import okhttp3.Request
|
||||
import okhttp3.Response
|
||||
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||
import org.jsoup.Jsoup
|
||||
import org.jsoup.nodes.Document
|
||||
import org.jsoup.nodes.Element
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Locale
|
||||
import kotlin.math.ceil
|
||||
|
||||
class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
@ -96,6 +108,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||
}
|
||||
}
|
||||
)
|
||||
.addInterceptor(TextInterceptor)
|
||||
.build()
|
||||
|
||||
private val preferences: SharedPreferences by lazy {
|
||||
@ -339,32 +352,6 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||
|
||||
// Pages
|
||||
|
||||
// TODO: Split off into library file or something, because Webtoons is using the exact same wordwrap and toImage functions
|
||||
// multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/Webtoons.kt
|
||||
private fun wordwrap(t: String, lineWidth: Int) = buildString {
|
||||
val text = t.replace("\n", "\n ")
|
||||
var charCount = 0
|
||||
text.split(" ").forEach { w ->
|
||||
if (w.contains("\n")) {
|
||||
charCount = 0
|
||||
}
|
||||
if (charCount > lineWidth) {
|
||||
append("\n")
|
||||
charCount = 0
|
||||
}
|
||||
append("$w ")
|
||||
charCount += w.length + 1
|
||||
}
|
||||
}
|
||||
|
||||
private fun toImage(t: String, fontSize: Int, bold: Boolean = false): String {
|
||||
val text = wordwrap(t.replace("&", "&").replace("\\s*<br>\\s*".toRegex(), "\n"), 65)
|
||||
val imgHeight = (text.lines().size + 2) * fontSize * 1.3
|
||||
return "https://placehold.jp/" + fontSize + "/ffffff/000000/1500x" + ceil(imgHeight).toInt() + ".png?" +
|
||||
"css=%7B%22text-align%22%3A%22%20left%22%2C%22padding-left%22%3A%22%203%25%22" + (if (bold) "%2C%22font-weight%22%3A%22%20600%22" else "") + "%7D&" +
|
||||
"text=" + Uri.encode(text)
|
||||
}
|
||||
|
||||
override fun pageListParse(document: Document): List<Page> {
|
||||
var pages = document.select("img.content__img").mapIndexed { i, img ->
|
||||
Page(i, "", img.let { if (it.hasAttr("data-src")) it.attr("abs:data-src") else it.attr("abs:src") })
|
||||
@ -374,13 +361,12 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||
val episodeStory = document.select("p.js-episode-story").html()
|
||||
|
||||
if (episodeStory.isNotEmpty()) {
|
||||
val storyImage = toImage(episodeStory, 42)
|
||||
|
||||
val creator = document.select("a.name.js-fb-tracking")[0].text()
|
||||
val creatorImage = toImage("Author's Notes from $creator", 43, true)
|
||||
|
||||
pages = pages + Page(pages.size, "", creatorImage)
|
||||
pages = pages + Page(pages.size, "", storyImage)
|
||||
pages = pages + Page(
|
||||
pages.size, "",
|
||||
"http://note/" + Uri.encode(creator) + "/" + Uri.encode(episodeStory)
|
||||
)
|
||||
}
|
||||
}
|
||||
return pages
|
||||
@ -534,4 +520,100 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
|
||||
private const val SHOW_LOCK_PREF_KEY = "showChapterLock"
|
||||
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
|
||||
}
|
||||
|
||||
// TODO: Split off into library file or something, because Webtoons is using the exact same TextInterceptor
|
||||
// multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/Webtoons.kt
|
||||
object TextInterceptor : Interceptor {
|
||||
// With help from:
|
||||
// https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13304#issuecomment-1234532897
|
||||
// https://medium.com/over-engineering/drawing-multiline-text-to-canvas-on-android-9b98f0bfa16a
|
||||
|
||||
// Designer values:
|
||||
private const val WIDTH: Int = 1000
|
||||
private const val X_PADDING: Float = 50f
|
||||
private const val Y_PADDING: Float = 25f
|
||||
private const val HEADING_FONT_SIZE: Float = 36f
|
||||
private const val BODY_FONT_SIZE: Float = 30f
|
||||
private const val SPACING_MULT: Float = 1.1f
|
||||
private const val SPACING_ADD: Float = 2f
|
||||
|
||||
// No need to touch this one:
|
||||
private const val HOST = "note"
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val request = chain.request()
|
||||
val url = request.url
|
||||
if (url.host != HOST) return chain.proceed(request)
|
||||
|
||||
val creator = textFixer("Author's Notes from ${url.pathSegments[0]}")
|
||||
val story = textFixer(url.pathSegments[1])
|
||||
|
||||
// Heading
|
||||
val paintHeading = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = HEADING_FONT_SIZE
|
||||
typeface = Typeface.DEFAULT_BOLD
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val heading: StaticLayout = StaticLayout(
|
||||
creator, paintHeading, (WIDTH - 2 * X_PADDING).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||
)
|
||||
|
||||
// Body
|
||||
val paintBody = TextPaint().apply {
|
||||
color = Color.BLACK
|
||||
textSize = BODY_FONT_SIZE
|
||||
typeface = Typeface.DEFAULT
|
||||
isAntiAlias = true
|
||||
}
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val body: StaticLayout = StaticLayout(
|
||||
story, paintBody, (WIDTH - 2 * X_PADDING).toInt(),
|
||||
Layout.Alignment.ALIGN_NORMAL, SPACING_MULT, SPACING_ADD, true
|
||||
)
|
||||
|
||||
// Image building
|
||||
val imgHeight: Int = (heading.height + body.height + 2 * Y_PADDING).toInt()
|
||||
val bitmap: Bitmap = Bitmap.createBitmap(WIDTH, imgHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas: Canvas = Canvas(bitmap)
|
||||
|
||||
// Image drawing
|
||||
canvas.drawColor(Color.WHITE)
|
||||
heading.draw(canvas, X_PADDING, Y_PADDING)
|
||||
body.draw(canvas, X_PADDING, Y_PADDING + heading.height.toFloat())
|
||||
|
||||
// Image converting & returning
|
||||
val stream: ByteArrayOutputStream = ByteArrayOutputStream()
|
||||
bitmap.compress(CompressFormat.PNG, 0, stream)
|
||||
val responseBody = stream.toByteArray().toResponseBody("image/png".toMediaType())
|
||||
return Response.Builder()
|
||||
.request(request)
|
||||
.protocol(Protocol.HTTP_1_1)
|
||||
.code(200)
|
||||
.message("OK")
|
||||
.body(responseBody)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun textFixer(t: String): String {
|
||||
return t
|
||||
.replace("&", "&")
|
||||
.replace("'", "'")
|
||||
.replace(""", "\"")
|
||||
.replace("<", "<")
|
||||
.replace(">", ">")
|
||||
.replace("\\s*<br>\\s*".toRegex(), "\n")
|
||||
}
|
||||
|
||||
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
|
||||
canvas.save()
|
||||
canvas.translate(x, y)
|
||||
this.draw(canvas)
|
||||
canvas.restore()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user