Add MangaReader Extension (#11811)
* Add MangaReader extension * Add quality preference * Fix text search * Unscramble images * Implement requested changes - Only un-shuffle shuffled images - Update icons Co-authored-by: ObserverOfTime <chronobserver@disroot.org>
This commit is contained in:
parent
adb8b29dda
commit
8d76062832
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest package="eu.kanade.tachiyomi.extension" />
|
|
@ -0,0 +1,12 @@
|
||||||
|
apply plugin: 'com.android.application'
|
||||||
|
apply plugin: 'kotlin-android'
|
||||||
|
|
||||||
|
ext {
|
||||||
|
extName = 'MangaReader'
|
||||||
|
pkgNameSuffix = 'all.mangareaderto'
|
||||||
|
extClass = '.MangaReaderFactory'
|
||||||
|
extVersionCode = 1
|
||||||
|
isNsfw = true
|
||||||
|
}
|
||||||
|
|
||||||
|
apply from: "$rootDir/common.gradle"
|
Binary file not shown.
After Width: | Height: | Size: 5.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
After Width: | Height: | Size: 7.6 KiB |
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
After Width: | Height: | Size: 20 KiB |
Binary file not shown.
After Width: | Height: | Size: 122 KiB |
|
@ -0,0 +1,196 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.preference.ListPreference
|
||||||
|
import androidx.preference.PreferenceScreen
|
||||||
|
import eu.kanade.tachiyomi.network.GET
|
||||||
|
import eu.kanade.tachiyomi.network.asObservableSuccess
|
||||||
|
import eu.kanade.tachiyomi.source.ConfigurableSource
|
||||||
|
import eu.kanade.tachiyomi.source.model.FilterList
|
||||||
|
import eu.kanade.tachiyomi.source.model.Page
|
||||||
|
import eu.kanade.tachiyomi.source.model.SChapter
|
||||||
|
import eu.kanade.tachiyomi.source.model.SManga
|
||||||
|
import eu.kanade.tachiyomi.source.online.ParsedHttpSource
|
||||||
|
import eu.kanade.tachiyomi.util.asJsoup
|
||||||
|
import org.json.JSONObject
|
||||||
|
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
|
||||||
|
|
||||||
|
open class MangaReader(
|
||||||
|
override val lang: String
|
||||||
|
) : ConfigurableSource, ParsedHttpSource() {
|
||||||
|
override val name = "MangaReader"
|
||||||
|
|
||||||
|
override val baseUrl = "https://mangareader.to"
|
||||||
|
|
||||||
|
override val supportsLatest = true
|
||||||
|
|
||||||
|
override val client = network.client.newBuilder()
|
||||||
|
.addInterceptor(MangaReaderImageInterceptor())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
override fun latestUpdatesRequest(page: Int) =
|
||||||
|
GET("$baseUrl/filter?sort=latest-updated&language=$lang&page=$page", headers)
|
||||||
|
|
||||||
|
override fun latestUpdatesSelector() = searchMangaSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesNextPageSelector() = searchMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun latestUpdatesFromElement(element: Element) =
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun popularMangaRequest(page: Int) =
|
||||||
|
GET("$baseUrl/filter?sort=most-viewed&language=$lang&page=$page", headers)
|
||||||
|
|
||||||
|
override fun popularMangaSelector() = searchMangaSelector()
|
||||||
|
|
||||||
|
override fun popularMangaNextPageSelector() = searchMangaNextPageSelector()
|
||||||
|
|
||||||
|
override fun popularMangaFromElement(element: Element) =
|
||||||
|
searchMangaFromElement(element)
|
||||||
|
|
||||||
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList) =
|
||||||
|
if (query.isNotBlank()) {
|
||||||
|
Uri.parse("$baseUrl/search").buildUpon().run {
|
||||||
|
appendQueryParameter("keyword", query)
|
||||||
|
appendQueryParameter("page", page.toString())
|
||||||
|
|
||||||
|
GET(toString(), headers)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Uri.parse("$baseUrl/filter").buildUpon().run {
|
||||||
|
appendQueryParameter("language", lang)
|
||||||
|
appendQueryParameter("page", page.toString())
|
||||||
|
filters.ifEmpty(::getFilterList).forEach { filter ->
|
||||||
|
when (filter) {
|
||||||
|
is Select -> {
|
||||||
|
appendQueryParameter(filter.param, filter.selection)
|
||||||
|
}
|
||||||
|
is DateFilter -> {
|
||||||
|
filter.state.forEach {
|
||||||
|
appendQueryParameter(it.param, it.selection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is GenresFilter -> {
|
||||||
|
appendQueryParameter(filter.param, filter.selection)
|
||||||
|
}
|
||||||
|
else -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
GET(toString(), headers)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun searchMangaSelector() = ".manga_list-sbs .manga-poster"
|
||||||
|
|
||||||
|
override fun searchMangaNextPageSelector() = ".page-link[title=Next]"
|
||||||
|
|
||||||
|
override fun searchMangaFromElement(element: Element) =
|
||||||
|
SManga.create().apply {
|
||||||
|
url = element.attr("href")
|
||||||
|
element.selectFirst(".manga-poster-img").let {
|
||||||
|
title = it.attr("alt")
|
||||||
|
thumbnail_url = it.attr("src")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val authorSelector = ".item-head:containsOwn(Authors) ~ a"
|
||||||
|
|
||||||
|
private val statusSelector = ".item-head:containsOwn(Status) + .name"
|
||||||
|
|
||||||
|
override fun mangaDetailsParse(document: Document) =
|
||||||
|
SManga.create().apply {
|
||||||
|
setUrlWithoutDomain(document.location())
|
||||||
|
document.getElementById("ani_detail").let { el ->
|
||||||
|
title = el.selectFirst(".manga-name").text().trim()
|
||||||
|
description = el.selectFirst(".description")?.text()?.trim()
|
||||||
|
thumbnail_url = el.selectFirst(".manga-poster-img").attr("src")
|
||||||
|
genre = el.select(".genres > a")?.joinToString { it.text() }
|
||||||
|
author = el.select(authorSelector)?.joinToString {
|
||||||
|
it.text().replace(",", "")
|
||||||
|
}
|
||||||
|
artist = author // TODO: separate authors and artists
|
||||||
|
status = when (el.selectFirst(statusSelector)?.text()) {
|
||||||
|
"Finished" -> SManga.COMPLETED
|
||||||
|
"Publishing" -> SManga.ONGOING
|
||||||
|
else -> SManga.UNKNOWN
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun chapterListSelector() = "#$lang-chapters .item"
|
||||||
|
|
||||||
|
override fun chapterFromElement(element: Element) =
|
||||||
|
SChapter.create().apply {
|
||||||
|
chapter_number = element.attr("data-number").toFloatOrNull() ?: -1f
|
||||||
|
element.selectFirst(".item-link").let {
|
||||||
|
url = it.attr("href")
|
||||||
|
name = it.attr("title")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun pageListRequest(id: String) =
|
||||||
|
GET("$baseUrl/ajax/image/list/chap/$id?quality=$quality", headers)
|
||||||
|
|
||||||
|
override fun fetchPageList(chapter: SChapter) =
|
||||||
|
client.newCall(pageListRequest(chapter)).asObservableSuccess().map { res ->
|
||||||
|
res.asJsoup().getElementById("wrapper").attr("data-reading-id").let {
|
||||||
|
val call = client.newCall(pageListRequest(it))
|
||||||
|
val json = JSONObject(call.execute().body!!.string())
|
||||||
|
pageListParse(Jsoup.parse(json.getString("html")))
|
||||||
|
}
|
||||||
|
}!!
|
||||||
|
|
||||||
|
override fun pageListParse(document: Document): List<Page> =
|
||||||
|
document.getElementsByClass("iv-card").mapIndexed { idx, img ->
|
||||||
|
val url = img.attr("data-url")
|
||||||
|
if (img.hasClass("shuffled")) {
|
||||||
|
Page(idx, "", "$url&shuffled=true")
|
||||||
|
} else {
|
||||||
|
Page(idx, "", url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun imageUrlParse(document: Document) =
|
||||||
|
throw UnsupportedOperationException("Not used")
|
||||||
|
|
||||||
|
private val preferences by lazy {
|
||||||
|
Injekt.get<Application>().getSharedPreferences("source_$id", 0x0000)!!
|
||||||
|
}
|
||||||
|
|
||||||
|
private val quality by lazy {
|
||||||
|
preferences.getString("quality", "medium")!!
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setupPreferenceScreen(screen: PreferenceScreen) {
|
||||||
|
ListPreference(screen.context).apply {
|
||||||
|
key = "quality"
|
||||||
|
title = "Quality"
|
||||||
|
summary = "%s"
|
||||||
|
entries = arrayOf("Low", "Medium", "High")
|
||||||
|
entryValues = arrayOf("low", "medium", "high")
|
||||||
|
setDefaultValue("medium")
|
||||||
|
|
||||||
|
setOnPreferenceChangeListener { _, newValue ->
|
||||||
|
preferences.edit().putString("quality", newValue as String).commit()
|
||||||
|
}
|
||||||
|
}.let(screen::addPreference)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getFilterList() =
|
||||||
|
FilterList(
|
||||||
|
Note,
|
||||||
|
TypeFilter(),
|
||||||
|
StatusFilter(),
|
||||||
|
RatingFilter(),
|
||||||
|
ScoreFilter(),
|
||||||
|
StartDateFilter(),
|
||||||
|
EndDateFilter(),
|
||||||
|
SortFilter(),
|
||||||
|
GenresFilter()
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,8 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.SourceFactory
|
||||||
|
|
||||||
|
class MangaReaderFactory : SourceFactory {
|
||||||
|
override fun createSources() =
|
||||||
|
listOf(MangaReader("en"), MangaReader("ja"))
|
||||||
|
}
|
|
@ -0,0 +1,247 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.source.model.Filter
|
||||||
|
import java.util.Calendar
|
||||||
|
|
||||||
|
object Note : Filter.Text("NOTE: Ignored if using text search!")
|
||||||
|
|
||||||
|
sealed class Select(
|
||||||
|
name: String,
|
||||||
|
val param: String,
|
||||||
|
values: Array<String>
|
||||||
|
) : Filter.Select<String>(name, values) {
|
||||||
|
open val selection: String
|
||||||
|
get() = if (state == 0) "" else state.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
class TypeFilter(
|
||||||
|
values: Array<String> = types
|
||||||
|
) : Select("Type", "type", values) {
|
||||||
|
companion object {
|
||||||
|
private val types: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"All",
|
||||||
|
"Manga",
|
||||||
|
"One-Shot",
|
||||||
|
"Doujinshi",
|
||||||
|
"Light Novel",
|
||||||
|
"Manhwa",
|
||||||
|
"Manhua",
|
||||||
|
"Comic"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class StatusFilter(
|
||||||
|
values: Array<String> = statuses
|
||||||
|
) : Select("Status", "status", values) {
|
||||||
|
companion object {
|
||||||
|
private val statuses: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"All",
|
||||||
|
"Finished",
|
||||||
|
"Publishing",
|
||||||
|
"On Hiatus",
|
||||||
|
"Discontinued",
|
||||||
|
"Not yet published"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RatingFilter(
|
||||||
|
values: Array<String> = ratings
|
||||||
|
) : Select("Rating Type", "rating_type", values) {
|
||||||
|
companion object {
|
||||||
|
private val ratings: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"All",
|
||||||
|
"G - All Ages",
|
||||||
|
"PG - Children",
|
||||||
|
"PG-13 - Teens 13 or older",
|
||||||
|
"R - 17+ (violence & profanity)",
|
||||||
|
"R+ - Mild Nudity",
|
||||||
|
"Rx - Hentai"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ScoreFilter(
|
||||||
|
values: Array<String> = scores
|
||||||
|
) : Select("Score", "score", values) {
|
||||||
|
companion object {
|
||||||
|
private val scores: Array<String>
|
||||||
|
get() = arrayOf(
|
||||||
|
"All",
|
||||||
|
"(1) Appalling",
|
||||||
|
"(2) Horrible",
|
||||||
|
"(3) Very Bad",
|
||||||
|
"(4) Bad",
|
||||||
|
"(5) Average",
|
||||||
|
"(6) Fine",
|
||||||
|
"(7) Good",
|
||||||
|
"(8) Very Good",
|
||||||
|
"(9) Great",
|
||||||
|
"(10) Masterpiece"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DateSelect(
|
||||||
|
name: String,
|
||||||
|
param: String,
|
||||||
|
values: Array<String>
|
||||||
|
) : Select(name, param, values) {
|
||||||
|
override val selection: String
|
||||||
|
get() = if (state == 0) "" else values[state]
|
||||||
|
}
|
||||||
|
|
||||||
|
class YearFilter(
|
||||||
|
param: String,
|
||||||
|
values: Array<String> = years
|
||||||
|
) : DateSelect("Year", param, values) {
|
||||||
|
companion object {
|
||||||
|
private val nextYear by lazy {
|
||||||
|
Calendar.getInstance()[Calendar.YEAR] + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private val years: Array<String>
|
||||||
|
get() = Array(nextYear - 1916) {
|
||||||
|
if (it == 0) "Any" else (nextYear - it).toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MonthFilter(
|
||||||
|
param: String,
|
||||||
|
values: Array<String> = months
|
||||||
|
) : DateSelect("Month", param, values) {
|
||||||
|
companion object {
|
||||||
|
private val months: Array<String>
|
||||||
|
get() = Array(13) {
|
||||||
|
if (it == 0) "Any" else "%02d".format(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class DayFilter(
|
||||||
|
param: String,
|
||||||
|
values: Array<String> = days
|
||||||
|
) : DateSelect("Day", param, values) {
|
||||||
|
companion object {
|
||||||
|
private val days: Array<String>
|
||||||
|
get() = Array(32) {
|
||||||
|
if (it == 0) "Any" else "%02d".format(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class DateFilter(
|
||||||
|
type: String,
|
||||||
|
values: List<DateSelect>
|
||||||
|
) : Filter.Group<DateSelect>("$type Date", values)
|
||||||
|
|
||||||
|
class StartDateFilter(
|
||||||
|
values: List<DateSelect> = parts
|
||||||
|
) : DateFilter("Start", values) {
|
||||||
|
companion object {
|
||||||
|
private val parts: List<DateSelect>
|
||||||
|
get() = listOf(
|
||||||
|
YearFilter("sy"),
|
||||||
|
MonthFilter("sm"),
|
||||||
|
DayFilter("sd")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class EndDateFilter(
|
||||||
|
values: List<DateSelect> = parts
|
||||||
|
) : DateFilter("End", values) {
|
||||||
|
companion object {
|
||||||
|
private val parts: List<DateSelect>
|
||||||
|
get() = listOf(
|
||||||
|
YearFilter("ey"),
|
||||||
|
MonthFilter("em"),
|
||||||
|
DayFilter("ed")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SortFilter(
|
||||||
|
values: Array<String> = orders.keys.toTypedArray()
|
||||||
|
) : Select("Sort", "sort", values) {
|
||||||
|
override val selection: String
|
||||||
|
get() = orders[values[state]]!!
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val orders = mapOf(
|
||||||
|
"Default" to "default",
|
||||||
|
"Latest Updated" to "latest-updated",
|
||||||
|
"Score" to "score",
|
||||||
|
"Name A-Z" to "name-az",
|
||||||
|
"Release Date" to "release-date",
|
||||||
|
"Most Viewed" to "most-viewed"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Genre(name: String, val id: String) : Filter.CheckBox(name)
|
||||||
|
|
||||||
|
class GenresFilter(
|
||||||
|
values: List<Genre> = genres
|
||||||
|
) : Filter.Group<Genre>("Genres", values) {
|
||||||
|
val param = "genres"
|
||||||
|
|
||||||
|
val selection: String
|
||||||
|
get() = state.filter { it.state }.joinToString(",") { it.id }
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val genres: List<Genre>
|
||||||
|
get() = listOf(
|
||||||
|
Genre("Action", "1"),
|
||||||
|
Genre("Adventure", "2"),
|
||||||
|
Genre("Cars", "3"),
|
||||||
|
Genre("Comedy", "4"),
|
||||||
|
Genre("Dementia", "5"),
|
||||||
|
Genre("Demons", "6"),
|
||||||
|
Genre("Doujinshi", "7"),
|
||||||
|
Genre("Drama", "8"),
|
||||||
|
Genre("Ecchi", "9"),
|
||||||
|
Genre("Fantasy", "10"),
|
||||||
|
Genre("Game", "11"),
|
||||||
|
Genre("Gender Bender", "12"),
|
||||||
|
Genre("Harem", "13"),
|
||||||
|
Genre("Hentai", "14"),
|
||||||
|
Genre("Historical", "15"),
|
||||||
|
Genre("Horror", "16"),
|
||||||
|
Genre("Josei", "17"),
|
||||||
|
Genre("Kids", "18"),
|
||||||
|
Genre("Magic", "19"),
|
||||||
|
Genre("Martial Arts", "20"),
|
||||||
|
Genre("Mecha", "21"),
|
||||||
|
Genre("Military", "22"),
|
||||||
|
Genre("Music", "23"),
|
||||||
|
Genre("Mystery", "24"),
|
||||||
|
Genre("Parody", "25"),
|
||||||
|
Genre("Police", "26"),
|
||||||
|
Genre("Psychological", "27"),
|
||||||
|
Genre("Romance", "28"),
|
||||||
|
Genre("Samurai", "29"),
|
||||||
|
Genre("School", "30"),
|
||||||
|
Genre("Sci-Fi", "31"),
|
||||||
|
Genre("Seinen", "32"),
|
||||||
|
Genre("Shoujo", "33"),
|
||||||
|
Genre("Shoujo Ai", "34"),
|
||||||
|
Genre("Shounen", "35"),
|
||||||
|
Genre("Shounen Ai", "36"),
|
||||||
|
Genre("Slice of Life", "37"),
|
||||||
|
Genre("Space", "38"),
|
||||||
|
Genre("Sports", "39"),
|
||||||
|
Genre("Super Power", "40"),
|
||||||
|
Genre("Supernatural", "41"),
|
||||||
|
Genre("Thriller", "42"),
|
||||||
|
Genre("Vampire", "43"),
|
||||||
|
Genre("Yaoi", "44"),
|
||||||
|
Genre("Yuri", "45"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,192 @@
|
||||||
|
package eu.kanade.tachiyomi.extension.all.mangareaderto
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Rect
|
||||||
|
import okhttp3.Interceptor
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.Response
|
||||||
|
import okhttp3.ResponseBody.Companion.toResponseBody
|
||||||
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.InputStream
|
||||||
|
import kotlin.math.ceil
|
||||||
|
import kotlin.math.floor
|
||||||
|
|
||||||
|
class MangaReaderImageInterceptor : Interceptor {
|
||||||
|
|
||||||
|
private var s = IntArray(256)
|
||||||
|
private var arc4i = 0
|
||||||
|
private var arc4j = 0
|
||||||
|
|
||||||
|
override fun intercept(chain: Interceptor.Chain): Response {
|
||||||
|
val response = chain.proceed(chain.request())
|
||||||
|
|
||||||
|
// shuffled page requests should have shuffled=true query parameter
|
||||||
|
if (chain.request().url.queryParameter("shuffled") != "true")
|
||||||
|
return response
|
||||||
|
|
||||||
|
val image = unscrambleImage(response.body!!.byteStream())
|
||||||
|
val body = image.toResponseBody("image/png".toMediaTypeOrNull())
|
||||||
|
return response.newBuilder()
|
||||||
|
.body(body)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun unscrambleImage(image: InputStream): ByteArray {
|
||||||
|
// obfuscated code (imgReverser function): https://mangareader.to/js/read.min.js
|
||||||
|
// essentially, it shuffles arrays of the image slices using the key 'stay'
|
||||||
|
|
||||||
|
val bitmap = BitmapFactory.decodeStream(image)
|
||||||
|
|
||||||
|
val result = Bitmap.createBitmap(bitmap.width, bitmap.height, Bitmap.Config.ARGB_8888)
|
||||||
|
val canvas = Canvas(result)
|
||||||
|
|
||||||
|
val horizontalParts = ceil(bitmap.width / SLICE_SIZE.toDouble()).toInt()
|
||||||
|
val totalParts = horizontalParts * ceil(bitmap.height / SLICE_SIZE.toDouble()).toInt()
|
||||||
|
|
||||||
|
// calculate slices
|
||||||
|
val slices: HashMap<Int, MutableList<Rect>> = hashMapOf()
|
||||||
|
|
||||||
|
for (i in 0 until totalParts) {
|
||||||
|
val row = floor(i / horizontalParts.toDouble()).toInt()
|
||||||
|
|
||||||
|
val x = (i - row * horizontalParts) * SLICE_SIZE
|
||||||
|
val y = row * SLICE_SIZE
|
||||||
|
val width = if (x + SLICE_SIZE <= bitmap.width) SLICE_SIZE else bitmap.width - x
|
||||||
|
val height = if (y + SLICE_SIZE <= bitmap.height) SLICE_SIZE else bitmap.height - y
|
||||||
|
|
||||||
|
val srcRect = Rect(x, y, width, height)
|
||||||
|
val key = width - height
|
||||||
|
if (!slices.containsKey(key)) {
|
||||||
|
slices[key] = mutableListOf()
|
||||||
|
}
|
||||||
|
slices[key]?.add(srcRect)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle groups of slices
|
||||||
|
for (sliceEntry in slices) {
|
||||||
|
// reset random number generator for every un-shuffle
|
||||||
|
resetRng()
|
||||||
|
|
||||||
|
val currentSlices = sliceEntry.value
|
||||||
|
val sliceCount = currentSlices.count()
|
||||||
|
|
||||||
|
// un-shuffle slice indices
|
||||||
|
val orderedSlices = IntArray(sliceCount)
|
||||||
|
val keys = MutableList(sliceCount) { it }
|
||||||
|
|
||||||
|
for (i in currentSlices.indices) {
|
||||||
|
val r = floor(prng() * keys.count()).toInt()
|
||||||
|
val g = keys[r]
|
||||||
|
keys.removeAt(r)
|
||||||
|
orderedSlices[g] = i
|
||||||
|
}
|
||||||
|
|
||||||
|
// draw slices
|
||||||
|
val cols = getColumnCount(currentSlices)
|
||||||
|
|
||||||
|
val groupX = currentSlices[0].left
|
||||||
|
val groupY = currentSlices[0].top
|
||||||
|
|
||||||
|
for ((i, orderedIndex) in orderedSlices.withIndex()) {
|
||||||
|
val slice = currentSlices[i]
|
||||||
|
|
||||||
|
val row = floor((orderedIndex / cols).toDouble()).toInt()
|
||||||
|
val col = orderedIndex - row * cols
|
||||||
|
|
||||||
|
val width = slice.right
|
||||||
|
val height = slice.bottom
|
||||||
|
|
||||||
|
val x = groupX + col * width
|
||||||
|
val y = groupY + row * height
|
||||||
|
|
||||||
|
val srcRect = Rect(x, y, x + width, y + height)
|
||||||
|
val dstRect = Rect(
|
||||||
|
slice.left,
|
||||||
|
slice.top,
|
||||||
|
slice.left + width,
|
||||||
|
slice.top + height
|
||||||
|
)
|
||||||
|
|
||||||
|
canvas.drawBitmap(bitmap, srcRect, dstRect, null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val output = ByteArrayOutputStream()
|
||||||
|
result.compress(Bitmap.CompressFormat.PNG, 100, output)
|
||||||
|
return output.toByteArray()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getColumnCount(slices: List<Rect>): Int {
|
||||||
|
if (slices.count() == 1) return 1
|
||||||
|
var t: Int? = null
|
||||||
|
for (i in slices.indices) {
|
||||||
|
if (t == null) t = slices[i].top
|
||||||
|
if (t != slices[i].top) {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return slices.count()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resetRng() {
|
||||||
|
arc4i = 0
|
||||||
|
arc4j = 0
|
||||||
|
initializeS()
|
||||||
|
arc4(256) // RC4-drop[256]
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initializeS() {
|
||||||
|
val t = IntArray(256)
|
||||||
|
for (i in 0..255) {
|
||||||
|
s[i] = i
|
||||||
|
t[i] = KEY[i % KEY.size]
|
||||||
|
}
|
||||||
|
var j = 0
|
||||||
|
var tmp: Int
|
||||||
|
for (i in 0..255) {
|
||||||
|
j = (j + s[i] + t[i]) and 0xFF
|
||||||
|
tmp = s[j]
|
||||||
|
s[j] = s[i]
|
||||||
|
s[i] = tmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun prng(): Double {
|
||||||
|
var n = arc4(6)
|
||||||
|
var d = 281474976710656.0 // 256^6 (start with 6 chunks in n)
|
||||||
|
var x = 0L
|
||||||
|
while (n < 4503599627370496) { // 2^52 (52 significant digits in a double)
|
||||||
|
n = (n + x) * 256
|
||||||
|
d *= 256
|
||||||
|
x = arc4(1)
|
||||||
|
if (n < 0) break // overflow
|
||||||
|
}
|
||||||
|
return (n + x) / d
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun arc4(count: Int): Long {
|
||||||
|
var t: Int
|
||||||
|
var tmp: Int
|
||||||
|
var r: Long = 0
|
||||||
|
|
||||||
|
repeat(count) {
|
||||||
|
arc4i = (arc4i + 1) and 0xFF
|
||||||
|
arc4j = (arc4j + s[arc4i]) and 0xFF
|
||||||
|
tmp = s[arc4j]
|
||||||
|
s[arc4j] = s[arc4i]
|
||||||
|
s[arc4i] = tmp
|
||||||
|
t = (s[arc4i] + s[arc4j]) and 0xFF
|
||||||
|
|
||||||
|
r = r * 256 + s[t]
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val KEY = "stay".map { it.toByte().toInt() }
|
||||||
|
private const val SLICE_SIZE = 200
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue