From b01cf8e0ba2b7701b444ddf6c0c57fdeaf8ee8ca Mon Sep 17 00:00:00 2001 From: Alessandro Jean Date: Mon, 17 Aug 2020 15:11:00 -0300 Subject: [PATCH] Fix image decoding method at Viz Shonen Jump. (#4150) --- src/en/vizshonenjump/build.gradle | 4 +- .../en/vizshonenjump/VizImageInterceptor.kt | 121 +++++++++++++----- .../en/vizshonenjump/VizShonenJump.kt | 44 ++++--- 3 files changed, 122 insertions(+), 47 deletions(-) diff --git a/src/en/vizshonenjump/build.gradle b/src/en/vizshonenjump/build.gradle index d208ae50b..cbabb3752 100644 --- a/src/en/vizshonenjump/build.gradle +++ b/src/en/vizshonenjump/build.gradle @@ -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" diff --git a/src/en/vizshonenjump/src/eu/kanade/tachiyomi/extension/en/vizshonenjump/VizImageInterceptor.kt b/src/en/vizshonenjump/src/eu/kanade/tachiyomi/extension/en/vizshonenjump/VizImageInterceptor.kt index 0d473b583..753fd5129 100644 --- a/src/en/vizshonenjump/src/eu/kanade/tachiyomi/extension/en/vizshonenjump/VizImageInterceptor.kt +++ b/src/en/vizshonenjump/src/eu/kanade/tachiyomi/extension/en/vizshonenjump/VizImageInterceptor.kt @@ -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, dst: Pair, 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 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." } } diff --git a/src/en/vizshonenjump/src/eu/kanade/tachiyomi/extension/en/vizshonenjump/VizShonenJump.kt b/src/en/vizshonenjump/src/eu/kanade/tachiyomi/extension/en/vizshonenjump/VizShonenJump.kt index 7242f6a38..696617cb8 100644 --- a/src/en/vizshonenjump/src/eu/kanade/tachiyomi/extension/en/vizshonenjump/VizShonenJump.kt +++ b/src/en/vizshonenjump/src/eu/kanade/tachiyomi/extension/en/vizshonenjump/VizShonenJump.kt @@ -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 { 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."