New Lib: Text Interceptor (#13859)

* move TextInterceptor to `lib`

to be used for author notes, image alt texts and what not

* refactor Tapastic to use `lib:extensions-lib`

* Refactor Webtoons to use `:lib-textinterceptor`

this turned out to be more complicated than I thought it'd be
TextInterceptor was used for Author Notes which, looking at previous messages,
was only intended to be added to Webtoons Source and not the entire webtoons
multisrc (i.e. WebtoonsTranslate and DongmanManhua don't seem to be making use
of the Show Author's Notes setting). This was in my favor, since having
to deal with additional.gradle to add dependencies to multisrc files doesn't
seem to work... I'll ask previous contributors just in case

* Fix `json` access

missed this while copying over code from `generated-src` to `multisrc/{overrides,src}`

* remove unused import

* make HOST name more clear

couldve used a better schema but that's something for some other time
also put the HOST in the lib itself so that one doesn't lose track of it in the extensions

* use android provided methods instead of hardcoding

based on https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13859/files#r996276738
that suggested the following SO answer: https://github.com/tachiyomiorg/tachiyomi-extensions/pull/13859/files

* remove unused import

* move url generation to helper function

* fix error

oops sorry for that
happened when I was copy pasting back and forth between two sources and one generated source

Co-authored-by: Navaneeth M Nambiar <nmnambiar@hornbill>
This commit is contained in:
nicki 2023-01-11 11:34:06 -06:00 committed by GitHub
parent 2f4658b3b3
commit 951ca60b56
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 263 additions and 278 deletions

View File

@ -0,0 +1,22 @@
plugins {
id("com.android.library")
kotlin("android")
}
android {
compileSdk = AndroidConfig.compileSdk
defaultConfig {
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
}
}
repositories {
mavenCentral()
}
dependencies {
compileOnly(libs.kotlin.stdlib)
compileOnly(libs.okhttp)
}

View File

@ -0,0 +1,2 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi.lib.textinterceptor" />

View File

@ -0,0 +1,121 @@
package eu.kanade.tachiyomi.lib.textinterceptor
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Typeface
import android.net.Uri
import android.os.Build
import android.text.Html
import android.text.Layout
import android.text.StaticLayout
import android.text.TextPaint
import okhttp3.Interceptor
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.Protocol
import okhttp3.Response
import okhttp3.ResponseBody.Companion.toResponseBody
import java.io.ByteArrayOutputStream
class 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
companion object {
// 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 = TextInterceptorHelper.HOST
}
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()
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(htmlString: String): String {
return if (Build.VERSION.SDK_INT >= 24) {
Html.fromHtml(htmlString , Html.FROM_HTML_MODE_LEGACY).toString()
} else {
Html.fromHtml(htmlString).toString()
}
}
private fun StaticLayout.draw(canvas: Canvas, x: Float, y: Float) {
canvas.save()
canvas.translate(x, y)
this.draw(canvas)
canvas.restore()
}
}
object TextInterceptorHelper {
const val HOST = "tachiyomi-lib-textinterceptor"
fun createUrl(creator: String, text: String): String {
return "http://$HOST/" + Uri.encode(creator) + "/" + Uri.encode(text)
}
}

View File

@ -0,0 +1,3 @@
dependencies {
implementation(project(':lib-textinterceptor'))
}

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.extension.all.webtoons
import eu.kanade.tachiyomi.multisrc.webtoons.Webtoons
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceFactory
import java.text.SimpleDateFormat
@ -18,8 +17,8 @@ class WebtoonsFactory : SourceFactory {
WebtoonsDE(),
)
}
class WebtoonsEN : Webtoons("Webtoons.com", "https://www.webtoons.com", "en")
class WebtoonsID : Webtoons("Webtoons.com", "https://www.webtoons.com", "id") {
class WebtoonsEN : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "en")
class WebtoonsID : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "id") {
// Override ID as part of the name was removed to be more consiten with other enteries
override val id: Long = 8749627068478740298
@ -36,8 +35,8 @@ class WebtoonsID : Webtoons("Webtoons.com", "https://www.webtoons.com", "id") {
return GregorianCalendar(year.toInt(), monthIndex, day.toInt()).time.time
}
}
class WebtoonsTH : Webtoons("Webtoons.com", "https://www.webtoons.com", "th", dateFormat = SimpleDateFormat("d MMM yyyy", Locale("th")))
class WebtoonsES : Webtoons("Webtoons.com", "https://www.webtoons.com", "es") {
class WebtoonsTH : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "th", dateFormat = SimpleDateFormat("d MMM yyyy", Locale("th")))
class WebtoonsES : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "es") {
// Android seems to be unable to parse es dates like Indonesian; we'll use a short hard-coded table instead.
private val dateMap: Array<String> = arrayOf(
"ene", "feb", "mar", "abr", "may", "jun", "jul", "ago", "sep", "oct", "nov", "dic"
@ -50,9 +49,9 @@ class WebtoonsES : Webtoons("Webtoons.com", "https://www.webtoons.com", "es") {
return GregorianCalendar(year.toInt(), monthIndex, day.toInt()).time.time
}
}
class WebtoonsFR : Webtoons("Webtoons.com", "https://www.webtoons.com", "fr", dateFormat = SimpleDateFormat("d MMM yyyy", Locale.FRENCH))
class WebtoonsZH : Webtoons("Webtoons.com", "https://www.webtoons.com", "zh-Hant", "zh-hant", "zh_TW", SimpleDateFormat("yyyy/MM/dd", Locale.TRADITIONAL_CHINESE)) {
class WebtoonsFR : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "fr", dateFormat = SimpleDateFormat("d MMM yyyy", Locale.FRENCH))
class WebtoonsZH : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "zh-Hant", "zh-hant", "zh_TW", SimpleDateFormat("yyyy/MM/dd", Locale.TRADITIONAL_CHINESE)) {
// Due to lang code getting more specific
override val id: Long = 2959982438613576472
}
class WebtoonsDE : Webtoons("Webtoons.com", "https://www.webtoons.com", "de", dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN))
class WebtoonsDE : WebtoonsSrc("Webtoons.com", "https://www.webtoons.com", "de", dateFormat = SimpleDateFormat("dd.MM.yyyy", Locale.GERMAN))

