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:
altaccosc 2022-09-02 20:47:49 +02:00 committed by GitHub
parent 8f0a4c8903
commit 356849909c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 229 additions and 65 deletions

View File

@ -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("&amp;", "&").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("&amp;", "&")
.replace("&#39;", "'")
.replace("&quot;", "\"")
.replace("&lt;", "<")
.replace("&gt;", ">")
.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()
}
}
}

View File

@ -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")
)

View File

@ -6,7 +6,7 @@ ext {
extName = 'Tapas'
pkgNameSuffix = 'en.tapastic'
extClass = '.Tapastic'
extVersionCode = 16
extVersionCode = 17
}
apply from: "$rootDir/common.gradle"

View File

@ -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("&amp;", "&").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("&amp;", "&")
.replace("&#39;", "'")
.replace("&quot;", "\"")
.replace("&lt;", "<")
.replace("&gt;", ">")
.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()
}
}
}