Fix image decoding method at Viz Shonen Jump. (#4150)
This commit is contained in:
parent
d239be6c9a
commit
b01cf8e0ba
@ -5,12 +5,12 @@ ext {
|
|||||||
extName = 'VIZ Shonen Jump'
|
extName = 'VIZ Shonen Jump'
|
||||||
pkgNameSuffix = 'en.vizshonenjump'
|
pkgNameSuffix = 'en.vizshonenjump'
|
||||||
extClass = '.VizShonenJump'
|
extClass = '.VizShonenJump'
|
||||||
extVersionCode = 2
|
extVersionCode = 3
|
||||||
libVersion = '1.2'
|
libVersion = '1.2'
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
implementation 'com.drewnoakes:metadata-extractor:2.14.0'
|
||||||
}
|
}
|
||||||
|
|
||||||
apply from: "$rootDir/common.gradle"
|
apply from: "$rootDir/common.gradle"
|
||||||
|
@ -4,9 +4,11 @@ import android.graphics.Bitmap
|
|||||||
import android.graphics.BitmapFactory
|
import android.graphics.BitmapFactory
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
import android.graphics.Rect
|
import android.graphics.Rect
|
||||||
import androidx.exifinterface.media.ExifInterface
|
import com.drew.imaging.ImageMetadataReader
|
||||||
|
import com.drew.metadata.exif.ExifSubIFDDirectory
|
||||||
import java.io.ByteArrayInputStream
|
import java.io.ByteArrayInputStream
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
import java.io.IOException
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
import okhttp3.Interceptor
|
import okhttp3.Interceptor
|
||||||
import okhttp3.MediaType
|
import okhttp3.MediaType
|
||||||
@ -33,14 +35,17 @@ class VizImageInterceptor : Interceptor {
|
|||||||
// See: https://github.com/inorichi/tachiyomi-extensions/issues/2678#issuecomment-645857603
|
// See: https://github.com/inorichi/tachiyomi-extensions/issues/2678#issuecomment-645857603
|
||||||
val byteOutputStream = ByteArrayOutputStream()
|
val byteOutputStream = ByteArrayOutputStream()
|
||||||
image.copyTo(byteOutputStream)
|
image.copyTo(byteOutputStream)
|
||||||
|
|
||||||
val byteInputStreamForImage = ByteArrayInputStream(byteOutputStream.toByteArray())
|
val byteInputStreamForImage = ByteArrayInputStream(byteOutputStream.toByteArray())
|
||||||
val byteInputStreamForExif = ByteArrayInputStream(byteOutputStream.toByteArray())
|
val byteInputStreamForMetadata = ByteArrayInputStream(byteOutputStream.toByteArray())
|
||||||
|
|
||||||
|
val imageData = getImageData(byteInputStreamForMetadata)
|
||||||
|
|
||||||
val input = BitmapFactory.decodeStream(byteInputStreamForImage)
|
val input = BitmapFactory.decodeStream(byteInputStreamForImage)
|
||||||
val width = input.width
|
val width = input.width
|
||||||
val height = input.height
|
val height = input.height
|
||||||
val newWidth = width - WIDTH_CUT
|
val newWidth = (width - WIDTH_CUT).coerceAtLeast(imageData.width)
|
||||||
val newHeight = height - HEIGHT_CUT
|
val newHeight = (height - HEIGHT_CUT).coerceAtLeast(imageData.height)
|
||||||
val blockWidth = newWidth / CELL_WIDTH_COUNT
|
val blockWidth = newWidth / CELL_WIDTH_COUNT
|
||||||
val blockHeight = newHeight / CELL_HEIGHT_COUNT
|
val blockHeight = newHeight / CELL_HEIGHT_COUNT
|
||||||
|
|
||||||
@ -48,32 +53,47 @@ class VizImageInterceptor : Interceptor {
|
|||||||
val canvas = Canvas(result)
|
val canvas = Canvas(result)
|
||||||
|
|
||||||
// Draw the borders.
|
// Draw the borders.
|
||||||
canvas.copyCell(input, Pair(0, 0), Pair(0, 0), newWidth, blockHeight)
|
|
||||||
canvas.copyCell(input,
|
|
||||||
Pair(0, blockHeight + 10), Pair(0, blockHeight),
|
|
||||||
blockWidth, newHeight - 2 * blockHeight)
|
|
||||||
canvas.copyCell(input,
|
|
||||||
Pair(0, (CELL_HEIGHT_COUNT - 1) * (blockHeight + 10)),
|
|
||||||
Pair(0, (CELL_HEIGHT_COUNT - 1) * blockHeight),
|
|
||||||
newWidth, height - (CELL_HEIGHT_COUNT - 1) * (blockHeight + 10))
|
|
||||||
canvas.copyCell(input,
|
|
||||||
Pair((CELL_WIDTH_COUNT - 1) * (blockWidth + 10), blockHeight + 10),
|
|
||||||
Pair((CELL_WIDTH_COUNT - 1) * blockWidth, blockHeight),
|
|
||||||
blockWidth + (newWidth - CELL_WIDTH_COUNT * blockWidth),
|
|
||||||
newHeight - 2 * blockHeight)
|
|
||||||
|
|
||||||
// Get the key from the EXIF tag.
|
// Top border.
|
||||||
val exifInterface = ExifInterface(byteInputStreamForExif)
|
canvas.drawImage(
|
||||||
val uniqueId = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID)!!
|
from = input,
|
||||||
val key = uniqueId.split(":")
|
srcX = 0, srcY = 0,
|
||||||
.map { it.toInt(16) }
|
dstX = 0, dstY = 0,
|
||||||
|
width = newWidth, height = blockHeight
|
||||||
|
)
|
||||||
|
// Left border.
|
||||||
|
canvas.drawImage(
|
||||||
|
from = input,
|
||||||
|
srcX = 0, srcY = blockHeight + 10,
|
||||||
|
dstX = 0, dstY = blockHeight,
|
||||||
|
width = blockWidth, height = newHeight - 2 * blockHeight
|
||||||
|
)
|
||||||
|
// Bottom border.
|
||||||
|
canvas.drawImage(
|
||||||
|
from = input,
|
||||||
|
srcX = 0, srcY = (CELL_HEIGHT_COUNT - 1) * (blockHeight + 10),
|
||||||
|
dstX = 0, dstY = (CELL_HEIGHT_COUNT - 1) * blockHeight,
|
||||||
|
width = newWidth, height = height - (CELL_HEIGHT_COUNT - 1) * (blockHeight + 10)
|
||||||
|
)
|
||||||
|
// Right border.
|
||||||
|
canvas.drawImage(
|
||||||
|
from = input,
|
||||||
|
srcX = (CELL_WIDTH_COUNT - 1) * (blockWidth + 10), srcY = blockHeight + 10,
|
||||||
|
dstX = (CELL_WIDTH_COUNT - 1) * blockWidth, dstY = blockHeight,
|
||||||
|
width = blockWidth + (newWidth - CELL_WIDTH_COUNT * blockWidth),
|
||||||
|
height = newHeight - 2 * blockHeight
|
||||||
|
)
|
||||||
|
|
||||||
// Draw the inner cells.
|
// Draw the inner cells.
|
||||||
for ((m, y) in key.iterator().withIndex()) {
|
for ((m, y) in imageData.key.iterator().withIndex()) {
|
||||||
canvas.copyCell(input,
|
canvas.drawImage(
|
||||||
Pair((m % (CELL_WIDTH_COUNT - 2) + 1) * (blockWidth + 10), (m / (CELL_WIDTH_COUNT - 2) + 1) * (blockHeight + 10)),
|
from = input,
|
||||||
Pair((y % (CELL_WIDTH_COUNT - 2) + 1) * blockWidth, (y / (CELL_WIDTH_COUNT - 2) + 1) * blockHeight),
|
srcX = (m % INNER_CELL_COUNT + 1) * (blockWidth + 10),
|
||||||
blockWidth, blockHeight)
|
srcY = (m / INNER_CELL_COUNT + 1) * (blockHeight + 10),
|
||||||
|
dstX = (y % INNER_CELL_COUNT + 1) * blockWidth,
|
||||||
|
dstY = (y / INNER_CELL_COUNT + 1) * blockHeight,
|
||||||
|
width = blockWidth, height = blockHeight
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val output = ByteArrayOutputStream()
|
val output = ByteArrayOutputStream()
|
||||||
@ -81,19 +101,60 @@ class VizImageInterceptor : Interceptor {
|
|||||||
return output.toByteArray()
|
return output.toByteArray()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Canvas.copyCell(from: Bitmap, src: Pair<Int, Int>, dst: Pair<Int, Int>, width: Int, height: Int) {
|
private fun Canvas.drawImage(
|
||||||
val srcRect = Rect(src.first, src.second, src.first + width, src.second + height)
|
from: Bitmap,
|
||||||
val dstRect = Rect(dst.first, dst.second, dst.first + width, dst.second + height)
|
srcX: Int,
|
||||||
|
srcY: Int,
|
||||||
|
dstX: Int,
|
||||||
|
dstY: Int,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
) {
|
||||||
|
val srcRect = Rect(srcX, srcY, srcX + width, srcY + height)
|
||||||
|
val dstRect = Rect(dstX, dstY, dstX + width, dstY + height)
|
||||||
drawBitmap(from, srcRect, dstRect, null)
|
drawBitmap(from, srcRect, dstRect, null)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getImageData(inputStream: InputStream): ImageData {
|
||||||
|
val metadata = ImageMetadataReader.readMetadata(inputStream)
|
||||||
|
|
||||||
|
val sizeDir = metadata.directories.firstOrNull {
|
||||||
|
it.containsTag(ExifSubIFDDirectory.TAG_IMAGE_WIDTH) &&
|
||||||
|
it.containsTag(ExifSubIFDDirectory.TAG_IMAGE_HEIGHT)
|
||||||
|
}
|
||||||
|
val metaWidth = sizeDir?.getInt(ExifSubIFDDirectory.TAG_IMAGE_WIDTH) ?: COMMON_WIDTH
|
||||||
|
val metaHeight = sizeDir?.getInt(ExifSubIFDDirectory.TAG_IMAGE_HEIGHT) ?: COMMON_HEIGHT
|
||||||
|
|
||||||
|
val keyDir = metadata.directories.firstOrNull {
|
||||||
|
it.containsTag(ExifSubIFDDirectory.TAG_IMAGE_UNIQUE_ID)
|
||||||
|
}
|
||||||
|
val metaUniqueId = keyDir?.getString(ExifSubIFDDirectory.TAG_IMAGE_UNIQUE_ID)
|
||||||
|
?: throw IOException(KEY_NOT_FOUND)
|
||||||
|
|
||||||
|
return ImageData(metaWidth, metaHeight, metaUniqueId)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class ImageData(val width: Int, val height: Int, val uniqueId: String) {
|
||||||
|
val key: List<Int> by lazy {
|
||||||
|
uniqueId.split(":")
|
||||||
|
.map { it.toInt(16) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val SIGNATURE = "Signature"
|
private const val SIGNATURE = "Signature"
|
||||||
private val MEDIA_TYPE = MediaType.parse("image/png")
|
private val MEDIA_TYPE = MediaType.parse("image/png")
|
||||||
|
|
||||||
private const val CELL_WIDTH_COUNT = 10
|
private const val CELL_WIDTH_COUNT = 10
|
||||||
private const val CELL_HEIGHT_COUNT = 15
|
private const val CELL_HEIGHT_COUNT = 15
|
||||||
|
private const val INNER_CELL_COUNT = CELL_WIDTH_COUNT - 2
|
||||||
|
|
||||||
private const val WIDTH_CUT = 90
|
private const val WIDTH_CUT = 90
|
||||||
private const val HEIGHT_CUT = 140
|
private const val HEIGHT_CUT = 140
|
||||||
|
|
||||||
|
private const val COMMON_WIDTH = 800
|
||||||
|
private const val COMMON_HEIGHT = 1200
|
||||||
|
|
||||||
|
private const val KEY_NOT_FOUND = "Decryption key not found in image metadata."
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -57,11 +57,13 @@ class VizShonenJump : ParsedHttpSource() {
|
|||||||
return MangasPage(mangas.sortedBy { it.title }, false)
|
return MangasPage(mangas.sortedBy { it.title }, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun popularMangaSelector(): String = "section.section_chapters div.o_sort_container div.o_sortable > a"
|
override fun popularMangaSelector(): String =
|
||||||
|
"section.section_chapters div.o_sort_container div.o_sortable > a"
|
||||||
|
|
||||||
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
override fun popularMangaFromElement(element: Element): SManga = SManga.create().apply {
|
||||||
title = element.select("div.pad-x-rg").first().text()
|
title = element.select("div.pad-x-rg").first().text()
|
||||||
thumbnail_url = element.select("div.pos-r img.disp-bl").first()?.attr("data-original")
|
thumbnail_url = element.select("div.pos-r img.disp-bl").first()
|
||||||
|
?.attr("data-original")
|
||||||
url = element.attr("href")
|
url = element.attr("href")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,16 +82,21 @@ class VizShonenJump : ParsedHttpSource() {
|
|||||||
|
|
||||||
override fun latestUpdatesSelector() = popularMangaSelector()
|
override fun latestUpdatesSelector() = popularMangaSelector()
|
||||||
|
|
||||||
override fun latestUpdatesFromElement(element: Element): SManga = popularMangaFromElement(element)
|
override fun latestUpdatesFromElement(element: Element): SManga =
|
||||||
|
popularMangaFromElement(element)
|
||||||
|
|
||||||
override fun latestUpdatesNextPageSelector(): String? = null
|
override fun latestUpdatesNextPageSelector(): String? = null
|
||||||
|
|
||||||
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||||
return super.fetchSearchManga(page, query, filters)
|
return super.fetchSearchManga(page, query, filters)
|
||||||
.map { MangasPage(it.mangas.filter { m -> m.title.contains(query, true) }, it.hasNextPage) }
|
.map {
|
||||||
|
val filteredMangas = it.mangas.filter { m -> m.title.contains(query, true) }
|
||||||
|
MangasPage(filteredMangas, it.hasNextPage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request = popularMangaRequest(page)
|
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request =
|
||||||
|
popularMangaRequest(page)
|
||||||
|
|
||||||
override fun searchMangaParse(response: Response): MangasPage {
|
override fun searchMangaParse(response: Response): MangasPage {
|
||||||
val mangasPage = super.searchMangaParse(response)
|
val mangasPage = super.searchMangaParse(response)
|
||||||
@ -114,7 +121,7 @@ class VizShonenJump : ParsedHttpSource() {
|
|||||||
?.replace("Created by ", "")
|
?.replace("Created by ", "")
|
||||||
artist = author
|
artist = author
|
||||||
status = SManga.ONGOING
|
status = SManga.ONGOING
|
||||||
description = seriesIntro.select("h2").first().text()
|
description = seriesIntro.select("h4").first().text()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,17 +132,22 @@ class VizShonenJump : ParsedHttpSource() {
|
|||||||
.add("X-Requested-With", "XMLHttpRequest")
|
.add("X-Requested-With", "XMLHttpRequest")
|
||||||
.set("Referer", response.request().url().toString())
|
.set("Referer", response.request().url().toString())
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
val loginCheckRequest = GET(REFRESH_LOGIN_LINKS_URL, newHeaders)
|
val loginCheckRequest = GET(REFRESH_LOGIN_LINKS_URL, newHeaders)
|
||||||
val document = client.newCall(loginCheckRequest).execute().asJsoup()
|
val document = client.newCall(loginCheckRequest).execute().asJsoup()
|
||||||
val isLoggedIn = document.select("div#o_account-links-content").first()!!.attr("logged_in")!!.toBoolean()
|
val isLoggedIn = document.select("div#o_account-links-content").first()!!
|
||||||
|
.attr("logged_in")!!.toBoolean()
|
||||||
|
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
return allChapters.map { oldChapter ->
|
return allChapters.map { oldChapter ->
|
||||||
oldChapter.apply { url = url.substringAfter("'").substringBeforeLast("'") }
|
oldChapter.apply {
|
||||||
|
url = url.substringAfter("'").substringBeforeLast("'")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allChapters.filter { !it.url.startsWith("javascript") }
|
return allChapters.filter { !it.url.startsWith("javascript") }
|
||||||
|
.sortedByDescending { it.chapter_number }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun chapterListSelector() =
|
override fun chapterListSelector() =
|
||||||
@ -151,12 +163,12 @@ class VizShonenJump : ParsedHttpSource() {
|
|||||||
val leftSide = element.select("div:nth-child(1) table").first()!!
|
val leftSide = element.select("div:nth-child(1) table").first()!!
|
||||||
val rightSide = element.select("div:nth-child(2) table").first()!!
|
val rightSide = element.select("div:nth-child(2) table").first()!!
|
||||||
|
|
||||||
name = rightSide.select("td.ch-num-list-spacing").first()!!.text()
|
name = rightSide.select("td").first()!!.text()
|
||||||
date_upload = DATE_FORMATTER.tryParseTime(leftSide.select("td[align=right]").first()!!.text())
|
date_upload = leftSide.select("td[align=right]").first()!!.text().toDate()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
chapter_number = name.substringAfter("Ch. ").toFloatOrNull() ?: 0F
|
||||||
scanlator = "VIZ Media"
|
scanlator = "VIZ Media"
|
||||||
|
|
||||||
url = element.attr("data-target-url")
|
url = element.attr("data-target-url")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -230,18 +242,20 @@ class VizShonenJump : ParsedHttpSource() {
|
|||||||
return GET(newImageUrl, newHeaders)
|
return GET(newImageUrl, newHeaders)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun SimpleDateFormat.tryParseTime(date: String): Long {
|
private fun String.toDate(): Long {
|
||||||
return try {
|
return try {
|
||||||
parse(date).time
|
DATE_FORMATTER.parse(this)!!.time
|
||||||
} catch (e: ParseException) {
|
} catch (e: ParseException) {
|
||||||
0L
|
0L
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/83.0.4103.106 Safari/537.36"
|
private const val USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36"
|
||||||
|
|
||||||
private val DATE_FORMATTER by lazy { SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH) }
|
private val DATE_FORMATTER by lazy {
|
||||||
|
SimpleDateFormat("MMMM d, yyyy", Locale.ENGLISH)
|
||||||
|
}
|
||||||
|
|
||||||
private const val COUNTRY_NOT_SUPPORTED = "Your country is not supported, try using a VPN."
|
private const val COUNTRY_NOT_SUPPORTED = "Your country is not supported, try using a VPN."
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user