View File

@ -0,0 +1,97 @@
package eu.kanade.tachiyomi.extension.all.webtoons
import android.app.Application
import android.content.SharedPreferences
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
import eu.kanade.tachiyomi.multisrc.webtoons.Webtoons
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Page
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.OkHttpClient
import org.jsoup.nodes.Document
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.SimpleDateFormat
import java.util.Locale
open class WebtoonsSrc(
override val name: String,
override val baseUrl: String,
override val lang: String,
langCode: String = lang,
override val localeForCookie: String = lang,
dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
) : ConfigurableSource, Webtoons(name, baseUrl, lang, langCode, localeForCookie, dateFormat) {
override val client: OkHttpClient = super.client.newBuilder()
.addInterceptor(TextInterceptor())
.build()
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply {
key = SHOW_AUTHORS_NOTES_KEY
title = "Show author's notes"
summary = "Enable to see the author's notes at the end of chapters (if they're there)."
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean(SHOW_AUTHORS_NOTES_KEY, checkValue).commit()
}
}
screen.addPreference(authorsNotesPref)
}
private fun showAuthorsNotesPref() = preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false)
override fun pageListParse(document: Document): List<Page> {
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
if (showAuthorsNotesPref()) {
val note = document.select("div.comment_area div.info_area p").text()
if (note.isNotEmpty()) {
val creator = document.select("div.creator_note span.author a").text().trim()
pages = pages + Page(
pages.size, "",
TextInterceptorHelper.createUrl(creator, note)
)
}
}
if (pages.isNotEmpty()) { return pages }
val docString = document.toString()
val docUrlRegex = Regex("documentURL:.*?'(.*?)'")
val motiontoonPathRegex = Regex("jpg:.*?'(.*?)\\{")
val docUrl = docUrlRegex.find(docString)!!.destructured.toList()[0]
val motiontoonPath = motiontoonPathRegex.find(docString)!!.destructured.toList()[0]
val motiontoonResponse = client.newCall(GET(docUrl, headers)).execute()
val motiontoonJson = json.parseToJsonElement(motiontoonResponse.body!!.string()).jsonObject
val motiontoonImages = motiontoonJson["assets"]!!.jsonObject["image"]!!.jsonObject
return motiontoonImages.entries
.filter { it.key.contains("layer") }
.mapIndexed { i, entry ->
Page(i, "", motiontoonPath + entry.value.jsonPrimitive.content)
}
}
companion object {
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
}
}

View File

@ -1,19 +1,6 @@
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
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter.Header
import eu.kanade.tachiyomi.source.model.Filter.Select
import eu.kanade.tachiyomi.source.model.Filter.Separator
@ -33,19 +20,13 @@ 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.net.SocketException
import java.text.ParseException
import java.text.SimpleDateFormat
@ -59,7 +40,7 @@ open class Webtoons(
open val langCode: String = lang,
open val localeForCookie: String = lang,
private val dateFormat: SimpleDateFormat = SimpleDateFormat("MMM d, yyyy", Locale.ENGLISH)
) : ConfigurableSource, ParsedHttpSource() {
) : ParsedHttpSource() {
override val supportsLatest = true
@ -84,7 +65,6 @@ open class Webtoons(
}
)
.addInterceptor(::sslRetryInterceptor)
.addInterceptor(TextInterceptor)
.build()
// m.webtoons.com throws an SSL error that can be solved by a simple retry
@ -112,16 +92,12 @@ open class Webtoons(
}
}
private val json: Json by injectLazy()
val json: Json by injectLazy()
override fun popularMangaSelector() = "not using"
override fun latestUpdatesSelector() = "div#dailyList > $day li > a"
private val preferences: SharedPreferences by lazy {
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)
}
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.add("Referer", "https://www.webtoons.com/$langCode/")
@ -129,23 +105,6 @@ open class Webtoons(
.add("Referer", "https://m.webtoons.com")
.build()
override fun setupPreferenceScreen(screen: PreferenceScreen) {
val authorsNotesPref = SwitchPreferenceCompat(screen.context).apply {
key = SHOW_AUTHORS_NOTES_KEY
title = "Show author's notes"
summary = "Enable to see the author's notes at the end of chapters (if they're there)."
setDefaultValue(false)
setOnPreferenceChangeListener { _, newValue ->
val checkValue = newValue as Boolean
preferences.edit().putBoolean(SHOW_AUTHORS_NOTES_KEY, checkValue).commit()
}
}
screen.addPreference(authorsNotesPref)
}
private fun showAuthorsNotesPref() = preferences.getBoolean(SHOW_AUTHORS_NOTES_KEY, false)
override fun popularMangaRequest(page: Int) = GET("$baseUrl/$langCode/dailySchedule", headers)
override fun popularMangaParse(response: Response): MangasPage {
@ -317,20 +276,6 @@ open class Webtoons(
override fun pageListParse(document: Document): List<Page> {
var pages = document.select("div#_imageList > img").mapIndexed { i, element -> Page(i, "", element.attr("data-url")) }
if (showAuthorsNotesPref()) {
val note = document.select("div.comment_area div.info_area p").text()
if (note.isNotEmpty()) {
val creator = document.select("div.creator_note span.author a").text().trim()
pages = pages + Page(
pages.size, "",
"http://note/" + Uri.encode(creator) + "/" + Uri.encode(note)
)
}
}
if (pages.isNotEmpty()) { return pages }
val docString = document.toString()
@ -354,102 +299,5 @@ open class Webtoons(
companion object {
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

@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.contentOrNull
@ -24,7 +23,6 @@ import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import rx.Observable
import uy.kohesive.injekt.injectLazy
open class WebtoonsTranslate(
override val name: String,
@ -42,8 +40,6 @@ open class WebtoonsTranslate(
private val pageSize = 24
private val json: Json by injectLazy()
override fun headersBuilder(): Headers.Builder = super.headersBuilder()
.removeAll("Referer")
.add("Referer", mobileBaseUrl.toString())

View File

@ -1,6 +1,6 @@
include(":core")
listOf("dataimage", "unpacker", "cryptoaes").forEach {
listOf("dataimage", "unpacker", "cryptoaes", "textinterceptor").forEach {
include(":lib-$it")
project(":lib-$it").projectDir = File("lib/$it")
}

View File

@ -11,3 +11,7 @@ ext {
}
apply from: "$rootDir/common.gradle"
dependencies {
implementation(project(':lib-textinterceptor'))
}

View File

@ -2,18 +2,12 @@ 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
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptor
import eu.kanade.tachiyomi.lib.textinterceptor.TextInterceptorHelper
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.model.Filter
@ -33,20 +27,15 @@ 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
@ -108,7 +97,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
}
}
)
.addInterceptor(TextInterceptor)
.addInterceptor(TextInterceptor())
.build()
private val preferences: SharedPreferences by lazy {
@ -372,7 +361,7 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
pages = pages + Page(
pages.size, "",
"http://note/" + Uri.encode(creator) + "/" + Uri.encode(episodeStory)
TextInterceptorHelper.createUrl(creator, episodeStory)
)
}
}
@ -528,100 +517,4 @@ class Tapastic : ConfigurableSource, ParsedHttpSource() {
private const val SHOW_AUTHORS_NOTES_KEY = "showAuthorsNotes"
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:105.0) Gecko/20100101 Firefox/105.0"
}
// 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()
}
}
}