From 356849909c041252ae96c8d25c1c5110d325ec31 Mon Sep 17 00:00:00 2001
From: altaccosc <97206249+altaccosc@users.noreply.github.com>
Date: Fri, 2 Sep 2022 20:47:49 +0200
Subject: [PATCH] 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>
---
.../tachiyomi/multisrc/webtoons/Webtoons.kt | 144 +++++++++++++----
.../multisrc/webtoons/WebtoonsGenerator.kt | 2 +-
src/en/tapastic/build.gradle | 2 +-
.../extension/en/tapastic/Tapastic.kt | 146 ++++++++++++++----
4 files changed, 229 insertions(+), 65 deletions(-)
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/Webtoons.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/Webtoons.kt
index 7ef333549..85c5883b9 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/Webtoons.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/Webtoons.kt
@@ -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*
\\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 {
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*
\\s*".toRegex(), "\n")
+ }
+
+ private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
+ canvas.save()
+ canvas.translate(x, y)
+ this.draw(canvas)
+ canvas.restore()
+ }
+ }
}
diff --git a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/WebtoonsGenerator.kt b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/WebtoonsGenerator.kt
index a18014028..af77686b1 100644
--- a/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/WebtoonsGenerator.kt
+++ b/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/webtoons/WebtoonsGenerator.kt
@@ -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")
)
diff --git a/src/en/tapastic/build.gradle b/src/en/tapastic/build.gradle
index 5f57dac21..0212465b1 100644
--- a/src/en/tapastic/build.gradle
+++ b/src/en/tapastic/build.gradle
@@ -6,7 +6,7 @@ ext {
extName = 'Tapas'
pkgNameSuffix = 'en.tapastic'
extClass = '.Tapastic'
- extVersionCode = 16
+ extVersionCode = 17
}
apply from: "$rootDir/common.gradle"
diff --git a/src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt b/src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt
index 265d07dbe..08f7e964b 100644
--- a/src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt
+++ b/src/en/tapastic/src/eu/kanade/tachiyomi/extension/en/tapastic/Tapastic.kt
@@ -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*
\\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 {
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*
\\s*".toRegex(), "\n")
+ }
+
+ private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
+ canvas.save()
+ canvas.translate(x, y)
+ this.draw(canvas)
+ canvas.restore()
+ }
+ }
}