Fix image decoding method at Viz Shonen Jump. (#4150)

This commit is contained in:
Alessandro Jean 2020-08-17 15:11:00 -03:00 committed by GitHub
parent d239be6c9a
commit b01cf8e0ba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 122 additions and 47 deletions

View File

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

View File

@ -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."
}
}

View File

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