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'
|
||||
pkgNameSuffix = 'en.vizshonenjump'
|
||||
extClass = '.VizShonenJump'
|
||||
extVersionCode = 2
|
||||
extVersionCode = 3
|
||||
libVersion = '1.2'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'androidx.exifinterface:exifinterface:1.2.0'
|
||||
implementation 'com.drewnoakes:metadata-extractor:2.14.0'
|
||||
}
|
||||
|
||||
apply from: "$rootDir/common.gradle"
|
||||
|
|
|
@ -4,9 +4,11 @@ import android.graphics.Bitmap
|
|||
import android.graphics.BitmapFactory
|
||||
import android.graphics.Canvas
|
||||
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.ByteArrayOutputStream
|
||||
import java.io.IOException
|
||||
import java.io.InputStream
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType
|
||||
|
@ -33,14 +35,17 @@ class VizImageInterceptor : Interceptor {
|
|||
// See: https://github.com/inorichi/tachiyomi-extensions/issues/2678#issuecomment-645857603
|
||||
val byteOutputStream = ByteArrayOutputStream()
|
||||
image.copyTo(byteOutputStream)
|
||||
|
||||
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 width = input.width
|
||||
val height = input.height
|
||||
val newWidth = width - WIDTH_CUT
|
||||
val newHeight = height - HEIGHT_CUT
|
||||
val newWidth = (width - WIDTH_CUT).coerceAtLeast(imageData.width)
|
||||
val newHeight = (height - HEIGHT_CUT).coerceAtLeast(imageData.height)
|
||||
val blockWidth = newWidth / CELL_WIDTH_COUNT
|
||||
val blockHeight = newHeight / CELL_HEIGHT_COUNT
|
||||
|
||||
|
@ -48,32 +53,47 @@ class VizImageInterceptor : Interceptor {
|
|||
val canvas = Canvas(result)
|
||||
|
||||
// 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.
|
||||
val exifInterface = ExifInterface(byteInputStreamForExif)
|
||||
val uniqueId = exifInterface.getAttribute(ExifInterface.TAG_IMAGE_UNIQUE_ID)!!
|
||||
val key = uniqueId.split(":")
|
||||
.map { it.toInt(16) }
|
||||
// Top border.
|
||||
canvas.drawImage(
|
||||
from = input,
|
||||
srcX = 0, srcY = 0,
|
||||
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.
|
||||
for ((m, y) in key.iterator().withIndex()) {
|
||||
canvas.copyCell(input,
|
||||
Pair((m % (CELL_WIDTH_COUNT - 2) + 1) * (blockWidth + 10), (m / (CELL_WIDTH_COUNT - 2) + 1) * (blockHeight + 10)),
|
||||
Pair((y % (CELL_WIDTH_COUNT - 2) + 1) * blockWidth, (y / (CELL_WIDTH_COUNT - 2) + 1) * blockHeight),
|
||||
blockWidth, blockHeight)
|
||||
for ((m, y) in imageData.key.iterator().withIndex()) {
|
||||
canvas.drawImage(
|
||||
from = input,
|
||||
srcX = (m % INNER_CELL_COUNT + 1) * (blockWidth + 10),
|
||||
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()
|
||||
|
@ -81,19 +101,60 @@ class VizImageInterceptor : Interceptor {
|
|||
return output.toByteArray()
|
||||
}
|
||||
|
||||
private fun Canvas.copyCell(from: Bitmap, src: Pair<Int, Int>, dst: Pair<Int, Int>, width: Int, height: Int) {
|
||||
val srcRect = Rect(src.first, src.second, src.first + width, src.second + height)
|
||||
val dstRect = Rect(dst.first, dst.second, dst.first + width, dst.second + height)
|
||||
private fun Canvas.drawImage(
|
||||
from: Bitmap,
|
||||
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)
|
||||
}
|
||||
|
||||
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 {
|
||||
private const val SIGNATURE = "Signature"
|
||||
private val MEDIA_TYPE = MediaType.parse("image/png")
|
||||
|
||||
private const val CELL_WIDTH_COUNT = 10
|
||||
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 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)
|
||||
}
|
||||
|
||||
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 {
|
||||
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")
|
||||
}
|
||||
|
||||
|
@ -80,16 +82,21 @@ class VizShonenJump : ParsedHttpSource() {
|
|||
|
||||
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 fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
|
||||
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 {
|
||||
val mangasPage = super.searchMangaParse(response)
|
||||
|
@ -114,7 +121,7 @@ class VizShonenJump : ParsedHttpSource() {
|
|||
?.replace("Created by ", "")
|
||||
artist = author
|
||||
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")
|
||||
.set("Referer", response.request().url().toString())
|
||||
.build()
|
||||
|
||||
val loginCheckRequest = GET(REFRESH_LOGIN_LINKS_URL, newHeaders)
|
||||
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) {
|
||||
return allChapters.map { oldChapter ->
|
||||
oldChapter.apply { url = url.substringAfter("'").substringBeforeLast("'") }
|
||||
oldChapter.apply {
|
||||
url = url.substringAfter("'").substringBeforeLast("'")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return allChapters.filter { !it.url.startsWith("javascript") }
|
||||
.sortedByDescending { it.chapter_number }
|
||||
}
|
||||
|
||||
override fun chapterListSelector() =
|
||||
|
@ -151,12 +163,12 @@ class VizShonenJump : ParsedHttpSource() {
|
|||
val leftSide = element.select("div:nth-child(1) table").first()!!
|
||||
val rightSide = element.select("div:nth-child(2) table").first()!!
|
||||
|
||||
name = rightSide.select("td.ch-num-list-spacing").first()!!.text()
|
||||
date_upload = DATE_FORMATTER.tryParseTime(leftSide.select("td[align=right]").first()!!.text())
|
||||
name = rightSide.select("td").first()!!.text()
|
||||
date_upload = leftSide.select("td[align=right]").first()!!.text().toDate()
|
||||
}
|
||||
|
||||
chapter_number = name.substringAfter("Ch. ").toFloatOrNull() ?: 0F
|
||||
scanlator = "VIZ Media"
|
||||
|
||||
url = element.attr("data-target-url")
|
||||
}
|
||||
|
||||
|
@ -230,18 +242,20 @@ class VizShonenJump : ParsedHttpSource() {
|
|||
return GET(newImageUrl, newHeaders)
|
||||
}
|
||||
|
||||
private fun SimpleDateFormat.tryParseTime(date: String): Long {
|
||||
private fun String.toDate(): Long {
|
||||
return try {
|
||||
parse(date).time
|
||||
DATE_FORMATTER.parse(this)!!.time
|
||||
} catch (e: ParseException) {
|
||||
0L
|
||||
}
|
||||
}
|
||||
|
||||
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."
|
||||
|
||||
|
|
Loading…
Reference in New Issue