From 2c00628e87aac8eff7f6ab623c50803235cd72d6 Mon Sep 17 00:00:00 2001 From: sinkableShip <89952969+sinkableShip@users.noreply.github.com> Date: Sun, 26 May 2024 14:50:49 +0700 Subject: [PATCH] Add Pixiv Comic (#3199) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * working, with webview to load page doubt: 1. wrong episode number (using list index instead of real chapter number) 2. should we add the unavailable chapter to show or not (start with ※) 3. webview approach (slow and might get in error, too uncontrollable) 4. differentiating tags (using #) and category might bring problem sinces the added # * check converting from response to ubytearray to image * works fine, keep logs * get rid of logs and another small things * add logo * clean forgotten things * lint check: fix comment * Apply suggestions from code review Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * some refactoring, rename extension name and package to Pixiv Comic * delete unused dependency * use serial name on model * Apply suggestions from code review Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> * prioritizing search over filter, change manga and chapter parse to just store the id, add tag interceptor in the case of tag not found --------- Co-authored-by: AwkwardPeak7 <48650614+AwkwardPeak7@users.noreply.github.com> --- src/ja/pixivcomic/build.gradle | 7 + .../res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3953 bytes .../res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 2170 bytes .../res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4995 bytes .../res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 8861 bytes .../res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 12165 bytes .../extension/ja/pixivcomic/PixivComic.kt | 315 ++++++++++++++++++ .../ja/pixivcomic/PixivComicModel.kt | 99 ++++++ .../extension/ja/pixivcomic/PixivComicUtil.kt | 78 +++++ .../ja/pixivcomic/ShuffledImageInterceptor.kt | 213 ++++++++++++ 10 files changed, 712 insertions(+) create mode 100644 src/ja/pixivcomic/build.gradle create mode 100644 src/ja/pixivcomic/res/mipmap-hdpi/ic_launcher.png create mode 100644 src/ja/pixivcomic/res/mipmap-mdpi/ic_launcher.png create mode 100644 src/ja/pixivcomic/res/mipmap-xhdpi/ic_launcher.png create mode 100644 src/ja/pixivcomic/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 src/ja/pixivcomic/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComic.kt create mode 100644 src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComicModel.kt create mode 100644 src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComicUtil.kt create mode 100644 src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/ShuffledImageInterceptor.kt diff --git a/src/ja/pixivcomic/build.gradle b/src/ja/pixivcomic/build.gradle new file mode 100644 index 000000000..8aee6d6cb --- /dev/null +++ b/src/ja/pixivcomic/build.gradle @@ -0,0 +1,7 @@ +ext { + extName = 'Pixiv Comic' + extClass = '.PixivComic' + extVersionCode = 1 +} + +apply from: "$rootDir/common.gradle" diff --git a/src/ja/pixivcomic/res/mipmap-hdpi/ic_launcher.png b/src/ja/pixivcomic/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..ad14c3ae37145a4547c632d1a5bedbe6635cbfc4 GIT binary patch literal 3953 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 wJ25kuL9io5nVV!Z literal 0 HcmV?d00001 diff --git a/src/ja/pixivcomic/res/mipmap-xhdpi/ic_launcher.png b/src/ja/pixivcomic/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..143f015f28d2ae2e4cd7118d996b05d3b7510bac GIT binary patch literal 4995 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 N002ovPDHLkV1nx3L~;NC literal 0 HcmV?d00001 diff --git a/src/ja/pixivcomic/res/mipmap-xxhdpi/ic_launcher.png b/src/ja/pixivcomic/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..efb62f5222ec2e4f5721b70736076a95759da220 GIT binary patch literal 8861 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 U_t+!;dpiMCpKB=A%3Fp1AD&!x!~g&Q literal 0 HcmV?d00001 diff --git a/src/ja/pixivcomic/res/mipmap-xxxhdpi/ic_launcher.png b/src/ja/pixivcomic/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..b238a83cbc9315e26cd3a752c769caed2466f386 GIT binary patch literal 12165 zcmb7KthH(D4KN9>`2IatI%MqK-siKu_NKG10k^j= zmqu{**B`=T*)g9mecjd}?0^8sip;46Vg+_O!FCshz%UN*a&`;_ygXrbuSXl8!J*>^ zNV<0c4D~c7-{VDsI`~4??+v|;^=}`tT^n1Kq(9-V@IYV+LAIS-vj;- zLHcX#R-c!ARts?c{?lipUHcgdYW`%g2G{*CdhDQul7$gsfKz-^d&c6|rRs}kj8jTQ za(>eNBjIARLtK*+1lj6^WrS~9A0a+9NNY??@o@D6_O?V!KCeEmb5lH^2`C9yS6kpO z@+h`n!BbrHqM;Q_KFhBnhBcHsCgX37wkZp%jr zsMD^bLcM?(bW1|6&NnfHw`PyTZ(n+_WCU1vEEpNDGR1N|>=TXYH4aR`q5UQ*pvWox zH-rJ+Mj+NVHBzf*TKs3!;yVX(8OIh5G*mu=2EsKtcnfV^>2BZe4L7k0(D90assijR z9o~KpyEiLtV6IjL)N07`3Nv%ItpD!@Apqr8{#{% z?)oY-zRtVO?RoW_D=dXZ|S?@}qzeiTwl0RKdK0p6-s zx{t;ENrCRL08y%KkfQfDsie`8V(x_ZG1rA!cASbilGc!h3L($Y*gYRsrN^Z-G5kw3 zz5e?h{Ut<$1gr)GhncDg!6KY6xK)wVd3x{hkdForr+gowcyu&=zpp?Vs`8dsvLqNe zl2(_AtNEn+&qtTtAttUSzH83bS}ctZwa(U{4OIN)#8ASnjG{u#|NS!ZF&lD= zGQC_X{z@ANnYQ~_t*oyy6t2(MwNn?+r(*_U1y>m3eJrS(zFL8Sq5Z{B%tw7phlZqe zx421gT;}fseyp}zhb-b76>I;MeS#Ibb znt_9KrgtG^zYMz+P)Vzb9;2`44lH`RSBzZ0R0z$zXdzKAV+r1Ac#-pB$c_kk>T;1H zEJV_KiY^e3?)sAgfz6|YIR!NMkuq5JJ?4_BnUFfU_57dPA*b=HQ*Mm|HP_u2-jR5t z&mT9bxKCQp^9Ma{^X`ZmcD`Imvp%>xjS_eq)cBZ2bMKN63`xH^{`xUQX>BGf@Yk~^ z@gr8|H4QLTocZfDs9lw<+r&6=gVKgdgzmUWk?d6M1$DkY=tstl4vwqm%qopUL{K(G@oR1xHfI%=6>=eB_^Lv6t zSX!;^Q{an^(#`RgZ?m)?%z4W?`#zQCz6-?Y;P1b7;e0OZXznVpbsUFVP609KQX0GG zpcnX@60UHXRP1^2=W_FRy()1*gHEe)=4;+>U*T*!jn?>c(7Lqv7E=9!+}IXX%)s`&Eg;rSW8A-~vHZI8rlQ%_ z>btrq1*;&1U0@901WE+jDphJb5Ib~mdys!35WwVCw}Cgka}eCxM075VlrqF4gs3AT z)qw7!CzoDi0AVH^G*S&kpc?!QKU4J-up-)VHEBRnYuY$3aA^b;z2TyUm*bl-(Krscg8Gg~#L9Tb3p~9Iq zt`@-~^Fwm`es2VNl>xmikvt6cr_4UildeXjS>rw3ppb0pk>!C(-V`bHV zV6sNmZ`U)GLcFp`bnsWWw9AkTP`_A%bn6&k>a>ZM=^>g^?BNpq+sOq0BF!PJf{8Du{6%Q$4jB0z+D|EWEPg{CvBTeZgFonc`b zmmcIukKB7_PY`5we~ZuIX5(y)U?Zm;R$wKI^PeV43;gnMrz6|HfBdCzLrcFlF#Hr~ zhGd{p=zmY8E6&@|8#C&Re#IEetA#^S~mI(8hx z2fF{|N+i#+QiH;6AvnN!;c7+s`XSi2)iNj=j~HYI+76XqEv5o3C{eu{ZHw3bcrBm; zr~9T+d7q&#x+k{O2Xi0JC6Bv*cv27wfABW733vn+PIPWP zVxU0#VJ&Amq`LnMgo7j46VF7hM!E~Y=ho-eALMM@Gak=;aTFxSfsw0Cf5Zlpf}=TY z-nVS7hW@#9?7=!RFr6jSHfg^EKeqR=j5hlX_hAi?vj_0=ZMD}a>A&KJY+!>qnKB6;6J`tq+0ZC`9f}Lp0$NgUvgKpmgLX3fUPUO z9Wp3p7Q#}dB^Os=U|&bU_)r51*anq|Ej^O}_G&CPz|sXREG5pm}Dn=QON$xQ}Fp4#6BfIt7fS4|UeB zS)*zuDMx-TVy{PS=W#r}&Pv%W|x{7m>|(y4aOMVdxqgi9cwt z`riXclv>bTdw`msd=0!JVXb<1=sd%jc7$&K7*^V~Kp9=$^;<<$bFFu@K?4-LoD_HP zh<$zf<7xh7p#v}imMXc%@4>|kkD5)Zg0wkCDbPA;Ru(6^{z9XLzP8_tD?!-eC9=5u z)$~D$2*bRuI(=L0Re{3{nyxonukCF*{G$awS7U*d7xlW38n2~ez*cZDmzC{V&Bi+Y zpv9CKEA*=$O6o?-{T8GRkXY#4cE=kUCf)~qo*%1FQ)DBp65~`z<)Pm^{&I1zxgC?b zk+9_8`<;Q@EPe6Ef=?z^s(NZCSy@5K%ZjK@4vqiioJ7w$r6GXs-LGScfySR+>=rR? z#5d<#W*h+L)1YHK9U^p)4&I47CPhx}F3IGZidT!v>!8YI6uJ9OzO5`aNXoV>`WCxL zD|ga~?*Ou>nT;BaJuK11ve-4r3turPo-+=WytZ3>zM4r}f&GxvBgQ;`^MizWS%C&~ z8LI1v+ox3+v-$GLS-xfrq2G#Kd1JK~Wr=o_^<2qsen9dO&T~MN(F}hYr|&*UDwF3B zU7_Z54@0#X8W`Rb{|0c=_aMst5|8Oimunz!G_0NNIWIQ<0X2r?*+nq*DT;RBKWy49 zU0g+qDv$?0QHEZ?NAPW`VUGtTz0?|-d#T!5a)#OrB)pTS|AsXA+3!J8`=^T5P~STm z3K$KV0p1^-RG9bUnUK~+wg%!Gfju8hfT6FSn&hGv_YT$u>lpAXQX_FhT~4wco|%7X z)kU3p2q!VmrS~zxql>8kFYn|5(cLoNsjv^`PsA$33g%IU11_%(jq%qXDkNMVDaPbh z?3VI{;GbVFWl%|l)PIo3L;8AxCrMtZSkGxLEC`YR@P18Fyq}A=o1dVfA|?&ykTw)_ zS!X>iqXRV3i4maQN`rsXL^IEChe`219?Gjzka~4OzuAX%p$cBOO&xSmqwzp34MMIk zI8H<)A)1(KLzOsJk-18chSmSkb*QfEG&LSUXm88H%CYBS%Kjv$VQ7;CK{rMC!pnY| z@Wr(ym9K-1ug2|GkIw8v2XdskoA13)J^wz^VfjqhlMhHMbw7`oQo!!lsbF&JpU1S; zmiw->dk`!A)8WCQD=tIV&S5rOWi*eui-o*fJe0FX<{OdEKhvAw{29!B)9{&(K?QIz z>o6RLf?tFBBtTNa$yU`$+EIxCDUxOV84IZ#rDR{U`n*#IR^FFkPb#_Q=PGs8!2Tf| zo1Q2_PI)js=~Eo_-_g&#Nx(ukqiv$%)|R2r2#bEwo~}Koj_%(OSE2`FkBeqr%L!UZ zj^ljPKT*~bv7I>e|H!ZCuRulure7o-1jra#grr-|ejM_*z*mVd+BpQjQT59eA`N=|hvfh6XNQ z#JcXesS)=FUurF_=E!3??C0ondx#gJ^+=N(-df8|jgJ4DK5oK!i);b(kT$T6XdNLH zfJJK!TtR`TjI@;mu0pk-3R<4+J_RjA0LVqTh&8OdQ*l1WZ`numn?)&@5 z8LRFXBnjIxCG`(Dq!nEDf1^J}@aB)~;*Fq{<^!71QLqq1^Tl6g18M}Gv43pQj{G5k?3FoVi zl&sfFeZO3iqG#pr9R@wy=iG^VUj-s6cVpcs=P9e6#;MYKtXtd!nN|BJ8RYe0 zL#u~jOT3N#bAN~?ZfZ8+Jc?IuBc$~A$V6ehKA5x2c=0B9arz8H6F@yMZ6#Ojpml7C z8*S<=93i}!zeIypWBvh%du@EOZ*!m3(9TXEIU7`rE?Km5$^qQwZ|GEjNjD)5q>Z;1 z;j}JtUd_f@(T;FbGz-osIf)BeeCe`4kfx85)LuQw%Q>cIGx*%D^Bglxnd$B zNxdS9)a1DSsq`d5;~0cZpSV;m=cMtMBVXW*ru-~m3wjEY!IlwvEi8-S*O^CB%Jm=H zz0uw|NiUM!kpfB37UN?^Oh>%-&y*VO(Iir)hHV+a;6^b)U@q;d&YJ^bYh2069=s9i zpKC!HXwnAnZ+^b|6Re?4-XwuP2QA4M^FBRYT3bHUABdSrhr_T`uj_OP&{t}Mb*@z#Z9RkQD{|QAn&;LDBIlLnc3Vc$NstX#B~LM z%mtGFLaIHsoi~1Fdf`{->n+VoGYW*?>V|16;OB!-9qfwLfvHQy8vfp z4bIjukhk~Yw!pNDWPjy=rI0 ztiC`@y-XK4lT8Q%UW#+HLB0Rmi6=S39TLpZo&oP~`Mv4;sVc=j0H2B5D5so7=P4Ll z?xG4PNcPKHy%fek;d4*rOFXU>QR~An{P9)dMO@sg4BU(dNSpjvsqdjXiR`}LP#xd; zF0X23TJd+5J`Rb}zE)`75L%#{h9(4ibos9OF>iNAK?N8^r7NA14T0>hq+ zMXoAvbv>UPgk7DZ8A?>{Auj7yd^`L7OBhhl@{{Gf#EoyC{bvP_29OkE(AUbi9>for z#?<_+Vj_lueHnizaEQk}(ma9~w5dJ^)V=B_y^-NA%b6QKH5wK^do{FSoI%zE-;O0S%ZdIqh!EUmQQT(?mhLO zaQd2UNAwVr!^<4k-9DKhQIj4X77K`&VW1^^<*Sr-U9r1Bn2XhOzf_sZJK<9*C6^IlMU|5(1_7%u zp94DmaHSVf<3q=Nl9T6*SDrtaecfM5eBEx(g5S2MwGaGbE{`|sSy8@!J?GNMmuxE-Rj_9}djpAfFieYHQNbWpIu=Q?d4 z)Edee8aDEY8;j_HYCC9b$FDpYY8>~8nIK=U*pmW{En$ceVpTu(bGe#dge7g|) zjg_ZOhs#J_c*9vLYBBw>g3J%%XA|KebIPHv<4R-%n<{nnjD3$E;jA*8QV~s9CcLqm z{L8`f)~6UhK_HyMUFP(Fo8plO2hh$aV=igQg8Xdu1Ojq`ltK)7Ku&>a^59=<6mx(-gTR0Xe+@gJpud1-G--1w^Qy(Lxu^$1LVKw6Y~cd}vncU)^YNkZK14NgSrM_Rvd*APj$t*-?VfjG;Ox8YD@ zKc94~OH1W)zwseouCWUuu#IrxyDS6clFS7ssr}n*^e(Ykzf983>NYDF<@YulwX?Fy>YW%^%%zFvj5u#}mX2?e1<4KCrcVM&C=ou`xd|b=C_dxPF&XPZh%sF1Ws+ zINvP_q=2`sJS4ihJ zxk&6|-*adM;g^kz#001iGe4T=74G!Ai9FnC?{+w_${RH^KDxbo23go-*=$ouQF=`H zMGjOAu?PKvmT-8J;0@5Eh25)@$51gATX}2^7nbhOSYm~eMvgGU6Ptton5}TaA0+KE zu_~C!9H_rmyni{(U+}9*xlzTgzd|29(pD6E8`gS?3v!|=%M27r2Et-8IFU_|r(joN z3NL*H&(TZl*;B<47*h|7cY6&~i{RVc8205|mhGNI9`yUkKBF23&s_Zd@JqfN6weDfz2pTiq$=_n@5fK934&y^94LwX<9v`<3m5i$IXM1*$O-3I)N`!=0 zDiAiNKH0Pe1tiGD=l|Ripxz5J_38#w_k>G(rvDn|70IfSCbW|iX`$H~B@R^UzHv0Y zH2M^q4Tzv=%Ea$bfYt~ELp@l%e<;1QmU&$tXRcD@Sh9igsujiTw@RWNZJF^BsP>S} zmCJYI-vmYSNiQOA~(Fn?@~mfl&Rmk&r95Q%0;=Bzu^re0cm=1f%cX%c`{# zzy1-gnU3U|ST<6dxf}H2<=P@KVF|7yo?hj38ekQU&j@mosj1L!fuUbj>LVXnKwn6a zo5b@r_tQoz7Bo*0e|8aNmOI_}=X;uGt0Z5v&)O3+rSWQa;C^#wq@g$10TyzD>@;wO!xZ0EblcXA!A@pZpyRCjmpyrUx?gdS*9Le5>+C z$9Aa}*+&pNrxeDTJvJK6Rj^d)H9js#>a~c1^3|pjcr@F!7*M!Byv}12i@#Y!s?@b; za4=w6qF@;!Vb!|xtw9CS!0NU9zSLTFeqR8;_y+~fyEsvT@F&!s33D2X#GEF#?0xrS zei&Krk%p!$Yx!P`Sz=yf6PR2Y875@64cr#KX}zc1Nj4IzNTP!SmcqROX#wV;V}R|O zyUYIl82AfAMCN96(|PBedPupP&*6Q$A7&T6OLaazl7d<5aL}EFC7voxq10}lk%iM<|Bv|UW_+&1MpuS-?NRTsHQ2X(!DH~zXYCAn zN}uRT{}_b+l3g!h#@D32mSQX^)5CEo2X%Ju6cg8 z;)F?D(wOUU z6J_W9E~aE5_ZGZ*%}#9WDw^SR&U{bsVu%uI& zt#Mp``YQntg`?Xd_=5&myj}~uwXLX_t%2IUH&?eypwrDfxpusMIRNf>RB&(gNat;D z=qpR~5=yYML=rSD&h<`&mTD$*?5wR2Y8n+tc*StW?8k3#Q|AK`_K8mjP{4d^d zj6&!g*qTLF&A zKN9P&0YDcj8HG=G_;;)lg8!^os{d?>~ftza$5k$aC;I8_}GHm@OxYnrD6#!)| z+k0i2KL84$mUekS6WTQI3{Z{=HK8=#_=2YUQom-r{fLX8ZsdVDy-&tWH9AFim|djW zLyD;AojD-rvr5zF$_OLw7*@hl6>+QXy{qiv6TOJ!Ab=YbxI(l^fkxGmJ=CqQBK}N{ zK%?4}Uq3+3FL2Vo#gz%8i~w0YbFk&5`V~dou6reGO#KDj@w2t#JdV(nN3E~hSrKw= zY<4^mUY36^NuNpEfb^br8A(R`U`}eYeMI4{n2EmQGTRkfj?)omIWA(E=uDxQM&iu z{UGvnnhQP^#5?Ad$L9T>@O1^rIPc@`@FXxjAcQXokX#*xU0j}SH3=ilr|$ymFW*sO zhI3qVost37N4noeW8Z(V=`u^~qs%$NQ`yT{H6i_%wq!RcL@M`m+x}BWdHz}-D~)D6 zAkRPl0GX>mf3IQKix4qAqTXPArpJ&e zEBiR$t^YSxWbRDnt0(;-s7i$er{MBF&(-d^UMfXWJ9|b1Cp4QK`3fX{$EMR|%-Iy? z7nLAkCYCBzLzE4%Yb2xYKF{oAZ){)xFs5wBNj~@`(z2*V(T8|fIIPg%S@0HYRtDtC^5^SI9AR~8^p4#avREvfq>J}P6B<)X zOy+uvw~`keaoo)eu0FgzXAyZbLg8OqW?V?#^OT-grEV3!Jh|Ea^R!&`JFaI(EOA0@ zax}m71{kSz-t{z%OmAl*5)3wsPRCKZgyR5>awV`Kit8gmXq z*km*NH{par@ee8tzEx-gj&4w}S!Qm{U*;BSbj|b~q7-;s1h_2CF*-eTI?<4^^IUm| z4Uu5^$}Yk+U8Qq>Xsf<4GeorLUkrmyM*N$dzw?BjrS_ryt z7w*}+x)UiqRRX9#S0mp#KD3L5MBsLbJca%Rx}*t*1| z$7&p>BF;P`hw_=r&ckpbl41++YN03rPXe`)@Wt+Pw_pn(g>^wC)P&{P0Zrn}0OOxC zn}}nlYl75lKMQG9pK{LeXR3+%@5jczu_MlU5xpu%pYLY)n#3^^6zJ6PIWKOC=wHKw zzD;M8%WO0Qyar$)4`u1kw3c{r|IBG$D!3&nI&9#HIE(ci8-TU`Rt zlVc}B{rQy>f;EpFEr#|~M+m*7MK{H9QZ(6KFI_K!Uzobv}0%>`cVXc&?N5=mR{{Rg4DdV?D^U7VgZ zt`Oc0iB4|ii4-0B40fE3R0HRS2+0-tvu|zPFPXnc5Ms~a%kwf zIEpOo$Pkzny|jDkSNj~{qZSf}`0eX z-Q>TZ`zG^$ASaVk6j4{%br|u~I-S(3U~u=ydt==oH2t|X4HWH9Wal11p}7t#5zqJy zwX3j>fvO9Mupt=0y%i*a+2|n=U2XEYU*nueCO1S$IRj;gv#UncPx>JFLa%p}UkC2| zR}hd+TGAx?P9t>qyN zFNfB<5cC2Wj6!tMWuG7!5Y-6YXKjRHKVNd%YdW{JoW0*Z(v*nRi|oDKJ$PpYpa1RA z;Kg5kN-W;(#9j#zH0OB0m;=DTI#KZJrBd6X(2`-dX=AV|IUjRG3$C0}u)C^=lvCE) z*EqWMgf#|-m(6-2cUjym&{Oyy1wgYpzy`MeCmd9u)VlI06$*!4wM_iQVnQ4NH6qL# z+-Zi3&n##(a5uEZhrvaEWkboLmGC+d{aP}lcBUb+FKoxbqnfOS;mn=#-efh0Q9v^_ z`h!r&_!1SUYd|R^_OB@zqoXeejp8PUaw>q zshfxMy{|QOlHZr)1%LKL*E%e|csoUf-MBh0qStL>6zx)K;DJgADnCVTQy_71Z4eu! z;5T8}PmuIIsZXa9_{g-C8Y(osytF-@izqFlP%Os)vj6pU&Ag z)$cBgr4%R+5CwHf2-FuDw>y)TXotsUv0*mhBT~$b&F5RfJGUmBCZtA4{~hO$Rs)*h z+Yhk$;6AMshp*u6E%I;RHv{0UwC+eS5gF#pDB3m!M*wsmj;twb)|j5B{u=X>26GZ@3q4>kF+ z@7EMPirY;SRrjN!C9A*CqFMRaRKLFX&?v)4?ZlpcPsP51$tdQ}(1*KeNv|~G`B&#g zp_K2Rc_Sf}U%D>H<|^MY`9ikdgZx@-QsT@%Q{z7~pYW*?FoOPB|8QLdP=?(B0Ux6K z_^32+t&2F})sM5km)RfzknvGFf!L6Ji(&Pbn!ACI8z#M8^5jf%_$(f`fk4D08g*0sfQJ2-cfp8*HDt+7-R@xdgA znwZt3F2+n}MV>czifIIM9gb8d(GKLi2E6*^;v#kT7Sa*oLmYZYKlmfG0P(i0Q19L* zSTQm-9YUK@ks^7ML{mCrsk-GD2JPgXZ3$yNnwDf?thbDXM{V&3bgu6&w~f3gw9Swp zf4$GTN>P;@h?*K-)Jrz&pxhDQp@fvYKiP^K`MKfRs}AhoplnkB>GkODLuam<*Pd17 zT2Bj=tJGr)cos>p)*%{9+%wtomJ-&6aXH;1tG{&@`&Y@30?TZK8KP@B2UpxC!q?D$ z-ftht#62v;lQQoij2JKXxAwO^808|jNK@n_;mdG+Kd{SQd zvrlJ{Pb=iXLMwciP1+i-1h9MQDPJhcyojf72B*>&8n1*SC_~$E(cj#HKLU~J06CNr z^plPD>8GrvCeFx9srQAgd4h~3Rc#BnQ9VTO3;mDRuJj->I|>nzyIvZ6s(zTIp>^iu zJA;-JNEPt3_#DiS6d{`&jGyM@d~$^viZ6<~9YZ%9x-l!v$!B|JQkI0E1i3vq%w0pg zj5e2U1Ab3Bx7VAHA<^|5$Zdi>G9=Ff8qE$Rv3n1h!>%9dZ8v_K;Fa5&Pq<=#pes4X zrP2im?gXL^wtT+}@XfXmT72s{8DG&CxOor062pBL7|o}PdbIwB0(nFU8YM16nAPBu z!I~0^PBG@vk1ua?7_~FJ(H~n|VqZy_z4XEoZGorvUUM2(^Tr$xCbsq9b*p%5yX!HCsV)#`C6|G0Ao=@yY+^E|(nk~I_ zgQxjOakM3MJ4u{%0(OI{GXdjq@2|qGRZjfMFC&TO>bvB%d=GWxSz*br3 zlY&jbI=OgF%mnwyOC>C(s-2lriMgGrcLg^6Lzd*b=4NLSvWnsbbV> z$JI?M%uBGP&;L*x(*NtmjEO)NGTj6oyho9Zgu)$S?fzo7*>N@anwevm6Y4=a$BJ?vRF4bl{By9btD$-W`RX%AupDdDlhUYa0aVQZ{!O(VRfl%#6C zUJ0XS>7icrSHUaApE+XkfH5-!EOf)3#+gau&88S=em?`~oaL#;Sq*F%AYbcCcIFk<_DecvnC0O0z4pa!x-9EfeMK@8Gty;aER_o#H|-%O;g#vy^^hw!~O#2Mf^R zOP4M0)7{z0ktrFe*+^|O+Vzfw$bzHgHB;uX=Iy&lj~(^IWewu|{6 DKPYtz literal 0 HcmV?d00001 diff --git a/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComic.kt b/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComic.kt new file mode 100644 index 000000000..deeb2f7a4 --- /dev/null +++ b/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComic.kt @@ -0,0 +1,315 @@ +package eu.kanade.tachiyomi.extension.ja.pixivcomic + +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.source.model.Filter +import eu.kanade.tachiyomi.source.model.FilterList +import eu.kanade.tachiyomi.source.model.MangasPage +import eu.kanade.tachiyomi.source.model.Page +import eu.kanade.tachiyomi.source.model.SChapter +import eu.kanade.tachiyomi.source.model.SManga +import eu.kanade.tachiyomi.source.online.HttpSource +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import okhttp3.HttpUrl +import okhttp3.HttpUrl.Companion.toHttpUrl +import okhttp3.Request +import okhttp3.Response +import org.jsoup.Jsoup +import uy.kohesive.injekt.injectLazy + +class PixivComic : HttpSource() { + override val lang: String = "ja" + override val supportsLatest = true + override val name = "Pixivコミック" + override val baseUrl = "https://comic.pixiv.net" + + private val json: Json by injectLazy() + + // since there's no page option for popular manga, we use this as storage storing manga id + private val alreadyLoadedPopularMangaIds = mutableSetOf() + + // used to determine if popular manga has next page or not + private var popularMangaCountRequested = 0 + + /** + * the key can be any kind of string with minimum length of 1, + * the same key must be passed in [imageRequest] and [ShuffledImageInterceptor] + */ + private val key by lazy { + randomString() + } + + private val timeAndHash by lazy { + getTimeAndHash() + } + + override val client = network.cloudflareClient.newBuilder() + .addInterceptor(ShuffledImageInterceptor(key)) + .addNetworkInterceptor(::tagInterceptor) + .build() + + override fun headersBuilder() = super.headersBuilder() + .add("Referer", "$baseUrl/") + .add("X-Requested-With", "pixivcomic") + + override fun popularMangaRequest(page: Int): Request { + if (page == 1) alreadyLoadedPopularMangaIds.clear() + popularMangaCountRequested = POPULAR_MANGA_COUNT_PER_PAGE * page + + val url = apiBuilder() + .addPathSegments("rankings/popularity") + .addQueryParameter("label", "総合") + .addQueryParameter("count", popularMangaCountRequested.toString()) + .build() + + return GET(url, headers) + } + + override fun popularMangaParse(response: Response): MangasPage { + val popular = json.decodeFromString>(response.body.string()) + + val mangas = popular.data.ranking.filterNot { + alreadyLoadedPopularMangaIds.contains(it.id) + }.map { manga -> + SManga.create().apply { + title = manga.title + thumbnail_url = manga.mainImageUrl + url = manga.id.toString() + + alreadyLoadedPopularMangaIds.add(manga.id) + } + } + + return MangasPage(mangas, popular.data.ranking.size == popularMangaCountRequested) + } + + override fun latestUpdatesRequest(page: Int): Request { + val url = apiBuilder() + .addPathSegments("works/recent_updates") + .addQueryParameter("page", page.toString()) + .build() + + return GET(url, headers) + } + + override fun latestUpdatesParse(response: Response): MangasPage { + val latest = json.decodeFromString>(response.body.string()) + + val mangas = latest.data.officialWorks.map { manga -> + SManga.create().apply { + title = manga.name + thumbnail_url = manga.image.main + url = manga.id.toString() + } + } + + return MangasPage(mangas, latest.data.nextPageNumber != null) + } + + override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request { + val apiBuilder = apiBuilder() + + when { + // for searching with tags, all tags started with # + query.startsWith("#") -> { + val tag = query.removePrefix("#") + apiBuilder + .addPathSegment("tags") + .addPathSegment(tag) + .addPathSegments("works/v2") + .addQueryParameter("page", page.toString()) + } + query.isNotBlank() -> { + apiBuilder + .addPathSegments("works/search/v2") + .addPathSegment(query) + .addQueryParameter("page", page.toString()) + } + else -> { + var tagIsBlank = true + filters.forEach { filter -> + when (filter) { + is Tag -> { + if (filter.state.isNotBlank()) { + apiBuilder + .addPathSegment("tags") + .addPathSegment(filter.state.removePrefix("#")) + .addPathSegments("works/v2") + .addQueryParameter("page", page.toString()) + .build() + + tagIsBlank = false + } + } + is Category -> { + if (tagIsBlank) { + apiBuilder + .addPathSegment("categories") + .addPathSegment(filter.values[filter.state]) + .addPathSegments("works") + .addQueryParameter("page", page.toString()) + .build() + } + } + else -> {} + } + } + } + } + + return GET(apiBuilder.build(), headers) + } + + override fun searchMangaParse(response: Response) = latestUpdatesParse(response) + + override fun getFilterList() = FilterList(TagHeader(), Tag(), CategoryHeader(), Category()) + + private class TagHeader : Filter.Header(TAG_HEADER_TEXT) + + private class Tag : Filter.Text("Tag") + + private class CategoryHeader : Filter.Header(CATEGORY_HEADER_TEXT) + + private class Category : Filter.Select("Category", categories) + + override fun mangaDetailsRequest(manga: SManga): Request { + val url = apiBuilder() + .addPathSegments("works/v5") + .addPathSegment(manga.url) + .build() + + return GET(url, headers) + } + + override fun mangaDetailsParse(response: Response): SManga { + val manga = json.decodeFromString>(response.body.string()) + val mangaInfo = manga.data.officialWork + + return SManga.create().apply { + description = Jsoup.parse(mangaInfo.description).wholeText() + author = mangaInfo.author + + val categories = mangaInfo.categories?.map { it.name } ?: listOf() + val tags = mangaInfo.tags?.map { "#${it.name}" } ?: listOf() + + val genreString = categories.plus(tags).joinToString(", ") + genre = genreString + } + } + + override fun getMangaUrl(manga: SManga): String { + val url = baseUrl.toHttpUrl().newBuilder() + .addPathSegment("works") + .addPathSegment(manga.url) + .build() + + return url.toString() + } + + override fun chapterListRequest(manga: SManga): Request { + val url = apiBuilder() + .addPathSegment("works") + .addPathSegment(manga.url) + .addPathSegments("episodes/v2") + .addQueryParameter("order", "desc") + .build() + + return GET(url, headers) + } + + override fun chapterListParse(response: Response): List { + val chapters = json.decodeFromString>(response.body.string()) + + return chapters.data.episodes.filter { episodeInfo -> + episodeInfo.episode != null + }.mapIndexed { i, episodeInfo -> + SChapter.create().apply { + val episode = episodeInfo.episode!! + + name = episode.numberingTitle.plus(": ${episode.subTitle}") + url = episode.id.toString() + date_upload = episode.readStartAt + chapter_number = i.toFloat() + } + } + } + + override fun getChapterUrl(chapter: SChapter): String { + val url = baseUrl.toHttpUrl().newBuilder() + .addPathSegments("viewer/stories") + .addPathSegment(chapter.url) + .build() + + return url.toString() + } + + override fun pageListRequest(chapter: SChapter): Request { + val url = apiBuilder() + .addPathSegment("episodes") + .addPathSegment(chapter.url) + .addPathSegment("read_v4") + .build() + + val header = headers.newBuilder() + .add("X-Client-Time", timeAndHash.first) + .add("X-Client-Hash", timeAndHash.second) + .build() + + return GET(url, header) + } + + override fun pageListParse(response: Response): List { + val shuffledPages = json.decodeFromString>(response.body.string()) + + return shuffledPages.data.readingEpisode.pages.mapIndexed { i, page -> + Page(i, imageUrl = page.url) + } + } + + override fun imageUrlParse(response: Response): String { + throw UnsupportedOperationException() + } + + override fun imageRequest(page: Page): Request { + val header = headers.newBuilder() + .add("X-Cobalt-Thumber-Parameter-GridShuffle-Key", key) + .build() + + return GET(page.imageUrl!!, header) + } + + private fun apiBuilder(): HttpUrl.Builder { + return baseUrl.toHttpUrl() + .newBuilder() + .addPathSegments("api/app") + } + + companion object { + private const val POPULAR_MANGA_COUNT_PER_PAGE = 30 + private const val TAG_HEADER_TEXT = "Can only filter 1 type (Category or Tag) at a time" + private const val CATEGORY_HEADER_TEXT = "This filter by Category is ignored if Tag isn't at blank" + private val categories = arrayOf( + "恋愛", + "動物", + "グルメ", + "ファンタジー", + "ホラー・ミステリー", + "アクション", + "エッセイ", + "ギャグ・コメディ", + "日常", + "ヒューマンドラマ", + "スポーツ", + "お仕事", + "BL", + "TL", + "百合", + "pixivコミック限定", + "映像化", + "コミカライズ", + "タテヨミ", + "読み切り", + "その他", + ) + } +} diff --git a/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComicModel.kt b/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComicModel.kt new file mode 100644 index 000000000..525ae578e --- /dev/null +++ b/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComicModel.kt @@ -0,0 +1,99 @@ +package eu.kanade.tachiyomi.extension.ja.pixivcomic + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +internal class ApiResponse( + val data: T, +) + +@Serializable +internal class Popular( + val ranking: List, +) { + @Serializable + internal class RankingItem( + val id: Int, + val title: String, + @SerialName("main_image_url") + val mainImageUrl: String, + ) +} + +@Serializable +internal class Latest( + @SerialName("next_page_number") + val nextPageNumber: Int?, + @SerialName("official_works") + val officialWorks: List, +) + +@Serializable +internal class Manga( + @SerialName("official_work") + val officialWork: OfficialWork, +) + +@Serializable +internal class OfficialWork( + val id: Int, + val name: String, + val image: Image, + val author: String, + val description: String, + val categories: List?, + val tags: List?, +) { + @Serializable + internal class Category( + val name: String, + ) + + @Serializable + internal class Tag( + val name: String, + ) + + @Serializable + internal class Image( + val main: String, + ) +} + +@Serializable +internal class Chapters( + val episodes: List, +) { + @Serializable + internal class EpisodeInfo( + val episode: Episode?, + ) + + @Serializable + internal class Episode( + val id: Int, + @SerialName("numbering_title") + val numberingTitle: String, + @SerialName("sub_title") + val subTitle: String, + @SerialName("read_start_at") + val readStartAt: Long, + ) +} + +@Serializable +internal class Pages( + @SerialName("reading_episode") + val readingEpisode: ReadingEpisode, +) { + @Serializable + internal class ReadingEpisode( + val pages: List, + ) { + @Serializable + internal class SinglePage( + val url: String, + ) + } +} diff --git a/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComicUtil.kt b/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComicUtil.kt new file mode 100644 index 000000000..4c6c9d05f --- /dev/null +++ b/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/PixivComicUtil.kt @@ -0,0 +1,78 @@ +package eu.kanade.tachiyomi.extension.ja.pixivcomic + +import android.os.Build +import okhttp3.Interceptor +import okhttp3.Response +import java.security.MessageDigest +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone +import kotlin.math.abs + +private const val TIME_SALT = "mAtW1X8SzGS880fsjEXlM73QpS1i4kUMBhyhdaYySk8nWz533nrEunaSplg63fzT" + +private class NoSuchTagException(message: String) : Exception(message) + +internal fun tagInterceptor(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + if (request.url.pathSegments.contains("tags") && response.code == 404) { + throw NoSuchTagException("The inputted tag doesn't exist") + } + return response +} + +internal fun randomString(): String { + // the average length of key + val length = (30..40).random() + + return buildString(length) { + val charPool = ('a'..'z') + ('A'..'Z') + (0..9) + + for (i in 0 until length) { + append(charPool.random()) + } + } +} + +@OptIn(ExperimentalUnsignedTypes::class) +internal fun getTimeAndHash(): Pair { + val timeFormatted = if (Build.VERSION.SDK_INT < 24) { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).format(Date()) + .plus(getCurrentTimeZoneOffsetString()) + } else { + SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssXXX", Locale.ENGLISH).format(Date()) + } + + val saltedTimeArray = timeFormatted.plus(TIME_SALT).toByteArray() + val saltedTimeHash = MessageDigest.getInstance("SHA-256") + .digest(saltedTimeArray).toUByteArray() + val hexadecimalTimeHash = saltedTimeHash.joinToString("") { + var hex = Integer.toHexString(it.toInt()) + if (hex.length < 2) { + hex = "0$hex" + } + return@joinToString hex + } + + return Pair(timeFormatted, hexadecimalTimeHash) +} + +/** + * workaround to retrieve time zone offset for android with version lower than 24 + */ +private fun getCurrentTimeZoneOffsetString(): String { + val timeZone = TimeZone.getDefault() + val offsetInMillis = timeZone.rawOffset + + val hours = offsetInMillis / (1000 * 60 * 60) + val minutes = (offsetInMillis % (1000 * 60 * 60)) / (1000 * 60) + + val sign = if (hours >= 0) "+" else "-" + val formattedHours = String.format("%02d", abs(hours)) + val formattedMinutes = String.format("%02d", abs(minutes)) + + return "$sign$formattedHours:$formattedMinutes" +} diff --git a/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/ShuffledImageInterceptor.kt b/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/ShuffledImageInterceptor.kt new file mode 100644 index 000000000..199c0c7db --- /dev/null +++ b/src/ja/pixivcomic/src/eu/kanade/tachiyomi/extension/ja/pixivcomic/ShuffledImageInterceptor.kt @@ -0,0 +1,213 @@ +package eu.kanade.tachiyomi.extension.ja.pixivcomic + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.graphics.Canvas +import android.graphics.Paint +import okhttp3.Interceptor +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.Response +import okhttp3.ResponseBody +import okhttp3.ResponseBody.Companion.asResponseBody +import okio.Buffer +import java.io.InputStream +import java.security.MessageDigest +import kotlin.math.ceil +import kotlin.math.floor + +private const val SHUFFLE_SALT = "4wXCKprMMoxnyJ3PocJFs4CYbfnbazNe" +private const val BYTES_PER_PIXEL = 4 +private const val GRID_SIZE = 32 + +internal class ShuffledImageInterceptor(private val key: String) : Interceptor { + override fun intercept(chain: Interceptor.Chain): Response { + val request = chain.request() + val response = chain.proceed(request) + + if (request.headers["X-Cobalt-Thumber-Parameter-GridShuffle-Key"] == null) { + return response + } + + val imageBody = response.body.byteStream() + .toDeShuffledImage(key) + + return response.newBuilder() + .body(imageBody) + .build() + } + + @OptIn(ExperimentalUnsignedTypes::class) + private fun InputStream.toDeShuffledImage(key: String): ResponseBody { + // get the image color data + val shuffledImageBitmap = BitmapFactory.decodeStream(this) + + val width = shuffledImageBitmap.width + val height = shuffledImageBitmap.height + + val shuffledImageArray = UByteArray(width * height * BYTES_PER_PIXEL) + + var index = 0 + for (y in 0 until height) { + for (x in 0 until width) { + val pixel = shuffledImageBitmap.getPixel(x, y) + + val alpha = pixel shr 24 and 0xff + val red = pixel shr 16 and 0xff + val green = pixel shr 8 and 0xff + val blue = pixel and 0xff + + shuffledImageArray[index++] = alpha.toUByte() + shuffledImageArray[index++] = red.toUByte() + shuffledImageArray[index++] = green.toUByte() + shuffledImageArray[index++] = blue.toUByte() + } + } + + // deShuffle the shuffled image + val deShuffledImageArray = deShuffleImage(shuffledImageArray, width, height, key) + + // place it back together + val deShuffledImageBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(deShuffledImageBitmap) + + index = 0 + for (y in 0 until height) { + for (x in 0 until width) { + // it's rgba + val red = deShuffledImageArray[index++] + val green = deShuffledImageArray[index++] + val blue = deShuffledImageArray[index++] + val alpha = deShuffledImageArray[index++] + + canvas.drawPoint( + x.toFloat(), + y.toFloat(), + Paint().apply { + setARGB(red.toInt(), green.toInt(), blue.toInt(), alpha.toInt()) + }, + ) + } + } + + return Buffer().run { + deShuffledImageBitmap.compress(Bitmap.CompressFormat.PNG, 100, outputStream()) + asResponseBody("image/png".toMediaType()) + } + } + + @OptIn(ExperimentalUnsignedTypes::class) + private fun deShuffleImage( + shuffledImageArray: UByteArray, + width: Int, + height: Int, + key: String, + ): UByteArray { + val verticalGridTotal = ceil(height.toFloat() / GRID_SIZE).toInt() + val horizontalGridTotal = floor(width.toFloat() / GRID_SIZE).toInt() + val grid2DArray = Array(verticalGridTotal) { Array(horizontalGridTotal) { it } } + + val saltedKeyArray = SHUFFLE_SALT.plus(key).toByteArray() + val saltedKeyHash = MessageDigest.getInstance("SHA-256").digest(saltedKeyArray).toUByteArray() + val saltedKeyHashArray = saltedKeyHash.first16ByteBecome4UInt() + val hash = HashAlgorithm(saltedKeyHashArray) + + for (i in 0 until 100) hash.next() + + for (i in 0 until verticalGridTotal) { + val gridArray = grid2DArray[i] + + for (j in (horizontalGridTotal - 1) downTo 1) { + val hashIndex = hash.next() % (j + 1).toUInt() + val grid = gridArray[j] + gridArray[j] = gridArray[hashIndex.toInt()] + gridArray[hashIndex.toInt()] = grid + } + } + + for (i in 0 until verticalGridTotal) { + val gridArray = grid2DArray[i] + val indexOfIndexGridArray = gridArray.mapIndexed { index, _ -> + gridArray.indexOf(index) + } + grid2DArray[i] = indexOfIndexGridArray.toTypedArray() + } + + val deShuffledImageArray = UByteArray(shuffledImageArray.size) + for (row in 0 until height) { + val verticalGridIndex = floor(row.toFloat() / GRID_SIZE).toInt() + val gridArray = grid2DArray[verticalGridIndex] + + for (horizontalGridIndex in 0 until horizontalGridTotal) { + // places square grid to the places it supposed to be + val gridFrom = gridArray[horizontalGridIndex] + + val gridToIndex = horizontalGridIndex * GRID_SIZE + val toIndex = (row * width + gridToIndex) * BYTES_PER_PIXEL + val gridFromIndex = gridFrom * GRID_SIZE + val fromIndex = (row * width + gridFromIndex) * BYTES_PER_PIXEL + + val gridSizeBytes = GRID_SIZE * BYTES_PER_PIXEL + for (i in 0 until gridSizeBytes) { + deShuffledImageArray[toIndex + i] = shuffledImageArray[fromIndex + i] + } + + // copy the small part of image that don't get shuffled (most right side of the image) + val horizontalIndex = horizontalGridTotal * GRID_SIZE + val startIndex = (row * width + horizontalIndex) * BYTES_PER_PIXEL + val lastIndex = (row * width + width) * BYTES_PER_PIXEL + + for (i in startIndex until lastIndex) { + deShuffledImageArray[i] = shuffledImageArray[i] + } + } + } + return deShuffledImageArray + } + + @OptIn(ExperimentalUnsignedTypes::class) + private fun UByteArray.first16ByteBecome4UInt(): UIntArray { + val binaries = this.copyOfRange(0, 16).map { + var binaryString = Integer.toBinaryString(it.toInt()) + for (i in binaryString.length until 8) { + binaryString = "0$binaryString" + } + return@map binaryString + } + + return UIntArray(4) { i -> + val binariesIndexStart = i * 4 + val stringBuilder = StringBuilder() + for (index in binariesIndexStart + 3 downTo binariesIndexStart) { + stringBuilder.append(binaries[index]) + } + Integer.parseUnsignedInt(stringBuilder.toString(), 2).toUInt() + } + } + + @OptIn(ExperimentalUnsignedTypes::class) + private class HashAlgorithm(val hashArray: UIntArray) { + init { + if (hashArray.all { it == 0u }) { + hashArray[0] = 1u + } + } + + fun next(): UInt { + val e = 9u * shiftOr((5u * hashArray[1]), 7) + val t = hashArray[1] shl 9 + + hashArray[2] = hashArray[2] xor hashArray[0] + hashArray[3] = hashArray[3] xor hashArray[1] + hashArray[1] = hashArray[1] xor hashArray[2] + hashArray[0] = hashArray[0] xor hashArray[3] + hashArray[2] = hashArray[2] xor t + hashArray[3] = shiftOr(this.hashArray[3], 11) + + return e + } + + private fun shiftOr(value: UInt, by: Int): UInt { + return (((value shl (by % 32))) or (value.toInt() ushr (32 - by)).toUInt()) + } + } +}