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

View File

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

View File

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