From 474009c1a45982261f7e440fd0997a753fbdd998 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 29 Sep 2012 01:06:21 +0530 Subject: [PATCH 01/34] ... --- src/calibre/devices/mtp/driver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 57bc8f6c6c..2fe0843484 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -158,6 +158,7 @@ class MTP_DEVICE(BASE): def books(self, oncard=None, end_session=True): from calibre.devices.mtp.books import JSONCodec from calibre.devices.mtp.books import BookList, Book + self.report_progress(0, _('Listing files, this can take a while')) self.get_driveinfo() # Ensure driveinfo is loaded sid = {'carda':self._carda_id, 'cardb':self._cardb_id}.get(oncard, self._main_id) @@ -172,7 +173,7 @@ class MTP_DEVICE(BASE): steps = len(all_books) + 2 count = 0 - self.report_progress(0, _('Reading metadata from device')) + self.report_progress(0, _('Reading ebook metadata')) # Read the cache if it exists storage = self.filesystem_cache.storage(sid) cache = storage.find_path((self.METADATA_CACHE,)) From 3733d739f7001f9752c41d137652e85daad1a650 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 29 Sep 2012 09:13:19 +0530 Subject: [PATCH 02/34] Fix #1058342 (Icon for recipe fokke en sukke (NL)) --- recipes/icons/automatiseringgids.png | Bin 0 -> 32005 bytes recipes/icons/fokkeensukke.png | Bin 0 -> 56544 bytes recipes/icons/tweakers_net.png | Bin 0 -> 3828 bytes recipes/icons/vrijnederland.png | Bin 0 -> 3772 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 recipes/icons/automatiseringgids.png create mode 100644 recipes/icons/fokkeensukke.png create mode 100644 recipes/icons/tweakers_net.png create mode 100644 recipes/icons/vrijnederland.png diff --git a/recipes/icons/automatiseringgids.png b/recipes/icons/automatiseringgids.png new file mode 100644 index 0000000000000000000000000000000000000000..c042faa8d5280fc93327e2185ba8972e9f2c6280 GIT binary patch literal 32005 zcmeIb2V7In(lC5N?_ESd2wgg%_aZeEK{`qik^rFvLZ~WOQA9yOQ9(fw1O!D;q^YPV zMXXc>>>yI4D81#~6F~7_?!Djh{l4$L_dXsyo7vgfIkU6%?3^9QugLEq4ofq%8H7L} zAPevhB7fD=xAP~E2;u&OP^5~25~R5qZ9_u?!56(C6$F9*%Qy)Nj^oR?`*+3rd>3!-V0Ac0@ubj!25J2F&C^1_dk$D+(U7%>5T}02YhKko1`t zbpvrR`hdj>vrr;Fgrs2S?T0fX5QDt|Xk$Sj;$sORB<~=5T)3_xOb@cHWdG~@Bs?Y% zM^eBLao!{XaWxO4Za4|B_gnM00D{jlt*+uq8WZC`WQix?f{lno@0g!Vz2PVj31nOq z>Th`b^J2kFfY3qu8p^tg6aeP?4U*`T#sE5BO6s+-8wV{61;J-^%IAFYkKvVwj zsfIrk=toBY`v14<2hL!W!-ER=uNsg3qEJ67;oovJS{3*YE$2UfMEaY8{iuz9lP(b+ z;p4bG&xU$~dEW~k;!E&aolXC0mbSC8voo=SX^jb3+!pU(-0JYPX6F2VGLQVL6Z$_K zwEsM#GwQC+jj+mBXG|D|OZ5**0h6pOBaBqnt|(UX{&AhNGC%&+6#jDt`okQ~_+!Eb zPx~>8ufeWJ>4+!zc~i9gpR4J=DZ?5)hy2Wn#!P?|6B_GXaO1;zo7rkS^!W58qAB}HTFMxDE@QP z{znD-w;!K>3hxx+{a-mi|4SwObHDswH$wllo&fg0XNdmSiumWY@V{=14h_Zr_m0zl zSGYA}_5ba6mOoB1D{n_ec4+WM3~y!sTsyD4Whp5sD1*i1zqWNtiV0ow!nu;?kI$sc z((sJE{A#;~_m9`EfAog>XIWQY2>&SRKjN}h($)Nb7Ibxs{Ew2Z3aYC}*?8AyqSy_n zzb@MiU~2<}0Oo|1Uy7YUl9I+o2`PcWo)v2YDDqBlsF?L1OZ~% zM!`ghBbIqcaZqARAaVJkqU4;UL@XdhUtf?C1VTCr2C;&VRq044$~kimx#5rPf3^$Ea$*h($gV@N0vqdd_O*rb98 zA$~A1d|BgsNo(VqNrd3#_;RuB{rp$bDRH{+AUp;a?i^$t4l*Ntc@8Zl4WPNeO;3rS zP1f+H_ahP_LVrQg0eg@iJ_HvM5e)LMm^#4R!7e_)fjFyo1c_jd3&9bASp*9ZkR#~# zE5uC8#4rXWLfqb}V z5<1+%(Z z7v%6}BnAB|IkS(SF(HURT%%mhW&WGZeCfce;}vyg-!5tAQo^V48|z%au4I7z;Fv@rO?3?5cd)YV69zPz7g*s zgd705KSF#Eg1p8y3)=3AZ5CoN!V-LNUdA?nVgzEN#31Y*kPSiZh7Le13=9m63@nU{ zES$_t%$)1kSyW>z*<4mLIp9yT^M9ynm*Sk&~K6gbPJ$wAGtiG;FUj9nXo2rJKdX*V*6q zQBfuCdudEBk?Jt^VH^{##+y^NU(zJa5o`M35=ni1i_=Hkw;KoQS|dJvHUEUJf&go;um%3bbducXa6)OL`4OprJ=|~Pe-dp5kUmS zMFS)V6Xm8=-g|-vmf*hq%P|#kpAW=+sm2cLFyX4>5_GTt^pa{1T7VFj)?-bb)JeFH zKScO@38a3CK< z3^f=`kC^9Ylc97sgZ>&aWGOMwPli^dOWOSGbhdk%^O(-{{bb0ziww=l6w+Rk50ww? z%QX^yE&U0p!tL1~5J@BbtZvWK9iSK0STeH-{bo_wH9bGcCP}W3-^xn;F-5?&F za%iS>`#qiJ1t9f1k-w+3ymGbjYhLp8H)->%Wv>9tCbzw{#L>rUG#_9YwEMRuj#>pRHMS^Pq>VK|3Dp74151W%Uj zrnlT=NXYzKX4+bfB?b)&!_^sH`z>I0;JhK<1mkzMp!jx)c@2X_>bp~*KGN>ITADEU zEH{6f424TAX{QKWi8yM`I;Ek1qFIPqE~VF1?;07prf`T^{b(2M1lM+@&y~g*A0~=Q zv(HQwE55EGLti#dSCAp`s}r|_dV=me&wG09<*hpTzUq#z8y64K`F}czrV}-u_798S z*WKrr#l?C2+y&BPqSgKdEd%n4Nyk*QgtT}n+7d0EYJZ%F@^A9(KC|)t2*Xm*iBuCV z)?*xoqKB`?uFrgS{|4JV#&-@&pz=pkmbMjJGs@;SH-v1_8Hnm<$B%{1DYz~Qb9dbC z_MduzAuLEOj;3HC}u8c)ox6@ssc=mw1PJz6n#= zgY&h68VaF%&{7Uk4u$CLg2l0o+46$3;olbd5TB!KM?Lx@iaGD^yvu4Z{q>4LMVJBc zd1ZWWIzRedWW%Sa(#2_w$+(8NyQO=+(|)cUy192L>ZQxJ&s7(nB5w{dv~g6a6}kRZpO5d&D zV1kzYa)g&12Z|`+r>VMLbI%L$vpK+aXjUTfay5(Ns1?~17=$}5u{jKWV z+p9#PXt?kl#yD2?#Cw9#eH|Ob$dFD886vj~(oZiro(KygD&Ne@+#|mJJiprF=)#c~ zB^SZ(2hgKOBtgxm&lOgqwpyq;Ze7xP%_kb$!DSq6|COTf0{Ra{;((` z+t`w2i1pY>hV=#5y2pLV7LD!HZ*GCYCGP`m>|`#e08KLFI&vjLI^tTF#+eb@@*b$F zDE3nFNfpAkcZIVmU#=Y*3Q;>v`s}gY>h4qD>Al5DhfpI+^v=MSeWMz! zol@T)-|Lj@Fk83S6lY^_I9^3~>Npw71=?<%)E>HaS-rZ8ul{hLIRftDt7Tm7^Ag9> z+BfW<+=Jt8%QH`fH$Ie8jDI_0hSOT}VG4?cvz)&P_l6Fb!UJn-2i}$s5{l;`oOvxZRO!LWH zehWYDK|=~P=F(C^dtT!HLkGR`lN$dY(`*s^EWT|wUu z$tb;tyS+fW6R&qt5Gd?p$-c_t+A8OZbt8w3XJBRC*{Z93hoopyMuHUsX;YIA1fZz}BGMxxS}=+xO-ytHjx=s6`is{)ze0 zZN?s$-3P|2a`iH6)9w@pGO)&7J-fr8_Ur#pliBao(y@sS-N@+Lk!@L-1|<2~!ms{=9fm3GVE%Hk&$Bff?BpxqdfC5YqG5@(l;lBu z0Xs4O=2RMn`JG#_V-u3fb+Nu7c=36UL%7z(uUm9|8ZRhx^&FBv+23FgvKe*iEawA* zew{+`)W%pf1)ZVDJ5GB*2_xg%yK2@oEn@0#C=Su-%lmGOunrm7L8y8jKgB257nq8~ z-MGLhadF%b*L-*{_8F3$t7oq8bQ>XWvPaK)+nsXvPor;^l9tvTCG;=(ElADi?`0Mc zI=5{@igBcqD{J6pnM0}%zgcb-M4r!dzpI6L#9(PgJlvMugcdK=CPQBFwmZX0Gdi(a zH9h@<9ja#t<)u|4T?x9xCK0|Q>8BZdJN#6(vUOP{8EmV*FW9DA2c}xO=fxfY_l%ml z*nLs^$GiGv#f~?>6&s8n-DduAp|VTGzyd5A^y=O|e&vPP&-cc?pAU*jy*_uk_}ZX}rHoe)ls$0YML8)_0~xwkgLC*gP4Z*;N|gH**(F zg);A6DxI_?Lq>iPo!D(dB{5~4gZ)`xJ;qChB-DhzrEDQXbtmK7%S|m)X3g{CKRh-V z?7C{<$gnZVm{CfVQQi7rD(G_Ula43Bs91j|nGLfi+`SdKzaZSgetCLLMY9bJG!%Av zPIYdlsTxtDGW6VQvPW(J&9xQNHKjJJlkuL}VuYOuEo~*((YZU2P1Gh@`JHHRc>JLA zgGXhbJ11}5(pgX!st4nf9#}oJIZrAGOm>c-OP+KNmMjeGzfEpIi{}JM`$W_hZ{X;f8kZgz@zY4Z`uB%vV9@5cL)m7}jeWlvfaX*gFOnx)qi zo^!e~GFR2n4LW26;!TM+8M?C5g_wF>YvEED_h)|l8FPh5m6Q|0(_44VE=pawUUz+D zsW~dX|IXt(!L%Y4kkA)iy*16KJ)I4cv{JWh| zTWFs7!ll;Qu|DC+V2^9@?awmLv}|{6s2mSA)bQ%-oRD)rHlxM2F);rV7E$SIx9!7( zd&^YatKzz|39gN-;)sd#bF+fC@4EXsZQ8)@9zRkWLGF0cRZQv^Yh^HBz{kJQ6Y}a5I4}R;g0qfWSWo=dD~q)=GIfueA3upN zJQa8?^ive|+^Ji#epB1_N?Vquu<6E&8;&n3mgi??Jk6YKI?MiM)_Y0LGv)Gb{3ULx zbAf}mnx#XYKIx{tG+UcHRWn;2xD>Mk^zWTuvdI<$1s^^R8&%iBwyk(+ia6dq+!Ok~4k`;OvwM$UI&^Q2$R z2Dv`xF)QDe>6V%GJY84icoEl&&_SW@yhZGTCm6S#nJK60&U|7G8_ye=+g=+!`{>&3 z#^WX%H%n-_X?{eFmYw*pz31gpphvk;+J|w;4+_GA=eI}4DQfOfu1l`9luGb&y_=f) zK5%H!x0JOxX6c6O=-e&Yb&G*Ra&xv<-VNOV>%r=B#+kUSrHS+LquWt4cd2jt3FpR- zX1$hZR2e#LC;V)yRl_%{%vqZwb^6tVde+MH#Ad{T;Nw_3e}7;-nsDx1zou z%{nh(dwJ5@GvBC2@WjopJB37^>dqgk8Hkr~ck1Kj!iboVf**vPGs!W@i4?Jt8lqz+ z3LH$OA5gl18pbc7Z&i;FCL?Rw$k0`J-1^cId|)~Kj2I(dqXo(FqXaj1=PW(BGxq$t zl*ENRTjI6OdnqcjmhoE&w{tHQez&cXZxqV$%-QrodRn52y3MK$?Jl9;TY-#Cj7^k1 zkl%B4;%(-swvpYMHPl7E-#jfibt0R$ks;#K$@N~7Tsi(v9uLn8*e%uVHYP*aZMyZw zDLlCv>#P^@6_h_8mDnXYQ*=*rdP1n&@bpK$y85zigD<6^(|(b0?u?*QES$(@=KBm5 z8GG(%No+CTJUw72ELKN`c71vNe&%^o>ei((@WOI?S4F$uDEl4#shNWVkv=}#7rv^! zUi94$R!|(a76Ve}jr_GGrDuQ#P7%S!|&V`5y7DM7)k9>GYIue z);32AdSg=NS`EH!n#5pQ7oJ*|%pGGTLxmO#W_rlDcJ}?xui5Ed3Xy6T%1%p4**(h= zVJ`CY_4bE4fr#mW zL$k*kinz`$N_zV0$y1-1bi>uYo;dUQBEz_aNrZ=1=)=Z|c=C(W+aJWyJ3p=u?RvMh z!|8qC^-Nj$rIPRQomcHo4H&q_l{j?xB?fe(pUY%! z%$49lHEg=KzrY%K?2$4RTYV>5Tu1TpMRnH>_C5{+8AglL=e0xh#tk&x=grQQm#|08 zo*Id5d^R`RdWE)9_4Uc6XQ2~CWJvq|Q2cutiyh61>GZ=Fr%G+l)UjuT%?FX8BSI-+ zqDKR~;*1|TIcw;RC_(PN3Lfv=;>x#1<3o69UT?QP92mF%9T{Rh5uZOyhD>c2_%CAa zUSITU3MTAz)j3nAX7{`=p=RQA?b6#DOVW{VxN|;E?6TdQy*p*vD~6-LW@!e@dHYMg z8;qG6uxLeR9#6-!afmHhTSZo$oIP8g7AIu5fzk~P0 z*dZeb3lYH9I1WNWc91`SN#G8rK%szw`=3xCA!En^Qh^y#_8!?U01%>m983s>H&@}^ z_|?rfdN>XutMMQ92$%-8O@QA&UF7@s@54x%Rf50!g^6Gezwh5V_}vEnceVj|3|P4k z76ABZ9b|L#Ci>w>4mc7C93FwKaTJ@Q4T)s@i&c(k3pnHxiQ5|C4O`IQ(F@ufgj}@& zQbrM4gRRa~XHx_GyBY9)JN#3hCCSQEe${DX!5u9|3F~O7kaYUmaKVT~) z`3J8&f&C9?nAeJZitjIS2LrR{4|5efZ?Ip4b3pRJz>Eccf9jQH#p()=>GUffEBr8G zQgY_N86k$?aMdXAOR*8NUxHBxhG6dyJlfvO9z+&kTB8sXxMq65{qqTH?YfYWnZpmc z02O2gzA^ku>*{JQiWwGAQ7o(Yks{6ITqZ%wLN43B0n@*>(XMi#*n-W%e=)`aS%@pP zUzjJ5CESd-Dhteg^=a0|KXeJS1eepEK(SO)S|FU0qNiLKf&_zoiFoh}%e_Vi%X217 zN3;AL06nGsf;pW0VI7}oG~O42Y-}JIkc$DLM{upoyWn{Ueux%e;rj7G2>gHp5SvzL zz>^Rt%JX1LI}xEwfnf{c@k4lu6&%hBOZNiYaIb>nlwXQ95c%`;rm=$x(#9JDnk_iZ z2`MX~P})jb+R7S8l)AQ>y0)?s=usCCKV`c>waT9g*zbPAfIocEA*feqL79J+10nRY z90EORI}7z@J>0em6 zfgC^!852TdC`VL~n=sNyl$Mqz(gGJ11Z*wxcHWpkZz2|HOb8D34vB%Fys`p(>}vJ;hl#61|Ku00+hvmPTPbyh;C=EF_cQZP93cnn#)I>D)W?6~ ze9l5p#X;a*un6 zcSw`v6HQ21sTgoVDT0UuMj;FiDgVbt{6EgPMyxe*$l2q3aj@|Thja$54A|E|b%$W_ zu(v!UKIHFe_}&v_u~uTLNvZf1zuJqx5De$<;aqcix( z0fJnF`3YLoHdMsc0qfg1SknQ4xNHBplo1IDTJ;;Wl#ll z8@dnGL(Rat(hhY)@1Z_u2pWT?pm}g=i5|g<;6`jf2qPpAvIu2_CPE)!im*o5Bis-> z5PpadL*rhiW60B$XnS zCY2GD6_q2E2bCXH7*#CQ0jgxGGgLWL#ZvvtEn5PTdChu4^z+5(9v+yAZg@jbZ9JSoM3TST8Jfvx*`9L#9vqZ~ED@ZF#t4)ihb*1&AjiODUO{cv~TTc6kww<<*c8ZRU zj+ah~PJ_;Z&W$dBZYSMQy7P1;boc09(tVQx)y9_N1{S5Pr9E=i-+Kjf0SjHH}6vkY}YR2b` zeT?%=oJ>+odQ6T?0ZebO2}Q*(=P8*^j0_jBiRKjQA=q2rO_LGuLgB=KD3dB!ug zj%%IjI_GuK>oV8fS@)ionpct+%^SpE17 zUT~-2CBY|xlN*ILnr{rA~&)Fw?QttlNSeO~&7 z43&(UjK55#%yU^PS#?>w?0MOjaavcj#Zxe{UdS;BAm&@X=7r5NmkZu-{19 zD8Q)LXk@d}X5!}Z&C|x(#=DL0nNXXUn;bEDX3AyiWSU|6)=bpQ&#cI7%v{ZUr};e# zS_>pDr_NJ zv~8O08#^hxFuR(qbX)DVW^Vm#kFwuo-{8RQ;N@_|Vajo{V~S(9(T@_t-yEeP=yW!m`x6yBN-gbH0aHUh;WbaN)iuPOg)L%6mc-(L!^FWX5>tibJUG!&ghWnCowWH zi81}LX0drYX?9|EKG-F)YwxZPyNz~Vj6=lv#65@?i$4(Gx5s=>!Ct1lfqS3sQ`(oh zZ(_g8{^|o859~Sc>7e<+q6D@CVnXL3okQ7&X%6ELKTA|eJe#<51bgIhl2TH7(!x>9 z(Z|P7$IcuhC;KKpOHohBI!<>ye~07hYX7 zzj!N0GAI2K-KEG&pL3ma8}c;s3NQ0tPP)99ACmvRz`o#Np+;fh6~QYhMN~x*MFYj| z#V@X!T&=t&f9+xkZ%NX1=z7HU!O|V29c5N!4{m7QxOP+G=DBk2@*}qpw_b|_&V$gEG)ugq)4c*qfeplYUq7Uj6-@4<;X;e02EO{R#7Fs5iWKq3^(F*3aqv!u^F`l)u~_FdKL= zxP7p1D0FCkIN>Yj*K;GAMsAK8jy@T49qSt>j+4I~{l4LQ{)FnpgGsx|w^PAW^V3IW zHp~>vYR)#yxy<#?M=j7VoLQ7!tX{HO>LCY%4-h}FzgSu4g@h}3!zZLEzM!MMLlt3n z(GYM$4OU*j5i`;U2M*`!iH_FPiX!n?Jy91m8zq}iW1K(UB9@4Ah~46di4DYPVS!g_ zq;9l!bZ}@e_*@7HyjX{XYe(yeQkZK47)C3KA}J)KKt0jrZ!cVJ?7&)!h(oF>pyV-1 zD(Xm8bp>S&bq!@DIiw9(ewpEXz=9m9tfZuYR94nTsc5UJAXhF?@JJ%oSKHpiY~_I< zrnlD3b(D$%f#|0QR`gnmO3I4L%JP6hK0F4vLXMUX2^U-CU;4^eyDdoUoesB#19swvku^2^M5Dt8wL<$EwL%~H`F0*&&58}U6?ixZHn}19i9Q=cA zI0-CL|1otqe4<|wc(w&AXAJOI=qtARC=%ZHZ}P$^lqz2(#eq+8{`xU+$}gm$t6tCl zn*K-fN+v8u+n3@PTu&4q3|uNI2881Lz+=5bwT*$pBH-=Y5qu=J+A#j+qntJLI1}I| zLQfR_N=IHvOI}&SarG-5StVs{CEz$6W^Dspr~AhIYosgRx&14|Ha6NO1WW{6{b&Yg26%7+3Wr{!`BRT|l4FxY7KQn6m zvy5;xX|Hj24Lrk(A}L5$xW0oT;FhZ=3VIE=6j5-vlJ53T(tjp9h2TMx2U1e_E`LU7 zg7?FP!&o(SIb}5saBG5FOHNr`Sx#9)72E))sVb+Yq6BUffB{}pMNU~uT~0*_C8vVY z1he4jSF%$!2{(ld&yTr0jYl`x&37l<%>fH_yIIYNCoj3`Kb2Su+u>qQLA1gFoh0{nhF3_wR2F{MNv44gA)?Zw>s` z!2co*{Cqpdg@9M`DDdtLe3==;f0OE;Uce<60{ll%tzJJNDwxR{2EUN4lK<1+Wq)Q$ zkPh046E$wLWZlSh*-Z%n&rRt=6Wl#%1!oEAXO4~}5Ka;|U!KQJvdBLbwUQ@oB*Qv=s$G&B@u2+AP= zz=@ulK?Ei0&9lyEs~B)S#>l(bUfd^)n0oQ$ub#qs1PCp3r_(3G%LPw2U6M5ZCL}X> z<#Cc$#7U>#!0{rPeYj8Z4LkV9H=~jz$1iFRp2#RE3K6d1`zoJuNFZOeli{UdsvGW@ zM25}XLJ4W+8(iJTFK;eTcY@f;>Mu`xKRZI_`;4$HFif?qhA;5Gr{BI}lPnHHiE@6K zoMC1LV~g(F2hU_3z%J=12fW-RfBRzmqh0#G!5eW_6)jxmcY_W9zW_`{(_e}`mLAg! z1<91f-3|(hoG0!*G(8YYFrVQTh!b|SC_{Yh&2NgDAG>v|Vv*fGTb|Sq{!sZsT&aUc zm~F7whxWIv8_zbJ&~j)D#MZ@(My@+c9}%24P20<5<{oj$ zndJfUaWf1_DVf5MWOq!r#eC0 zL224V_u(F4fhuRF!3QMP;CL#v^m?2x*pXJGHAYFkd2^j^pP@?Yq`DYz_~KCGPdF|! zlbtBSB;`BbZ+|Q?&*(>2=cY(p#nc*ipY3Cx}^rr5gw~q@wmu$Mg_2^E} zeS_*4#ie7JD&?WKQOc5)0Pv#rn2vt#Yb!3>e8!lYKlXfj*m&P%H^%vyuSJN( zIr)#zPN?ge7hP?|Rq2bL?PMSzUR|>1b(k)4=7k=eO6%q8ZZ})#tE@9o5ytk-dBOHP#gEV|X=naALk-K0Yfi z<9^`bA#r=@8y>P<9NYKr&f4%Xkibo}-$mq6HkMj{Y%dLm)t2gr6iqAizGn~oYKN`1 z2U?l$jL(o3H_KsO`r%v?C$cB$H>b}&W%n?_%T{dp|S z-u_bEuZz0^-Qzc<%QGQNx3&X zAx~1A>_g;=W^s+pD2PI!oxGq0&Qcv?+@RF5j@X6Y|D}@#NqtMdcuGcw8 zCrh>*Z9B0}a(kM`w@;cEKgRXsjEN{bOxx1#)Zo!M7n$WE%)8}al|*=c z1-Z3^X)8C6^SOt_QP;Ekjxc;;x-=np*3-wUuJ-KVVc}p@xxIj^@!2xEQN60m zQQ>phf&{@$8Z(YZgVY7mMqf^uADYP)AgzxPJTGC|x;4*)@pvwpXlRxys+?gMiO=6F zwj*giK7YXBh3>WDcDEzJR`O{XZzOTV27mJF5HY>ThRVBUi5l*%7l zS30|?#p2-!mD9i;yWaOyLW6g0Yi&j2muYlfZewdx?aNp71(y$mKER!9HOn!b{r=Ip zfoRn{(QKfAp)#}F!w_duu*q`I7tBas@FR0!kE`8hv@BkPJiX6v#-#5$ZuZ)|YDOR{ zBYZ=#XLiH$Q%~Ml$7z=yMXMe<8a9iwu&WWqd5Irp-gwHW_aS6!B-r`LapA+GOi$Un z**GVc8wHD~dF?c8knCGu_dq@sjM#gd+cvfb`I;`-((hrsG%WS*<$2-4uV3ES9BP}` zk=%aH9CuHy1IAnA5;{%rds|+k?w2~E3a+6)S1^i73{Tm zJvj>#)@`xnCq{mb9=Sl(li)i1F6Xv+K>3uc=l0Is)T7_=@=9VIIV`#5tog{Tn zBf)TVz3RB4a`B^6(Y&?2+fxgGd$+u&cXprr^0jBwJ@1jPEc%|Cx{BYM4g>%1aSWc$9VMCd&HRdKn(B*u# z79ZLA@vh;g$4^;q=YGwy9Ma)7r}5NUr}(-~oj?6p!|~SSn@2p(K@m69&!M=v9K9UA zN@O0g2?*kid!|P`?jvvPJ#_az&h;G1=CzxVfsC6g9h2p8^}#zmHu8S%{vK|*(!S#N zy4y%{ZOOgvNu4p5HkAdX2q^E1jBqi-%3XQ9<&&_7aGdVMdF^jWyYho zg}14yZ{1O;agnp)vek`>a+UA?qWSx68cdwRtF#ZFPak@dyoi~c$_Q3TYE{PMWJLSL z^k->=y;pGJ)y$zD?3+}vi0s~RcYZQWPcPQ=$yB_%WcBl*f^g?Rr*dbRQvo)DCfgYJ z(Nd*4>R-g>wu%U(_tf61uN&-^7K>@^6#tOZ$dIZY(Z0bfuX2+&$IWdS{3$1GPPZ0x zSv!n-QX%%}WV&9a)|_DPhei8yhO$g^hO{82wD= z;nZd0cNuxoNFg_&oc6>XQ5E83z(S+`)4HbS#3`MixLD$+vKLCPK5I}t&M1o#vGftu|5B%_{j{Q7jSOMB zoYW3eVXs)+6TKjL?%vdN>-mF4YIpme;va~lYUNroc^y_TNV3gPl%MjFmeH$9)rvTv zMu^!R=&-ZBAd>$o!+}%=->WoLFckx)U@uDQ`JCue9X9thCva~{eUgKgjOu>9&EY!z5n7=)JH7^|kMhP>R{P1nm6?rJ z;qoXHWKPXfCq)Xs`Xz7mv+PyX&f)ee~k&#JrvAz zO^or%Y45jE&NvH~e^`+C`v9-d>U4BSy|X%dYtGRbNee zfAO^B`&r`Ggo7W`i66H~>9Z{iHcL+USlujj?DECxk}CJ)kZYFCUU@qk2b~7xhb3#g zarR@gGznq`G=f|sm)_8B-OoKxb8Dt&_h35mMtW8329X7)M3*M|PMm{DrKSs9QkaYvRXJBA8Hab z7PaYSO^S3rkJRQB2MYX zsASpl+|NmK(pgl^l5gv>6O`jD_qW$t#UBo`vMdkAb)S`NdI)9g^7yjLUTMj1+pTt? zjnAu&zCKB1!DjB5Ap5PoRkC{Ikb!b$+6`Wf>2;1Lgl;@-b#QEisdpj7WZ1+GsD17e4abQ>{lP6%lr~ zn^5b#QDZ3o=7!;P#dUt1=N@!Few9$BI#JZj)a2uopl|Ek+Y+X%z0X+^xpC@M#O`lo zXkDMyrQ~wm@auaX4(^s99d0C036w|LFx5Ckd18;07U$RopMFhR$AB)$8@5bhZ;d@9 zk$1!KhHF(9vzoxW#8+qmHsyz(K3qyyZXArF5$<6t_r2bt=@FJciXWa-t+IUO?1>3y z8CObW=HwnpGmM-!V)rqr427kOe_+D7KQ&4;3C^MmK2s4#0 zxaot3+ve~EznG;D2BT!C);i0!!qTAcC}Cb;K4odrZ9=;INoo5#YPWBfo3)PI@4xkR zP*GL8h%TVuV^wD1jf&hPgQ$kkFYa0KB5Y#o58tj3&uU8&WSYE` z>Q7y>TkQP7M3)oPL=ByY$|S8VX;sC%O-IR)ZO_$jFWk3pDnA!~gSa_+C) z7agm9I{l62L^2tgCUlnCwAR}6#A_WhUC%N$W%>B(%d=cdyz9jr)JvzVdKk9`^WNS< z{So~}keW*9%}a+)=GcCZ&lh*?e(G|v$>qA{O+{i`qG-0yRpH}@=ofQ2;zMda)oky2 zsu3V}OU3a0*tGQZR2>usZ!B(|A3C6MST%^f5{DD(6#_11+e5t2Sa(Ega&pPZm?7Wd zmNP!N<+;zsrik^%VXEgco4U5;3m!bX*U7%O!e?{DWa*7E*HDb9VoXDZUj9vquFzvH z#R2;4R-dXY4Y+i@#E5TR3>Q+1MlS(bJrRx!+%%Gv^6A{-l}w`u_mN;I730 literal 0 HcmV?d00001 diff --git a/recipes/icons/fokkeensukke.png b/recipes/icons/fokkeensukke.png new file mode 100644 index 0000000000000000000000000000000000000000..a9c3b5f109d71d9b6a455716dbcbad56ff0dc364 GIT binary patch literal 56544 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π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 literal 0 HcmV?d00001 diff --git a/recipes/icons/tweakers_net.png b/recipes/icons/tweakers_net.png new file mode 100644 index 0000000000000000000000000000000000000000..b60be0f64c69c115f5d542fa406137b54256283a GIT binary patch literal 3828 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 Date: Sat, 29 Sep 2012 12:15:46 +0530 Subject: [PATCH 03/34] MTP drivers: Provide feedback during filesystem scan --- src/calibre/devices/mtp/driver.py | 3 ++ src/calibre/devices/mtp/unix/driver.py | 6 +++- src/calibre/devices/mtp/unix/libmtp.c | 14 +++++---- .../mtp/windows/content_enumeration.cpp | 29 ++++++++++++------- src/calibre/devices/mtp/windows/device.cpp | 9 +++--- src/calibre/devices/mtp/windows/driver.py | 11 ++++++- src/calibre/devices/mtp/windows/global.h | 2 +- 7 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/calibre/devices/mtp/driver.py b/src/calibre/devices/mtp/driver.py index 2fe0843484..be2bab7638 100644 --- a/src/calibre/devices/mtp/driver.py +++ b/src/calibre/devices/mtp/driver.py @@ -155,6 +155,9 @@ class MTP_DEVICE(BASE): # }}} # Get list of books from device, with metadata {{{ + def filesystem_callback(self, msg): + self.report_progress(0, msg) + def books(self, oncard=None, end_session=True): from calibre.devices.mtp.books import JSONCodec from calibre.devices.mtp.books import BookList, Book diff --git a/src/calibre/devices/mtp/unix/driver.py b/src/calibre/devices/mtp/unix/driver.py index 71914cddc0..b8e8938c93 100644 --- a/src/calibre/devices/mtp/unix/driver.py +++ b/src/calibre/devices/mtp/unix/driver.py @@ -212,6 +212,9 @@ class MTP_DEVICE(MTPDeviceBase): ans += pprint.pformat(storage) return ans + def _filesystem_callback(self, entry): + self.filesystem_callback(_('Found object: %s')%entry.get('name', '')) + @property def filesystem_cache(self): if self._filesystem_cache is None: @@ -231,7 +234,8 @@ class MTP_DEVICE(MTPDeviceBase): storage.append({'id':sid, 'size':capacity, 'is_folder':True, 'name':name, 'can_delete':False, 'is_system':True}) - items, errs = self.dev.get_filesystem(sid) + items, errs = self.dev.get_filesystem(sid, + self._filesystem_callback) all_items.extend(items), all_errs.extend(errs) if not all_items and all_errs: raise DeviceError( diff --git a/src/calibre/devices/mtp/unix/libmtp.c b/src/calibre/devices/mtp/unix/libmtp.c index bf07c73a35..b62bd8a9c7 100644 --- a/src/calibre/devices/mtp/unix/libmtp.c +++ b/src/calibre/devices/mtp/unix/libmtp.c @@ -357,7 +357,7 @@ Device_storage_info(Device *self, void *closure) { // Device.get_filesystem {{{ -static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uint32_t parent_id, PyObject *ans, PyObject *errs) { +static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uint32_t parent_id, PyObject *ans, PyObject *errs, PyObject *callback) { LIBMTP_file_t *f, *files; PyObject *entry; int ok = 1; @@ -372,12 +372,13 @@ static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uin entry = build_file_metadata(f, storage_id); if (entry == NULL) { ok = 0; } else { + Py_XDECREF(PyObject_CallFunctionObjArgs(callback, entry, NULL)); if (PyList_Append(ans, entry) != 0) { ok = 0; } Py_DECREF(entry); } if (ok && f->filetype == LIBMTP_FILETYPE_FOLDER) { - if (!recursive_get_files(dev, storage_id, f->item_id, ans, errs)) { + if (!recursive_get_files(dev, storage_id, f->item_id, ans, errs, callback)) { ok = 0; } } @@ -394,19 +395,20 @@ static int recursive_get_files(LIBMTP_mtpdevice_t *dev, uint32_t storage_id, uin static PyObject * Device_get_filesystem(Device *self, PyObject *args) { - PyObject *ans, *errs; + PyObject *ans, *errs, *callback; unsigned long storage_id; int ok = 0; ENSURE_DEV(NULL); ENSURE_STORAGE(NULL); - if (!PyArg_ParseTuple(args, "k", &storage_id)) return NULL; + if (!PyArg_ParseTuple(args, "kO", &storage_id, &callback)) return NULL; + if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback is not a callable"); return NULL; } ans = PyList_New(0); errs = PyList_New(0); if (errs == NULL || ans == NULL) { PyErr_NoMemory(); return NULL; } LIBMTP_Clear_Errorstack(self->device); - ok = recursive_get_files(self->device, (uint32_t)storage_id, 0, ans, errs); + ok = recursive_get_files(self->device, (uint32_t)storage_id, 0, ans, errs, callback); dump_errorstack(self->device, errs); if (!ok) { Py_DECREF(ans); @@ -535,7 +537,7 @@ static PyMethodDef Device_methods[] = { }, {"get_filesystem", (PyCFunction)Device_get_filesystem, METH_VARARGS, - "get_filesystem(storage_id) -> Get the list of files and folders on the device in storage_id. Returns files, errors." + "get_filesystem(storage_id, callback) -> Get the list of files and folders on the device in storage_id. Returns files, errors. callback must be a callable that accepts a single argument. It is called with every found object." }, {"get_file", (PyCFunction)Device_get_file, METH_VARARGS, diff --git a/src/calibre/devices/mtp/windows/content_enumeration.cpp b/src/calibre/devices/mtp/windows/content_enumeration.cpp index 580f77f9b0..612ecbc915 100644 --- a/src/calibre/devices/mtp/windows/content_enumeration.cpp +++ b/src/calibre/devices/mtp/windows/content_enumeration.cpp @@ -136,8 +136,9 @@ public: HANDLE complete; ULONG self_ref; PyThreadState *thread_state; + PyObject *callback; - GetBulkCallback(PyObject *items_dict, HANDLE ev) : items(items_dict), complete(ev), self_ref(1), thread_state(NULL) {} + GetBulkCallback(PyObject *items_dict, HANDLE ev, PyObject* pycallback) : items(items_dict), complete(ev), self_ref(1), thread_state(NULL), callback(pycallback) {} ~GetBulkCallback() {} HRESULT __stdcall OnStart(REFGUID Context) { return S_OK; } @@ -195,6 +196,7 @@ public: Py_DECREF(temp); set_properties(obj, properties); + Py_XDECREF(PyObject_CallFunctionObjArgs(callback, obj, NULL)); properties->Release(); properties = NULL; } @@ -207,7 +209,7 @@ public: }; -static PyObject* bulk_get_filesystem(IPortableDevice *device, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *storage_id, IPortableDevicePropVariantCollection *object_ids) { +static PyObject* bulk_get_filesystem(IPortableDevice *device, IPortableDevicePropertiesBulk *bulk_properties, const wchar_t *storage_id, IPortableDevicePropVariantCollection *object_ids, PyObject *pycallback) { PyObject *folders = NULL; GUID guid_context = GUID_NULL; HANDLE ev = NULL; @@ -227,7 +229,7 @@ static PyObject* bulk_get_filesystem(IPortableDevice *device, IPortableDevicePro properties = create_filesystem_properties_collection(); if (properties == NULL) goto end; - callback = new (std::nothrow) GetBulkCallback(folders, ev); + callback = new (std::nothrow) GetBulkCallback(folders, ev, pycallback); if (callback == NULL) { PyErr_NoMemory(); goto end; } hr = bulk_properties->QueueGetValuesByObjectList(object_ids, properties, callback, &guid_context); @@ -272,7 +274,7 @@ end: // }}} // find_all_objects_in() {{{ -static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevicePropVariantCollection *object_ids, const wchar_t *parent_id) { +static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevicePropVariantCollection *object_ids, const wchar_t *parent_id, PyObject *callback) { /* * Find all children of the object identified by parent_id, recursively. * The child ids are put into object_ids. Returns False if any errors @@ -284,6 +286,7 @@ static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevice DWORD fetched, i; PROPVARIANT pv; BOOL ok = 1; + PyObject *id; PropVariantInit(&pv); pv.vt = VT_LPWSTR; @@ -303,10 +306,15 @@ static BOOL find_all_objects_in(IPortableDeviceContent *content, IPortableDevice if (SUCCEEDED(hr)) { for(i = 0; i < fetched; i++) { pv.pwszVal = child_ids[i]; + id = wchar_to_unicode(pv.pwszVal); + if (id != NULL) { + Py_XDECREF(PyObject_CallFunctionObjArgs(callback, id, NULL)); + Py_DECREF(id); + } hr2 = object_ids->Add(&pv); pv.pwszVal = NULL; if (FAILED(hr2)) { hresult_set_exc("Failed to add child ids to propvariantcollection", hr2); break; } - ok = find_all_objects_in(content, object_ids, child_ids[i]); + ok = find_all_objects_in(content, object_ids, child_ids[i], callback); if (!ok) break; } for (i = 0; i < fetched; i++) { CoTaskMemFree(child_ids[i]); child_ids[i] = NULL; } @@ -347,7 +355,7 @@ end: return ans; } -static PyObject* single_get_filesystem(IPortableDeviceContent *content, const wchar_t *storage_id, IPortableDevicePropVariantCollection *object_ids) { +static PyObject* single_get_filesystem(IPortableDeviceContent *content, const wchar_t *storage_id, IPortableDevicePropVariantCollection *object_ids, PyObject *callback) { DWORD num, i; PROPVARIANT pv; HRESULT hr; @@ -375,6 +383,7 @@ static PyObject* single_get_filesystem(IPortableDeviceContent *content, const wc if (SUCCEEDED(hr) && pv.pwszVal != NULL) { item = get_object_properties(devprops, properties, pv.pwszVal); if (item != NULL) { + Py_XDECREF(PyObject_CallFunctionObjArgs(callback, item, NULL)); PyDict_SetItem(ans, PyDict_GetItemString(item, "id"), item); Py_DECREF(item); item = NULL; ok = 1; @@ -429,7 +438,7 @@ end: return values; } // }}} -PyObject* wpd::get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties) { // {{{ +PyObject* wpd::get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties, PyObject *callback) { // {{{ PyObject *folders = NULL; IPortableDevicePropVariantCollection *object_ids = NULL; IPortableDeviceContent *content = NULL; @@ -447,11 +456,11 @@ PyObject* wpd::get_filesystem(IPortableDevice *device, const wchar_t *storage_id Py_END_ALLOW_THREADS; if (FAILED(hr)) { hresult_set_exc("Failed to create propvariantcollection", hr); goto end; } - ok = find_all_objects_in(content, object_ids, storage_id); + ok = find_all_objects_in(content, object_ids, storage_id, callback); if (!ok) goto end; - if (bulk_properties != NULL) folders = bulk_get_filesystem(device, bulk_properties, storage_id, object_ids); - else folders = single_get_filesystem(content, storage_id, object_ids); + if (bulk_properties != NULL) folders = bulk_get_filesystem(device, bulk_properties, storage_id, object_ids, callback); + else folders = single_get_filesystem(content, storage_id, object_ids, callback); end: if (content != NULL) content->Release(); diff --git a/src/calibre/devices/mtp/windows/device.cpp b/src/calibre/devices/mtp/windows/device.cpp index 3d8d442b6c..3886bb5e56 100644 --- a/src/calibre/devices/mtp/windows/device.cpp +++ b/src/calibre/devices/mtp/windows/device.cpp @@ -78,14 +78,15 @@ update_data(Device *self, PyObject *args) { // get_filesystem() {{{ static PyObject* py_get_filesystem(Device *self, PyObject *args) { - PyObject *storage_id, *ret; + PyObject *storage_id, *ret, *callback; wchar_t *storage; - if (!PyArg_ParseTuple(args, "O", &storage_id)) return NULL; + if (!PyArg_ParseTuple(args, "OO", &storage_id, &callback)) return NULL; + if (!PyCallable_Check(callback)) { PyErr_SetString(PyExc_TypeError, "callback is not a callable"); return NULL; } storage = unicode_to_wchar(storage_id); if (storage == NULL) return NULL; - ret = wpd::get_filesystem(self->device, storage, self->bulk_properties); + ret = wpd::get_filesystem(self->device, storage, self->bulk_properties, callback); free(storage); return ret; } // }}} @@ -163,7 +164,7 @@ static PyMethodDef Device_methods[] = { }, {"get_filesystem", (PyCFunction)py_get_filesystem, METH_VARARGS, - "get_filesystem(storage_id) -> Get all files/folders on the storage identified by storage_id. Tries to use bulk operations when possible." + "get_filesystem(storage_id, callback) -> Get all files/folders on the storage identified by storage_id. Tries to use bulk operations when possible. callback must be a callable that accepts a single argument. It is called with every found id and then with the metadata for every id." }, {"get_file", (PyCFunction)py_get_file, METH_VARARGS, diff --git a/src/calibre/devices/mtp/windows/driver.py b/src/calibre/devices/mtp/windows/driver.py index 202c8dfd6e..7253b4490c 100644 --- a/src/calibre/devices/mtp/windows/driver.py +++ b/src/calibre/devices/mtp/windows/driver.py @@ -214,6 +214,14 @@ class MTP_DEVICE(MTPDeviceBase): return True + def _filesystem_callback(self, obj): + if isinstance(obj, dict): + n = obj.get('name', '') + msg = _('Found object: %s')%n + else: + msg = _('Found id: %s')%obj + self.filesystem_callback(msg) + @property def filesystem_cache(self): if self._filesystem_cache is None: @@ -233,7 +241,8 @@ class MTP_DEVICE(MTPDeviceBase): break storage = {'id':storage_id, 'size':capacity, 'name':name, 'is_folder':True, 'can_delete':False, 'is_system':True} - id_map = self.dev.get_filesystem(storage_id) + id_map = self.dev.get_filesystem(storage_id, + self._filesystem_callback) for x in id_map.itervalues(): x['storage_id'] = storage_id all_storage.append(storage) items.append(id_map.itervalues()) diff --git a/src/calibre/devices/mtp/windows/global.h b/src/calibre/devices/mtp/windows/global.h index 212afd2cec..2a9361c18b 100644 --- a/src/calibre/devices/mtp/windows/global.h +++ b/src/calibre/devices/mtp/windows/global.h @@ -56,7 +56,7 @@ int pump_waiting_messages(); extern IPortableDeviceValues* get_client_information(); extern IPortableDevice* open_device(const wchar_t *pnp_id, IPortableDeviceValues *client_information); extern PyObject* get_device_information(IPortableDevice *device, IPortableDevicePropertiesBulk **bulk_properties); -extern PyObject* get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties); +extern PyObject* get_filesystem(IPortableDevice *device, const wchar_t *storage_id, IPortableDevicePropertiesBulk *bulk_properties, PyObject *callback); extern PyObject* get_file(IPortableDevice *device, const wchar_t *object_id, PyObject *dest, PyObject *callback); extern PyObject* create_folder(IPortableDevice *device, const wchar_t *parent_id, const wchar_t *name); extern PyObject* delete_object(IPortableDevice *device, const wchar_t *object_id); From f1ba6ad031f94dc242df05481083c61fe5f3482a Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 29 Sep 2012 12:17:45 +0530 Subject: [PATCH 04/34] ... --- src/calibre/devices/mtp/test.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/mtp/test.py b/src/calibre/devices/mtp/test.py index c273bac5e0..4eaf28a385 100644 --- a/src/calibre/devices/mtp/test.py +++ b/src/calibre/devices/mtp/test.py @@ -239,10 +239,12 @@ class TestDeviceInteraction(unittest.TestCase): # Test get_filesystem used_by_one = self.measure_memory_usage(1, - self.dev.dev.get_filesystem, self.storage.object_id) + self.dev.dev.get_filesystem, self.storage.object_id, lambda x: + x) used_by_many = self.measure_memory_usage(5, - self.dev.dev.get_filesystem, self.storage.object_id) + self.dev.dev.get_filesystem, self.storage.object_id, lambda x: + x) self.check_memory(used_by_one, used_by_many, 'Memory consumption during get_filesystem') From 4cafd33a52209e11ab399d166a176412c643cbd6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 29 Sep 2012 14:57:28 +0530 Subject: [PATCH 05/34] Fix Twitch Films recipe --- recipes/twitchfilms.recipe | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/recipes/twitchfilms.recipe b/recipes/twitchfilms.recipe index dab0643410..1ecfed172d 100644 --- a/recipes/twitchfilms.recipe +++ b/recipes/twitchfilms.recipe @@ -13,6 +13,7 @@ class Twitchfilm(BasicNewsRecipe): max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False + auto_cleanup = True encoding = 'utf-8' publisher = 'Twitch' masthead_url = 'http://twitchfilm.com/img/logo.png' @@ -26,18 +27,18 @@ class Twitchfilm(BasicNewsRecipe): , 'language' : language } - keep_only_tags=[dict(attrs={'class':'asset-header'})] - remove_tags_after=dict(attrs={'class':'asset-body'}) - remove_tags = [ dict(name='div', attrs={'class':['social','categories']}) - , dict(attrs={'id':'main-asset'}) - , dict(name=['meta','link','iframe','embed','object']) - ] + #keep_only_tags=[dict(attrs={'class':'asset-header'})] + #remove_tags_after=dict(attrs={'class':'asset-body'}) + #remove_tags = [ dict(name='div', attrs={'class':['social','categories']}) + #, dict(attrs={'id':'main-asset'}) + #, dict(name=['meta','link','iframe','embed','object']) + #] feeds = [(u'News', u'http://feeds.twitchfilm.net/TwitchEverything')] def preprocess_html(self, soup): for item in soup.findAll(style=True): - del item['style'] + del item['style'] for item in soup.findAll('a'): limg = item.find('img') if item.string is not None: From d4bcaa105f082a7c1719933dbceb4fd35ce38034 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 29 Sep 2012 15:13:04 +0530 Subject: [PATCH 06/34] ... --- manual/faq.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/manual/faq.rst b/manual/faq.rst index c71d440c8d..500b31b68f 100644 --- a/manual/faq.rst +++ b/manual/faq.rst @@ -555,7 +555,7 @@ There can be two reasons why |app| is showing a empty list of books: * Your |app| library folder changed its location. This can happen if it was on an external disk and the drive letter for that disk changed. Or if you accidentally moved the folder. In this case, |app| cannot find its library and so starts up with an empty library instead. To remedy this, do a right-click on the |app| icon in the |app| toolbar (it will say 0 books underneath it) and select Switch/create library. Click the little blue icon to select the new location of your |app| library and click OK. - * Your metadata.db file was deleted/corrupted. In this case, you can ask |app| to rebuild the metadata.db from its backups. Click-and-hold the |app| icon in the |app| toolbar (it will say 0 books underneath it) and select Library maintenance->Restore database. |app| will automatically rebuild metadata.db. + * Your metadata.db file was deleted/corrupted. In this case, you can ask |app| to rebuild the metadata.db from its backups. Right click the |app| icon in the |app| toolbar (it will say 0 books underneath it) and select Library maintenance->Restore database. |app| will automatically rebuild metadata.db. Content From The Web From 23c88e997ea6e06f628964cc71f84864decbefd9 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 29 Sep 2012 15:34:18 +0530 Subject: [PATCH 07/34] Fix Nature News --- recipes/freenature.recipe | 67 ++------------------------------------- 1 file changed, 2 insertions(+), 65 deletions(-) diff --git a/recipes/freenature.recipe b/recipes/freenature.recipe index 0b287842ec..34ac919f4e 100644 --- a/recipes/freenature.recipe +++ b/recipes/freenature.recipe @@ -11,23 +11,8 @@ class NatureNews(BasicNewsRecipe): max_articles_per_feed = 50 no_stylesheets = True - keep_only_tags = [dict(name='div', attrs={'id':'content'})] -# remove_tags_before = dict(name='h1', attrs={'class':'heading entry-title'}) -# remove_tags_after = dict(name='h2', attrs={'id':'comments'}) - remove_tags = [ - dict(name='h2', attrs={'id':'comments'}), - dict(attrs={'alt':'Advertisement'}), - dict(name='div', attrs={'class':'ad'}), - dict(attrs={'class':'Z3988'}), - dict(attrs={'class':['formatpublished','type-of-article','cleardiv','disclaimer','buttons','comments xoxo']}), - dict(name='a', attrs={'href':'#comments'}), - dict(name='h2',attrs={'class':'subheading plusicon icon-add-comment'}) - ] - - preprocess_regexps = [ - (re.compile(r'

ADVERTISEMENT

', re.DOTALL|re.IGNORECASE), lambda match: ''), - ] - + use_embedded_content = False + keep_only_tags = [dict(name='div', attrs={'id':'article'})] extra_css = ''' .author { text-align: right; font-size: small; line-height:1em; margin-top:0px; margin-left:0; margin-right:0; margin-bottom: 0; } .imagedescription { font-size: small; font-style:italic; line-height:1em; margin-top:5px; margin-left:0; margin-right:0; margin-bottom: 0; } @@ -36,51 +21,3 @@ class NatureNews(BasicNewsRecipe): feeds = [('Nature News', 'http://feeds.nature.com/news/rss/most_recent')] - def preprocess_html(self,soup): - # The author name is slightly buried - dig it up - author = soup.find('p', {'class':'byline'}) - if author: - # Find out the author's name - authornamediv = author.find('span',{'class':'author fn'}) - authornamelink = authornamediv.find('a') - if authornamelink: - authorname = authornamelink.contents[0] - else: - authorname = authornamediv.contents[0] - # Stick the author's name in the byline tag - tag = Tag(soup,'div') - tag['class'] = 'author' - tag.insert(0,authorname.strip()) - author.replaceWith(tag) - - # Change the intro from a p to a div - intro = soup.find('p',{'class':'intro'}) - if intro: - tag = Tag(soup,'div') - tag['class'] = 'intro' - tag.insert(0,intro.contents[0]) - intro.replaceWith(tag) - - # Change span class=imagedescription to div - descr = soup.find('span',{'class':'imagedescription'}) - if descr: - tag = Tag(soup,'div') - tag['class'] = 'imagedescription' - tag.insert(0,descr.renderContents()) - descr.replaceWith(tag) - - # The references are in a list, let's make them simpler - reflistcont = soup.find('ul',{'id':'article-refrences'}) - if reflistcont: - reflist = reflistcont.li.renderContents() - tag = Tag(soup,'div') - tag['class'] = 'article-references' - tag.insert(0,reflist) - reflistcont.replaceWith(tag) - - # Within the id=content div, we need to remove all the stuff after the end of the class=entry-content - entrycontent = soup.find('div',{'class':'entry-content'}) - for nextSibling in entrycontent.findNextSiblings(): - nextSibling.extract() - - return soup From c17b43d8cb166186a739eeb88f23ab699d768827 Mon Sep 17 00:00:00 2001 From: GRiker Date: Sat, 29 Sep 2012 19:40:27 -0600 Subject: [PATCH 08/34] Implemented special case icu code for OS X 10.6.x in establish_equivalencies --- .../library/catalogs/epub_mobi_builder.py | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/calibre/library/catalogs/epub_mobi_builder.py b/src/calibre/library/catalogs/epub_mobi_builder.py index 41c42b3705..baa28d5974 100644 --- a/src/calibre/library/catalogs/epub_mobi_builder.py +++ b/src/calibre/library/catalogs/epub_mobi_builder.py @@ -3,12 +3,13 @@ __license__ = 'GPL v3' __copyright__ = '2010, Greg Riker' -import datetime, htmlentitydefs, os, re, shutil, unicodedata, zlib +import datetime, htmlentitydefs, os, platform, re, shutil, unicodedata, zlib from copy import deepcopy from xml.sax.saxutils import escape from calibre import (prepare_string_for_xml, strftime, force_unicode, isbytestring) +from calibre.constants import isosx from calibre.customize.conversion import DummyReporter from calibre.customize.ui import output_profiles from calibre.ebooks.BeautifulSoup import BeautifulSoup, BeautifulStoneSoup, Tag, NavigableString @@ -643,12 +644,32 @@ class CatalogBuilder(object): c = item ordnum, ordlen = collation_order(c) - if last_ordnum != ordnum: - last_c = icu_upper(c[0:ordlen]) - if last_c in exceptions.keys(): - last_c = exceptions[unicode(last_c)] - last_ordnum = ordnum - cl_list[idx] = last_c + if isosx and platform.mac_ver()[0] < '10.7': + # Hackhackhackhackhack + # icu returns bogus results with curly apostrophes, maybe others under OS X 10.6.x + # When we see the magic combo of 0/-1 for ordnum/ordlen, special case the logic + if ordnum == 0 and ordlen == -1: + if icu_upper(c[0]) != last_c: + last_c = icu_upper(c[0]) + if last_c in exceptions.keys(): + last_c = exceptions[unicode(last_c)] + last_ordnum = ordnum + cl_list[idx] = last_c + else: + if last_ordnum != ordnum: + last_c = icu_upper(c[0:ordlen]) + if last_c in exceptions.keys(): + last_c = exceptions[unicode(last_c)] + last_ordnum = ordnum + cl_list[idx] = last_c + + else: + if last_ordnum != ordnum: + last_c = icu_upper(c[0:ordlen]) + if last_c in exceptions.keys(): + last_c = exceptions[unicode(last_c)] + last_ordnum = ordnum + cl_list[idx] = last_c if self.DEBUG and self.opts.verbose: print(" establish_equivalencies():") @@ -656,7 +677,7 @@ class CatalogBuilder(object): for idx, item in enumerate(item_list): print(" %s %s" % (cl_list[idx],item[sort_field])) else: - print(" %s %s" % (cl_list[0], item)) + print(" %s %s" % (cl_list[idx], item)) return cl_list From 0720d2ed74271709377657908885c73a98160ffd Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 30 Sep 2012 09:02:40 +0530 Subject: [PATCH 09/34] FB2 Input: Add support for tag. Fixes #1058591 (Support FB2 code tag) --- resources/templates/fb2.xsl | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/resources/templates/fb2.xsl b/resources/templates/fb2.xsl index 060c90ebbe..5e8fac59df 100644 --- a/resources/templates/fb2.xsl +++ b/resources/templates/fb2.xsl @@ -53,9 +53,11 @@ ul {margin-left: 0} - .epigraph{width:50%; margin-left : 35%;} + .epigraph{width:75%; margin-left : 25%; font-style: italic;} div.paragraph { text-indent: 2em; } + + .subtitle { text-align: center; } @@ -213,7 +215,7 @@ -
+
@@ -234,11 +236,11 @@ - + - + @@ -410,5 +412,9 @@ + + + + From b4d4133c6d2cd2ec1cf3496e8a885b74f049ecad Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 30 Sep 2012 09:03:56 +0530 Subject: [PATCH 10/34] Fix #1058840 (Updated recipe for Twitch films) --- recipes/twitchfilms.recipe | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/recipes/twitchfilms.recipe b/recipes/twitchfilms.recipe index 1ecfed172d..423dead311 100644 --- a/recipes/twitchfilms.recipe +++ b/recipes/twitchfilms.recipe @@ -1,5 +1,5 @@ __license__ = 'GPL v3' -__copyright__ = '2009-2011, Darko Miletic ' +__copyright__ = '2009-2012, Darko Miletic ' ''' twitchfilm.net/news/ ''' @@ -13,10 +13,8 @@ class Twitchfilm(BasicNewsRecipe): max_articles_per_feed = 100 no_stylesheets = True use_embedded_content = False - auto_cleanup = True encoding = 'utf-8' publisher = 'Twitch' - masthead_url = 'http://twitchfilm.com/img/logo.png' category = 'twitch, twitchfilm, movie news, movie reviews, cult cinema, independent cinema, anime, foreign cinema, geek talk' language = 'en' @@ -27,18 +25,18 @@ class Twitchfilm(BasicNewsRecipe): , 'language' : language } - #keep_only_tags=[dict(attrs={'class':'asset-header'})] - #remove_tags_after=dict(attrs={'class':'asset-body'}) - #remove_tags = [ dict(name='div', attrs={'class':['social','categories']}) - #, dict(attrs={'id':'main-asset'}) - #, dict(name=['meta','link','iframe','embed','object']) - #] + keep_only_tags=[dict(attrs={'class':'entry'})] + remove_tags_after=dict(attrs={'class':'text'}) + remove_tags = [ dict(name='div', attrs={'class':['social','categories']}) + , dict(attrs={'id':'main-asset'}) + , dict(name=['meta','link','iframe','embed','object']) + ] feeds = [(u'News', u'http://feeds.twitchfilm.net/TwitchEverything')] def preprocess_html(self, soup): for item in soup.findAll(style=True): - del item['style'] + del item['style'] for item in soup.findAll('a'): limg = item.find('img') if item.string is not None: From c6c878462ed5bbba9d31be6bcbbed7a454762a37 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 30 Sep 2012 17:13:49 +0530 Subject: [PATCH 11/34] Start work on replacing the use of fontconfig in windows --- setup/extensions.py | 6 + src/calibre/constants.py | 2 +- .../ebooks/conversion/plugins/pdf_output.py | 22 +- src/calibre/utils/fonts/embedflag.py | 45 --- src/calibre/utils/fonts/utils.py | 90 ++++++ src/calibre/utils/fonts/win_fonts.py | 112 ++++++++ src/calibre/utils/fonts/winfonts.cpp | 264 ++++++++++-------- 7 files changed, 376 insertions(+), 165 deletions(-) delete mode 100644 src/calibre/utils/fonts/embedflag.py create mode 100644 src/calibre/utils/fonts/utils.py create mode 100644 src/calibre/utils/fonts/win_fonts.py diff --git a/setup/extensions.py b/setup/extensions.py index f7d40ca72c..1827d32f4a 100644 --- a/setup/extensions.py +++ b/setup/extensions.py @@ -191,6 +191,12 @@ if iswindows: # needs_ddk=True, cflags=['/X'] ), + Extension('winfonts', + ['calibre/utils/fonts/winfonts.cpp'], + libraries=['Gdi32', 'User32'], + cflags=['/X'] + ), + ]) if isosx: diff --git a/src/calibre/constants.py b/src/calibre/constants.py index dd7abd89f5..899157c13b 100644 --- a/src/calibre/constants.py +++ b/src/calibre/constants.py @@ -91,7 +91,7 @@ class Plugins(collections.Mapping): 'speedup', ] if iswindows: - plugins.extend(['winutil', 'wpd']) + plugins.extend(['winutil', 'wpd', 'winfonts']) if isosx: plugins.append('usbobserver') if islinux or isosx: diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index b3eed763ac..3019255270 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -151,14 +151,28 @@ class PDFOutput(OutputFormatPlugin): oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log) if iswindows: + from calibre.utils.fonts.utils import remove_embed_restriction # On windows Qt generates an image based PDF if the html uses # embedded fonts. See https://launchpad.net/bugs/1053906 for f in walk(oeb_dir): if f.rpartition('.')[-1].lower() in {'ttf', 'otf'}: - self.log.warn('Found embedded font %s, removing it, as ' - 'embedded fonts on windows are not supported by ' - 'the PDF Output plugin'%os.path.basename(f)) - os.remove(f) + fixed = False + with open(f, 'r+b') as s: + raw = s.read() + try: + raw = remove_embed_restriction(raw) + except: + self.log.exception('Failed to remove embedding' + ' restriction from font %s, ignoring it'% + os.path.basename(f)) + else: + s.seek(0) + s.truncate() + s.write(raw) + fixed = True + + if not fixed: + os.remove(f) opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0] opf = OPF(opfpath, os.path.dirname(opfpath)) diff --git a/src/calibre/utils/fonts/embedflag.py b/src/calibre/utils/fonts/embedflag.py deleted file mode 100644 index 0c4e94bae6..0000000000 --- a/src/calibre/utils/fonts/embedflag.py +++ /dev/null @@ -1,45 +0,0 @@ -#!/usr/bin/env python -# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai -from __future__ import (unicode_literals, division, absolute_import, - print_function) - -__license__ = 'GPL v3' -__copyright__ = '2012, Kovid Goyal ' -__docformat__ = 'restructuredtext en' - -import sys, struct - -class UnsupportedFont(ValueError): - pass - -def remove_embed_restriction(raw): - sfnt_version = raw[:4] - if sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO'}: - raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sfnt_version) - - num_tables = struct.unpack_from(b'>H', raw, 4)[0] - - # Find OS/2 table - offset = 4 + 4*2 # Start of the Table record entries - os2_table_offset = None - for i in xrange(num_tables): - table_tag = raw[offset:offset+4] - offset += 16 # Size of a table record - if table_tag == b'OS/2': - os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0] - break - if os2_table_offset is None: - raise UnsupportedFont('Not a supported font, has no OS/2 table') - - version, = struct.unpack_from(b'>H', raw, os2_table_offset) - - fs_type_offset = os2_table_offset + struct.calcsize(b'>HhHH') - fs_type = struct.unpack_from(b'>H', raw, fs_type_offset)[0] - if fs_type == 0: - return raw - - return raw[:fs_type_offset] + struct.pack(b'>H', 0) + raw[fs_type_offset+2:] - -if __name__ == '__main__': - remove_embed_restriction(open(sys.argv[-1], 'rb').read()) - diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py new file mode 100644 index 0000000000..085373318b --- /dev/null +++ b/src/calibre/utils/fonts/utils.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import sys, struct + +class UnsupportedFont(ValueError): + pass + +def is_truetype_font(raw): + sfnt_version = raw[:4] + return (sfnt_version in {b'\x00\x01\x00\x00', b'OTTO'}, sfnt_version) + +def get_font_characteristics(raw): + num_tables = struct.unpack_from(b'>H', raw, 4)[0] + + # Find OS/2 table + offset = 4 + 4*2 # Start of the Table record entries + os2_table_offset = None + for i in xrange(num_tables): + table_tag = raw[offset:offset+4] + if table_tag == b'OS/2': + os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0] + break + offset += 16 # Size of a table record + if os2_table_offset is None: + raise UnsupportedFont('Not a supported font, has no OS/2 table') + + common_fields = b'>HhHHHhhhhhhhhhhh' + (version, char_width, weight, width, fs_type, subscript_x_size, + subscript_y_size, subscript_x_offset, subscript_y_offset, + superscript_x_size, superscript_y_size, superscript_x_offset, + superscript_y_offset, strikeout_size, strikeout_position, + family_class) = struct.unpack_from(common_fields, + raw, os2_table_offset) + offset = os2_table_offset + struct.calcsize(common_fields) + panose = struct.unpack_from(b'>'+b'B'*10, raw, offset) + panose + offset += 10 + (range1,) = struct.unpack_from(b'>L', raw, offset) + offset += struct.calcsize(b'>L') + if version > 0: + range2, range3, range4 = struct.unpack_from(b'>LLL', raw, offset) + offset += struct.calcsize(b'>LLL') + vendor_id = raw[offset:offset+4] + vendor_id + offset += 4 + selection, = struct.unpack_from(b'>H', raw, offset) + + is_italic = (selection & 0b1) != 0 + is_bold = (selection & 0b100000) != 0 + is_regular = (selection & 0b1000000) != 0 + return weight, is_italic, is_bold, is_regular + +def remove_embed_restriction(raw): + sfnt_version = raw[:4] + if sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO'}: + raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sfnt_version) + + num_tables = struct.unpack_from(b'>H', raw, 4)[0] + + # Find OS/2 table + offset = 4 + 4*2 # Start of the Table record entries + os2_table_offset = None + for i in xrange(num_tables): + table_tag = raw[offset:offset+4] + if table_tag == b'OS/2': + os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0] + break + offset += 16 # Size of a table record + if os2_table_offset is None: + raise UnsupportedFont('Not a supported font, has no OS/2 table') + + version, = struct.unpack_from(b'>H', raw, os2_table_offset) + + fs_type_offset = os2_table_offset + struct.calcsize(b'>HhHH') + fs_type = struct.unpack_from(b'>H', raw, fs_type_offset)[0] + if fs_type == 0: + return raw + + return raw[:fs_type_offset] + struct.pack(b'>H', 0) + raw[fs_type_offset+2:] + +if __name__ == '__main__': + raw = remove_embed_restriction(open(sys.argv[-1], 'rb').read()) + diff --git a/src/calibre/utils/fonts/win_fonts.py b/src/calibre/utils/fonts/win_fonts.py new file mode 100644 index 0000000000..41e0081627 --- /dev/null +++ b/src/calibre/utils/fonts/win_fonts.py @@ -0,0 +1,112 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, sys +from itertools import product + +from calibre import prints +from calibre.constants import plugins +from calibre.utils.fonts.utils import (is_truetype_font, + get_font_characteristics) + +class WinFonts(object): + + def __init__(self, winfonts): + self.w = winfonts + + def font_families(self): + names = set() + for font in self.w.enum_font_families(): + if ( + font['is_truetype'] and + # Fonts with names starting with @ are designed for + # vertical text + not font['name'].startswith('@') + ): + names.add(font['name']) + return sorted(names) + + def get_normalized_name(self, is_italic, weight): + if is_italic: + ft = 'bi' if weight == self.w.FW_BOLD else 'italic' + else: + ft = 'bold' if weight == self.w.FW_BOLD else 'normal' + return ft + + def fonts_for_family(self, family, normalize=True): + family = type(u'')(family) + ans = {} + for weight, is_italic in product( (self.w.FW_NORMAL, self.w.FW_BOLD), (False, True) ): + try: + data = self.w.font_data(family, is_italic, weight) + except Exception as e: + prints('Failed to get font data for font: %s [%s] with error: %s'% + (family, self.get_normalized_name(is_italic, weight), e)) + continue + + ok, sig = is_truetype_font(data) + if not ok: + prints('Not a supported font, sfnt_version: %r'%sig) + continue + ext = 'otf' if sig == b'OTTO' else 'ttf' + + try: + weight, is_italic, is_bold, is_regular = get_font_characteristics(data) + except Exception as e: + prints('Failed to get font characteristic for font: %s [%s]' + ' with error: %s'%(family, + self.get_normalized_name(is_italic, weight), e)) + continue + + if normalize: + ft = {(True, True):'bi', (True, False):'italic', (False, + True):'bold', (False, False):'normal'}[(is_italic, + is_bold)] + else: + ft = (1 if is_italic else 0, weight//10) + + ans[ft] = (ext, data) + + return ans + + +def load_winfonts(): + w, err = plugins['winfonts'] + if w is None: + raise RuntimeError('Failed to load the winfonts module: %s'%err) + return WinFonts(w) + +def test_ttf_reading(): + for f in sys.argv[1:]: + raw = open(f).read() + print (os.path.basename(f)) + get_font_characteristics(raw) + print() + +if __name__ == '__main__': + base = os.path.abspath(__file__) + d = os.path.dirname + pluginsd = os.path.join(d(d(d(base))), 'plugins') + if os.path.exists(os.path.join(pluginsd, 'winfonts.pyd')): + sys.path.insert(0, pluginsd) + import winfonts + w = WinFonts(winfonts) + else: + w = load_winfonts() + + print (w.w) + families = w.font_families() + print (families) + + for family in families: + print (family + ':') + for font, data in w.fonts_for_family(family).iteritems(): + print (' ', font, data[0], len(data[1])) + print () + diff --git a/src/calibre/utils/fonts/winfonts.cpp b/src/calibre/utils/fonts/winfonts.cpp index 8bd2cc7c02..0d991c004e 100644 --- a/src/calibre/utils/fonts/winfonts.cpp +++ b/src/calibre/utils/fonts/winfonts.cpp @@ -1,168 +1,202 @@ /* -:mod:`fontconfig` -- Pythonic interface to Windows font api +:mod:`winfont` -- Pythonic interface to Windows font api ============================================================ -.. module:: fontconfig +.. module:: winfonts :platform: All - :synopsis: Pythonic interface to the fontconfig library + :synopsis: Pythonic interface to the windows font routines .. moduleauthor:: Kovid Goyal Copyright 2009 */ +#define _UNICODE #define UNICODE +#define PY_SSIZE_T_CLEAN #include -#include -#include - -using namespace std; - -vector *get_font_data(HDC hdc) { - DWORD sz; - vector *data; - sz = GetFontData(hdc, 0, 0, NULL, 0); - data = new vector(sz); - if (GetFontData(hdc, 0, 0, &((*data)[0]), sz) == GDI_ERROR) { - delete data; data = NULL; - } - return data; +#include +#include +#include +// Utils {{{ +static wchar_t* unicode_to_wchar(PyObject *o) { + wchar_t *buf; + Py_ssize_t len; + if (o == NULL) return NULL; + if (!PyUnicode_Check(o)) {PyErr_Format(PyExc_TypeError, "The python object must be a unicode object"); return NULL;} + len = PyUnicode_GET_SIZE(o); + buf = (wchar_t *)calloc(len+2, sizeof(wchar_t)); + if (buf == NULL) { PyErr_NoMemory(); return NULL; } + len = PyUnicode_AsWideChar((PyUnicodeObject*)o, buf, len); + if (len == -1) { free(buf); PyErr_Format(PyExc_TypeError, "Invalid python unicode object."); return NULL; } + return buf; } -BOOL is_font_embeddable(ENUMLOGFONTEX *lpelfe) { - HDC hdc; - HFONT font; - HFONT old_font = NULL; - UINT sz; - size_t i; - LPOUTLINETEXTMETRICW metrics; - BOOL ans = TRUE; - hdc = GetDC(NULL); - font = CreateFontIndirect(&lpelfe->elfLogFont); - if (font != NULL) { - old_font = SelectObject(hdc, font); - sz = GetOutlineTextMetrics(hdc, 0, NULL); - metrics = new OUTLINETEXTMETRICW[sz]; - if ( GetOutlineTextMetrics(hdc, sz, metrics) != 0) { - for ( i = 0; i < sz; i++) { - if (metrics[i].otmfsType & 0x01) { - wprintf_s(L"Not embeddable: %s\n", lpelfe->elfLogFont.lfFaceName); - ans = FALSE; break; - } - } - } else ans = FALSE; - delete[] metrics; - DeleteObject(font); - SelectObject(hdc, old_font); - } else ans = FALSE; - ReleaseDC(NULL, hdc); +static PyObject* wchar_to_unicode(const wchar_t *o) { + PyObject *ans; + if (o == NULL) return NULL; + ans = PyUnicode_FromWideChar(o, wcslen(o)); + if (ans == NULL) PyErr_NoMemory(); return ans; } -int CALLBACK find_families_callback ( - ENUMLOGFONTEX *lpelfe, /* pointer to logical-font data */ - NEWTEXTMETRICEX *lpntme, /* pointer to physical-font data */ - int FontType, /* type of font */ - LPARAM lParam /* a combo box HWND */ - ) { - size_t i; - LPWSTR tmp; - vector *families = (vector*)lParam; +// }}} - if (FontType & TRUETYPE_FONTTYPE) { - for (i = 0; i < families->size(); i++) { - if (lstrcmp(families->at(i), lpelfe->elfLogFont.lfFaceName) == 0) - return 1; - } - tmp = new WCHAR[LF_FACESIZE]; - swprintf_s(tmp, LF_FACESIZE, L"%s", lpelfe->elfLogFont.lfFaceName); - families->push_back(tmp); - } +// Enumerate font families {{{ +struct EnumData { + HDC hdc; + PyObject *families; +}; + + +static PyObject* logfont_to_dict(const ENUMLOGFONTEX *lf, const TEXTMETRIC *tm, DWORD font_type, HDC hdc) { + PyObject *name, *full_name, *style, *script; + LOGFONT f = lf->elfLogFont; + + name = wchar_to_unicode(f.lfFaceName); + full_name = wchar_to_unicode(lf->elfFullName); + style = wchar_to_unicode(lf->elfStyle); + script = wchar_to_unicode(lf->elfScript); + + return Py_BuildValue("{s:N, s:N, s:N, s:N, s:O, s:O, s:O, s:O, s:l}", + "name", name, + "full_name", full_name, + "style", style, + "script", script, + "is_truetype", (font_type & TRUETYPE_FONTTYPE) ? Py_True : Py_False, + "is_italic", (tm->tmItalic != 0) ? Py_True : Py_False, + "is_underlined", (tm->tmUnderlined != 0) ? Py_True : Py_False, + "is_strikeout", (tm->tmStruckOut != 0) ? Py_True : Py_False, + "weight", tm->tmWeight + ); +} + +static int CALLBACK find_families_callback(const ENUMLOGFONTEX *lpelfe, const TEXTMETRIC *lpntme, DWORD font_type, LPARAM lParam) { + struct EnumData *enum_data = reinterpret_cast(lParam); + PyObject *font = logfont_to_dict(lpelfe, lpntme, font_type, enum_data->hdc); + if (font == NULL) return 0; + PyList_Append(enum_data->families, font); return 1; } - -vector* find_font_families(void) { +static PyObject* enum_font_families(PyObject *self, PyObject *args) { LOGFONTW logfont; HDC hdc; - vector *families; + PyObject *families; + struct EnumData enum_data; - families = new vector(); + families = PyList_New(0); + if (families == NULL) return PyErr_NoMemory(); SecureZeroMemory(&logfont, sizeof(logfont)); logfont.lfCharSet = DEFAULT_CHARSET; - logfont.lfPitchAndFamily = VARIABLE_PITCH | FF_DONTCARE; - StringCchCopyW(logfont.lfFaceName, 2, L"\0"); + logfont.lfFaceName[0] = L'\0'; hdc = GetDC(NULL); - EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback, - (LPARAM)(families), 0); + enum_data.hdc = hdc; + enum_data.families = families; + EnumFontFamiliesExW(hdc, &logfont, (FONTENUMPROC)find_families_callback, + (LPARAM)(&enum_data), 0); ReleaseDC(NULL, hdc); return families; } -inline void free_families_vector(vector *v) { - for (size_t i = 0; i < v->size(); i++) delete[] v->at(i); - delete v; +// }}} + +static PyObject* font_data(PyObject *self, PyObject *args) { + PyObject *ans = NULL, *italic, *pyname; + LOGFONTW lf; + HDC hdc; + LONG weight; + LPWSTR family = NULL; + HGDIOBJ old_font = NULL; + HFONT hf; + DWORD sz; + char *buf; + + SecureZeroMemory(&lf, sizeof(lf)); + + if (!PyArg_ParseTuple(args, "OOl", &pyname, &italic, &weight)) return NULL; + + family = unicode_to_wchar(pyname); + if (family == NULL) { Py_DECREF(ans); return NULL; } + StringCchCopyW(lf.lfFaceName, LF_FACESIZE, family); + free(family); + + lf.lfItalic = (PyObject_IsTrue(italic)) ? 1 : 0; + lf.lfWeight = weight; + lf.lfOutPrecision = OUT_TT_ONLY_PRECIS; + + hdc = GetDC(NULL); + + if ( (hf = CreateFontIndirect(&lf)) != NULL) { + + if ( (old_font = SelectObject(hdc, hf)) != NULL ) { + sz = GetFontData(hdc, 0, 0, NULL, 0); + if (sz != GDI_ERROR) { + buf = (char*)calloc(sz, sizeof(char)); + + if (buf != NULL) { + if (GetFontData(hdc, 0, 0, buf, sz) != GDI_ERROR) { + ans = PyBytes_FromStringAndSize(buf, sz); + if (ans == NULL) PyErr_NoMemory(); + } else PyErr_SetString(PyExc_ValueError, "GDI Error"); + free(buf); + } else PyErr_NoMemory(); + } else PyErr_SetString(PyExc_ValueError, "GDI Error"); + + SelectObject(hdc, old_font); + } else PyErr_SetFromWindowsErr(0); + DeleteObject(hf); + } else PyErr_SetFromWindowsErr(0); + + ReleaseDC(NULL, hdc); + + return ans; } -#ifdef TEST - -int main(int argc, char **argv) { - vector *all_families; - size_t i; - - all_families = find_font_families(); - - for (i = 0; i < all_families->size(); i++) - wprintf_s(L"%s\n", all_families->at(i)); - - free_families_vector(all_families); - - HDC hdc = GetDC(NULL); - HFONT font = CreateFont(72,0,0,0,0,0,0,0,0,0,0,0,0,L"Verdana"); - HFONT old_font = SelectObject(hdc, font); - vector *data = get_font_data(hdc); - DeleteObject(font); - SelectObject(hdc, old_font); - ReleaseDC(NULL, hdc); - if (data != NULL) printf("\nyay: %d\n", data->size()); - delete data; - - return 0; -} -#else - -#define PY_SSIZE_T_CLEAN -#include -# - static -PyMethodDef fontconfig_methods[] = { - {"find_font_families", fontconfig_find_font_families, METH_VARARGS, - "find_font_families(allowed_extensions)\n\n" - "Find all font families on the system for fonts of the specified types. If no " - "types are specified all font families are returned." +PyMethodDef winfonts_methods[] = { + {"enum_font_families", enum_font_families, METH_VARARGS, + "enum_font_families()\n\n" + "Enumerate all regular (not italic/bold/etc. variants) font families on the system. Note there will be multiple entries for every family (corresponding to each charset of the font)." }, + {"font_data", font_data, METH_VARARGS, + "font_data(family_name, italic, weight)\n\n" + "Return the raw font data for the specified font." + }, {NULL, NULL, 0, NULL} }; -extern "C" { PyMODINIT_FUNC -initfontconfig(void) { +initwinfonts(void) { PyObject *m; m = Py_InitModule3( - "fontconfig", fontconfig_methods, - "Find fonts." + "winfonts", winfonts_methods, + "Windows font API" ); if (m == NULL) return; -} + + PyModule_AddIntMacro(m, FW_DONTCARE); + PyModule_AddIntMacro(m, FW_THIN); + PyModule_AddIntMacro(m, FW_EXTRALIGHT); + PyModule_AddIntMacro(m, FW_ULTRALIGHT); + PyModule_AddIntMacro(m, FW_LIGHT); + PyModule_AddIntMacro(m, FW_NORMAL); + PyModule_AddIntMacro(m, FW_REGULAR); + PyModule_AddIntMacro(m, FW_MEDIUM); + PyModule_AddIntMacro(m, FW_SEMIBOLD); + PyModule_AddIntMacro(m, FW_DEMIBOLD); + PyModule_AddIntMacro(m, FW_BOLD); + PyModule_AddIntMacro(m, FW_EXTRABOLD); + PyModule_AddIntMacro(m, FW_ULTRABOLD); + PyModule_AddIntMacro(m, FW_HEAVY); + PyModule_AddIntMacro(m, FW_BLACK); } -#endif From 935049c1b39956c14e109ca52761ea352f101e3f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 08:56:03 +0530 Subject: [PATCH 12/34] Debug prints for failure to load styles --- src/calibre/gui2/__init__.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/calibre/gui2/__init__.py b/src/calibre/gui2/__init__.py index 69cafebdef..0ac0783bd5 100644 --- a/src/calibre/gui2/__init__.py +++ b/src/calibre/gui2/__init__.py @@ -12,6 +12,7 @@ from PyQt4.Qt import (QVariant, QFileInfo, QObject, SIGNAL, QBuffer, Qt, ORG_NAME = 'KovidsBrain' APP_UID = 'libprs500' +from calibre import prints from calibre.constants import (islinux, iswindows, isbsd, isfrozen, isosx, plugins, config_dir, filesystem_encoding, DEBUG) from calibre.utils.config import Config, ConfigProxy, dynamic, JSONConfig @@ -796,7 +797,8 @@ class Application(QApplication): path = os.path.join(sys.extensions_location, 'calibre_style.'+( 'pyd' if iswindows else 'so')) - self.pi.load_style(path, 'Calibre') + if not self.pi.load_style(path, 'Calibre'): + prints('Failed to load calibre style') # On OSX, on some machines, colors can be invalid. See https://bugs.launchpad.net/bugs/1014900 for role in (orig_pal.Button, orig_pal.Window): c = orig_pal.brush(role).color() @@ -853,6 +855,8 @@ class Application(QApplication): except: import traceback traceback.print_exc() + if not depth_ok: + prints('Color depth is less than 32 bits disabling modern look') if force_calibre_style or (depth_ok and gprefs['ui_style'] != 'system'): From df7a7565980ac93ae5080f2b4e300bd2d489c062 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 09:00:45 +0530 Subject: [PATCH 13/34] Fix #1059364 (Washington Post recipe needs an update) --- recipes/wash_post.recipe | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/recipes/wash_post.recipe b/recipes/wash_post.recipe index 61a469b47d..eff27b1f2a 100644 --- a/recipes/wash_post.recipe +++ b/recipes/wash_post.recipe @@ -64,8 +64,10 @@ class TheWashingtonPost(BasicNewsRecipe): def get_article_url(self, article): link = BasicNewsRecipe.get_article_url(self,article) + if article.id.startswith('http'): + link = article.id if not 'washingtonpost.com' in link: - self.log('Skipping adds:', link) + self.log('Skipping ads:', link) return None for it in ['_video.html','_gallery.html','_links.html']: if it in link: From d24d34bef7fb39fa0608815ccc8e82b46e623966 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 09:34:30 +0530 Subject: [PATCH 14/34] FB2 Input: Add support for th and strikethrought ags and also rowspan, colspan and align attributes. Fixes #1059351 (More spec compliant FB2 support) --- resources/templates/fb2.xsl | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/resources/templates/fb2.xsl b/resources/templates/fb2.xsl index 5e8fac59df..84fdd323bd 100644 --- a/resources/templates/fb2.xsl +++ b/resources/templates/fb2.xsl @@ -101,7 +101,7 @@ -
  • +
  • , # @@ -296,16 +296,30 @@ - - - - + + + + + + + + + + + + + + + + + +
    @@ -416,5 +430,9 @@ + + + + From 24a9d26176a26b72fdbf3ea827bc80df6b92fe2b Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 13:34:53 +0530 Subject: [PATCH 15/34] Do not use fontconfig on windows --- src/calibre/utils/fonts/__init__.py | 180 +++++++------------------- src/calibre/utils/fonts/fc.py | 168 +++++++++++++++++++++++++ src/calibre/utils/fonts/utils.py | 181 ++++++++++++++++++++------- src/calibre/utils/fonts/win_fonts.py | 32 ++++- 4 files changed, 376 insertions(+), 185 deletions(-) create mode 100644 src/calibre/utils/fonts/fc.py diff --git a/src/calibre/utils/fonts/__init__.py b/src/calibre/utils/fonts/__init__.py index 7b4f0abea4..c847718153 100644 --- a/src/calibre/utils/fonts/__init__.py +++ b/src/calibre/utils/fonts/__init__.py @@ -6,71 +6,22 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, sys +from calibre.constants import iswindows -from calibre.constants import plugins, iswindows, islinux, isbsd - -_fc, _fc_err = plugins['fontconfig'] - -if _fc is None: - raise RuntimeError('Failed to load fontconfig with error:'+_fc_err) - -if islinux or isbsd: - Thread = object -else: - from threading import Thread - -class FontConfig(Thread): +class Fonts(object): def __init__(self): - Thread.__init__(self) - self.daemon = True - self.failed = False + if iswindows: + from calibre.utils.fonts.win_fonts import load_winfonts + self.backend = load_winfonts() + else: + from calibre.utils.fonts.fc import fontconfig + self.backend = fontconfig - def run(self): - config = None - if getattr(sys, 'frameworks_dir', False): - config_dir = os.path.join(os.path.dirname( - getattr(sys, 'frameworks_dir')), 'Resources', 'fonts') - if isinstance(config_dir, unicode): - config_dir = config_dir.encode(sys.getfilesystemencoding()) - config = os.path.join(config_dir, 'fonts.conf') - if iswindows and getattr(sys, 'frozen', False): - config_dir = os.path.join(os.path.dirname(sys.executable), - 'fontconfig') - if isinstance(config_dir, unicode): - config_dir = config_dir.encode(sys.getfilesystemencoding()) - config = os.path.join(config_dir, 'fonts.conf') - try: - _fc.initialize(config) - except: - import traceback - traceback.print_exc() - self.failed = True - - def wait(self): - if not (islinux or isbsd): - self.join() - if self.failed: - raise RuntimeError('Failed to initialize fontconfig') - - def find_font_families(self, allowed_extensions=['ttf', 'otf']): - ''' - Return an alphabetically sorted list of font families available on the system. - - `allowed_extensions`: A list of allowed extensions for font file types. Defaults to - `['ttf', 'otf']`. If it is empty, it is ignored. - ''' - self.wait() - ans = _fc.find_font_families([bytes('.'+x) for x in allowed_extensions]) - ans = sorted(set(ans), cmp=lambda x,y:cmp(x.lower(), y.lower())) - ans2 = [] - for x in ans: - try: - ans2.append(x.decode('utf-8')) - except UnicodeDecodeError: - continue - return ans2 + def find_font_families(self, allowed_extensions={'ttf', 'otf'}): + if iswindows: + return self.backend.font_families() + return self.backend.find_font_families(allowed_extensions=allowed_extensions) def files_for_family(self, family, normalize=True): ''' @@ -80,89 +31,42 @@ class FontConfig(Thread): they are a tuple (slant, weight) otherwise they are strings from the set `('normal', 'bold', 'italic', 'bi', 'light', 'li')` ''' - self.wait() - if isinstance(family, unicode): - family = family.encode('utf-8') - fonts = {} - ofamily = str(family).decode('utf-8') - for fullname, path, style, nfamily, weight, slant in \ - _fc.files_for_family(str(family)): - style = (slant, weight) - if normalize: - italic = slant > 0 - normal = weight == 80 - bold = weight > 80 - if italic: - style = 'italic' if normal else 'bi' if bold else 'li' - else: - style = 'normal' if normal else 'bold' if bold else 'light' - try: - fullname, path = fullname.decode('utf-8'), path.decode('utf-8') - nfamily = nfamily.decode('utf-8') - except UnicodeDecodeError: - continue - if style in fonts: - if nfamily.lower().strip() == ofamily.lower().strip() \ - and 'Condensed' not in fullname and 'ExtraLight' not in fullname: - fonts[style] = (path, fullname) - else: - fonts[style] = (path, fullname) + if iswindows: + from calibre.ptempfile import PersistentTemporaryFile + fonts = self.backend.fonts_for_family(family, normalize=normalize) + ans = {} + for ft, val in fonts.iteritems(): + ext, name, data = val + pt = PersistentTemporaryFile('.'+ext) + pt.write(data) + pt.close() + ans[ft] = (name, pt.name) + return ans + return self.backend.files_for_family(family, normalize=normalize) - return fonts - - def match(self, name, all=False, verbose=False): + def fonts_for_family(self, family, normalize=True): ''' - Find the system font that most closely matches `name`, where `name` is a specification - of the form:: - familyname-::... + Just like files for family, except that it returns 3-tuples of the form + (extension, full name, font data). + ''' + if iswindows: + return self.backend.fonts_for_family(family, normalize=normalize) + files = self.backend.files_for_family(family, normalize=normalize) + ans = {} + for ft, val in files.iteritems(): + name, f = val + ext = f.rpartition('.')[-1].lower() + ans[ft] = (ext, name, open(f, 'rb').read()) + return ans - For example, `verdana:weight=bold:slant=italic` - - Returns a list of dictionaries, or a single dictionary. - Each dictionary has the keys: - 'weight', 'slant', 'family', 'file', 'fullname', 'style' - - `all`: If `True` return a sorted list of matching fonts, where the sort - is in order of decreasing closeness of matching. If `False` only the - best match is returned. ''' - self.wait() - if isinstance(name, unicode): - name = name.encode('utf-8') - fonts = [] - for fullname, path, style, family, weight, slant in \ - _fc.match(str(name), bool(all), bool(verbose)): - try: - fullname = fullname.decode('utf-8') - path = path.decode('utf-8') - style = style.decode('utf-8') - family = family.decode('utf-8') - fonts.append({ - 'fullname' : fullname, - 'path' : path, - 'style' : style, - 'family' : family, - 'weight' : weight, - 'slant' : slant - }) - except UnicodeDecodeError: - continue - return fonts if all else (fonts[0] if fonts else None) - -fontconfig = FontConfig() -if islinux or isbsd: - # On X11 Qt also uses fontconfig, so initialization must happen in the - # main thread. In any case on X11 initializing fontconfig should be very - # fast - fontconfig.run() -else: - fontconfig.start() +fontconfig = Fonts() def test(): - from pprint import pprint; - pprint(fontconfig.find_font_families()) - pprint(fontconfig.files_for_family('liberation serif')) + import os + print(fontconfig.find_font_families()) m = 'times new roman' if iswindows else 'liberation serif' - pprint(fontconfig.match(m+':slant=italic:weight=bold', verbose=True)) + for ft, val in fontconfig.files_for_family(m).iteritems(): + print val[0], ft, val[1], os.path.getsize(val[1]) if __name__ == '__main__': test() diff --git a/src/calibre/utils/fonts/fc.py b/src/calibre/utils/fonts/fc.py new file mode 100644 index 0000000000..a79b0e1963 --- /dev/null +++ b/src/calibre/utils/fonts/fc.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai +from __future__ import with_statement + +__license__ = 'GPL v3' +__copyright__ = '2009, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +import os, sys + +from calibre.constants import plugins, iswindows, islinux, isbsd + +_fc, _fc_err = plugins['fontconfig'] + +if _fc is None: + raise RuntimeError('Failed to load fontconfig with error:'+_fc_err) + +if islinux or isbsd: + Thread = object +else: + from threading import Thread + +class FontConfig(Thread): + + def __init__(self): + Thread.__init__(self) + self.daemon = True + self.failed = False + + def run(self): + config = None + if getattr(sys, 'frameworks_dir', False): + config_dir = os.path.join(os.path.dirname( + getattr(sys, 'frameworks_dir')), 'Resources', 'fonts') + if isinstance(config_dir, unicode): + config_dir = config_dir.encode(sys.getfilesystemencoding()) + config = os.path.join(config_dir, 'fonts.conf') + if iswindows and getattr(sys, 'frozen', False): + config_dir = os.path.join(os.path.dirname(sys.executable), + 'fontconfig') + if isinstance(config_dir, unicode): + config_dir = config_dir.encode(sys.getfilesystemencoding()) + config = os.path.join(config_dir, 'fonts.conf') + try: + _fc.initialize(config) + except: + import traceback + traceback.print_exc() + self.failed = True + + def wait(self): + if not (islinux or isbsd): + self.join() + if self.failed: + raise RuntimeError('Failed to initialize fontconfig') + + def find_font_families(self, allowed_extensions={'ttf', 'otf'}): + ''' + Return an alphabetically sorted list of font families available on the system. + + `allowed_extensions`: A list of allowed extensions for font file types. Defaults to + `['ttf', 'otf']`. If it is empty, it is ignored. + ''' + self.wait() + ans = _fc.find_font_families([bytes('.'+x) for x in allowed_extensions]) + ans = sorted(set(ans), cmp=lambda x,y:cmp(x.lower(), y.lower())) + ans2 = [] + for x in ans: + try: + ans2.append(x.decode('utf-8')) + except UnicodeDecodeError: + continue + return ans2 + + def files_for_family(self, family, normalize=True): + ''' + Find all the variants in the font family `family`. + Returns a dictionary of tuples. Each tuple is of the form (Full font name, path to font file). + The keys of the dictionary depend on `normalize`. If `normalize` is `False`, + they are a tuple (slant, weight) otherwise they are strings from the set + `('normal', 'bold', 'italic', 'bi', 'light', 'li')` + ''' + self.wait() + if isinstance(family, unicode): + family = family.encode('utf-8') + fonts = {} + ofamily = str(family).decode('utf-8') + for fullname, path, style, nfamily, weight, slant in \ + _fc.files_for_family(str(family)): + style = (slant, weight) + if normalize: + italic = slant > 0 + normal = weight == 80 + bold = weight > 80 + if italic: + style = 'italic' if normal else 'bi' if bold else 'li' + else: + style = 'normal' if normal else 'bold' if bold else 'light' + try: + fullname, path = fullname.decode('utf-8'), path.decode('utf-8') + nfamily = nfamily.decode('utf-8') + except UnicodeDecodeError: + continue + if style in fonts: + if nfamily.lower().strip() == ofamily.lower().strip() \ + and 'Condensed' not in fullname and 'ExtraLight' not in fullname: + fonts[style] = (path, fullname) + else: + fonts[style] = (path, fullname) + + return fonts + + def match(self, name, all=False, verbose=False): + ''' + Find the system font that most closely matches `name`, where `name` is a specification + of the form:: + familyname-::... + + For example, `verdana:weight=bold:slant=italic` + + Returns a list of dictionaries, or a single dictionary. + Each dictionary has the keys: + 'weight', 'slant', 'family', 'file', 'fullname', 'style' + + `all`: If `True` return a sorted list of matching fonts, where the sort + is in order of decreasing closeness of matching. If `False` only the + best match is returned. ''' + self.wait() + if isinstance(name, unicode): + name = name.encode('utf-8') + fonts = [] + for fullname, path, style, family, weight, slant in \ + _fc.match(str(name), bool(all), bool(verbose)): + try: + fullname = fullname.decode('utf-8') + path = path.decode('utf-8') + style = style.decode('utf-8') + family = family.decode('utf-8') + fonts.append({ + 'fullname' : fullname, + 'path' : path, + 'style' : style, + 'family' : family, + 'weight' : weight, + 'slant' : slant + }) + except UnicodeDecodeError: + continue + return fonts if all else (fonts[0] if fonts else None) + +fontconfig = FontConfig() +if islinux or isbsd: + # On X11 Qt also uses fontconfig, so initialization must happen in the + # main thread. In any case on X11 initializing fontconfig should be very + # fast + fontconfig.run() +else: + fontconfig.start() + +def test(): + from pprint import pprint; + pprint(fontconfig.find_font_families()) + pprint(fontconfig.files_for_family('liberation serif')) + m = 'times new roman' if iswindows else 'liberation serif' + pprint(fontconfig.match(m+':slant=italic:weight=bold', verbose=True)) + +if __name__ == '__main__': + test() diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py index 085373318b..6822cbe4dd 100644 --- a/src/calibre/utils/fonts/utils.py +++ b/src/calibre/utils/fonts/utils.py @@ -7,7 +7,9 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import sys, struct +import struct +from io import BytesIO +from collections import defaultdict class UnsupportedFont(ValueError): pass @@ -16,75 +18,170 @@ def is_truetype_font(raw): sfnt_version = raw[:4] return (sfnt_version in {b'\x00\x01\x00\x00', b'OTTO'}, sfnt_version) -def get_font_characteristics(raw): +def get_table(raw, name): + ''' Get the raw table bytes for the specified table in the font ''' num_tables = struct.unpack_from(b'>H', raw, 4)[0] - - # Find OS/2 table - offset = 4 + 4*2 # Start of the Table record entries - os2_table_offset = None + offset = 4*3 # start of the table record entries + table_offset = table_checksum = table_length = table_index = table = None + name = bytes(name.lower()) for i in xrange(num_tables): table_tag = raw[offset:offset+4] - if table_tag == b'OS/2': - os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0] + if table_tag.lower() == name: + table_checksum, table_offset, table_length = struct.unpack_from( + b'>3L', raw, offset+4) + table_index = offset break - offset += 16 # Size of a table record - if os2_table_offset is None: + offset += 4*4 + if table_offset is not None: + table = raw[table_offset:table_offset+table_length] + return table, table_index, table_offset, table_checksum + +def get_font_characteristics(raw): + ''' + Return (weight, is_italic, is_bold, is_regular, fs_type). These values are taken + from the OS/2 table of the font. See + http://www.microsoft.com/typography/otspec/os2.htm for details + ''' + os2_table = get_table(raw, 'os/2')[0] + if os2_table is None: raise UnsupportedFont('Not a supported font, has no OS/2 table') - common_fields = b'>HhHHHhhhhhhhhhhh' + common_fields = b'>Hh3H11h' (version, char_width, weight, width, fs_type, subscript_x_size, subscript_y_size, subscript_x_offset, subscript_y_offset, superscript_x_size, superscript_y_size, superscript_x_offset, superscript_y_offset, strikeout_size, strikeout_position, - family_class) = struct.unpack_from(common_fields, - raw, os2_table_offset) - offset = os2_table_offset + struct.calcsize(common_fields) - panose = struct.unpack_from(b'>'+b'B'*10, raw, offset) + family_class) = struct.unpack_from(common_fields, os2_table) + offset = struct.calcsize(common_fields) + panose = struct.unpack_from(b'>10B', os2_table, offset) panose offset += 10 - (range1,) = struct.unpack_from(b'>L', raw, offset) + (range1,) = struct.unpack_from(b'>L', os2_table, offset) offset += struct.calcsize(b'>L') if version > 0: - range2, range3, range4 = struct.unpack_from(b'>LLL', raw, offset) - offset += struct.calcsize(b'>LLL') - vendor_id = raw[offset:offset+4] + range2, range3, range4 = struct.unpack_from(b'>3L', os2_table, offset) + offset += struct.calcsize(b'>3L') + vendor_id = os2_table[offset:offset+4] vendor_id offset += 4 - selection, = struct.unpack_from(b'>H', raw, offset) + selection, = struct.unpack_from(b'>H', os2_table, offset) is_italic = (selection & 0b1) != 0 is_bold = (selection & 0b100000) != 0 is_regular = (selection & 0b1000000) != 0 - return weight, is_italic, is_bold, is_regular + return weight, is_italic, is_bold, is_regular, fs_type + +def decode_name_record(recs): + ''' + Get the English names of this font. See + http://www.microsoft.com/typography/otspec/name.htm for details. + ''' + if not recs: return None + unicode_names = {} + windows_names = {} + mac_names = {} + for platform_id, encoding_id, language_id, src in recs: + if language_id > 0x8000: continue + if platform_id == 0: + if encoding_id < 4: + try: + unicode_names[language_id] = src.decode('utf-16-be') + except ValueError: + continue + elif platform_id == 1: + try: + mac_names[language_id] = src.decode('utf-8') + except ValueError: + continue + elif platform_id == 2: + codec = {0:'ascii', 1:'utf-16-be', 2:'iso-8859-1'}.get(encoding_id, + None) + if codec is None: continue + try: + unicode_names[language_id] = src.decode(codec) + except ValueError: + continue + elif platform_id == 3: + codec = {1:16, 10:32}.get(encoding_id, None) + if codec is None: continue + try: + windows_names[language_id] = src.decode('utf-%d-be'%codec) + except ValueError: + continue + + # First try the windows names + # First look for the US English name + if 1033 in windows_names: + return windows_names[1033] + # Look for some other english name variant + for lang in (3081, 10249, 4105, 9225, 16393, 6153, 8201, 17417, 5129, + 13321, 18441, 7177, 11273, 2057, 12297): + if lang in windows_names: + return windows_names[lang] + + # Look for Mac name + if 0 in mac_names: + return mac_names[0] + + # Use unicode names + for val in unicode_names.itervalues(): + return val + + return None + +def get_font_names(raw): + table = get_table(raw, 'name')[0] + if table is None: + raise UnsupportedFont('Not a supported font, has no name table') + table_type, count, string_offset = struct.unpack_from(b'>3H', table) + + records = defaultdict(list) + + for i in xrange(count): + try: + platform_id, encoding_id, language_id, name_id, length, offset = \ + struct.unpack_from(b'>6H', table, 6+i*12) + except struct.error: + break + offset += string_offset + src = table[offset:offset+length] + records[name_id].append((platform_id, encoding_id, language_id, + src)) + + family_name = decode_name_record(records[1]) + subfamily_name = decode_name_record(records[2]) + full_name = decode_name_record(records[4]) + + return family_name, subfamily_name, full_name + def remove_embed_restriction(raw): - sfnt_version = raw[:4] - if sfnt_version not in {b'\x00\x01\x00\x00', b'OTTO'}: - raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sfnt_version) + ok, sig = is_truetype_font(raw) + if not ok: + raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sig) - num_tables = struct.unpack_from(b'>H', raw, 4)[0] - - # Find OS/2 table - offset = 4 + 4*2 # Start of the Table record entries - os2_table_offset = None - for i in xrange(num_tables): - table_tag = raw[offset:offset+4] - if table_tag == b'OS/2': - os2_table_offset = struct.unpack_from(b'>I', raw, offset+8)[0] - break - offset += 16 # Size of a table record - if os2_table_offset is None: + table, table_index, table_offset = get_table(raw, 'os/2') + if table is None: raise UnsupportedFont('Not a supported font, has no OS/2 table') - version, = struct.unpack_from(b'>H', raw, os2_table_offset) - - fs_type_offset = os2_table_offset + struct.calcsize(b'>HhHH') - fs_type = struct.unpack_from(b'>H', raw, fs_type_offset)[0] + fs_type_offset = struct.calcsize(b'>HhHH') + fs_type = struct.unpack_from(b'>H', table, fs_type_offset)[0] if fs_type == 0: return raw - return raw[:fs_type_offset] + struct.pack(b'>H', 0) + raw[fs_type_offset+2:] + f = BytesIO(raw) + f.seek(fs_type_offset + table_offset) + f.write(struct.pack(b'>H', 0)) + return f.getvalue() + +def test(): + import sys, os + for f in sys.argv[1:]: + print (os.path.basename(f)) + raw = open(f, 'rb').read() + print (get_font_names(raw)) + print (get_font_characteristics(raw)) if __name__ == '__main__': - raw = remove_embed_restriction(open(sys.argv[-1], 'rb').read()) + test() diff --git a/src/calibre/utils/fonts/win_fonts.py b/src/calibre/utils/fonts/win_fonts.py index 41e0081627..bcfa40758b 100644 --- a/src/calibre/utils/fonts/win_fonts.py +++ b/src/calibre/utils/fonts/win_fonts.py @@ -12,7 +12,7 @@ from itertools import product from calibre import prints from calibre.constants import plugins -from calibre.utils.fonts.utils import (is_truetype_font, +from calibre.utils.fonts.utils import (is_truetype_font, get_font_names, get_font_characteristics) class WinFonts(object): @@ -57,13 +57,18 @@ class WinFonts(object): ext = 'otf' if sig == b'OTTO' else 'ttf' try: - weight, is_italic, is_bold, is_regular = get_font_characteristics(data) + weight, is_italic, is_bold, is_regular = get_font_characteristics(data)[:4] except Exception as e: prints('Failed to get font characteristic for font: %s [%s]' ' with error: %s'%(family, self.get_normalized_name(is_italic, weight), e)) continue + try: + family_name, sub_family_name, full_name = get_font_names(data) + except: + pass + if normalize: ft = {(True, True):'bi', (True, False):'italic', (False, True):'bold', (False, False):'normal'}[(is_italic, @@ -71,7 +76,24 @@ class WinFonts(object): else: ft = (1 if is_italic else 0, weight//10) - ans[ft] = (ext, data) + if not (family_name or full_name): + # prints('Font %s [%s] has no names'%(family, + # self.get_normalized_name(is_italic, weight))) + family_name = family + name = full_name or family + ' ' + (sub_family_name or '') + + try: + name.encode('ascii') + except ValueError: + try: + sub_family_name.encode('ascii') + subf = sub_family_name + except: + subf = '' + + name = family + ((' ' + subf) if subf else '') + + ans[ft] = (ext, name, data) return ans @@ -105,8 +127,8 @@ if __name__ == '__main__': print (families) for family in families: - print (family + ':') + prints(family + ':') for font, data in w.fonts_for_family(family).iteritems(): - print (' ', font, data[0], len(data[1])) + prints(' ', font, data[0], data[1], len(data[2])) print () From 473ced12de8e86ff1e302ee1b6113b46bc843ea6 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 15:05:48 +0530 Subject: [PATCH 16/34] A nicer font family chooser combobox --- src/calibre/gui2/convert/__init__.py | 5 + src/calibre/gui2/convert/lrf_output.py | 22 +--- src/calibre/gui2/convert/lrf_output.ui | 13 +- src/calibre/gui2/font_family_chooser.py | 168 ++++++++++++++++++++++++ 4 files changed, 184 insertions(+), 24 deletions(-) create mode 100644 src/calibre/gui2/font_family_chooser.py diff --git a/src/calibre/gui2/convert/__init__.py b/src/calibre/gui2/convert/__init__.py index 38fb641987..ac81816174 100644 --- a/src/calibre/gui2/convert/__init__.py +++ b/src/calibre/gui2/convert/__init__.py @@ -19,6 +19,7 @@ from calibre.ebooks.conversion.config import load_defaults, \ load_specifics, GuiRecommendations from calibre import prepare_string_for_xml from calibre.customize.ui import plugin_for_input_format +from calibre.gui2.font_family_chooser import FontFamilyChooser def config_widget_for_input_plugin(plugin): name = plugin.name.lower().replace(' ', '_') @@ -144,6 +145,8 @@ class Widget(QWidget): return ans elif isinstance(g, QFontComboBox): return unicode(QFontInfo(g.currentFont()).family()) + elif isinstance(g, FontFamilyChooser): + return g.font_family elif isinstance(g, EncodingComboBox): ans = unicode(g.currentText()).strip() try: @@ -208,6 +211,8 @@ class Widget(QWidget): getattr(g, 'setCursorPosition', lambda x: x)(0) elif isinstance(g, QFontComboBox): g.setCurrentFont(QFont(val or '')) + elif isinstance(g, FontFamilyChooser): + g.font_family = val elif isinstance(g, EncodingComboBox): if val: g.setEditText(val) diff --git a/src/calibre/gui2/convert/lrf_output.py b/src/calibre/gui2/convert/lrf_output.py index 75764164dd..a643da6ed0 100644 --- a/src/calibre/gui2/convert/lrf_output.py +++ b/src/calibre/gui2/convert/lrf_output.py @@ -6,11 +6,8 @@ __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' -from PyQt4.Qt import Qt - from calibre.gui2.convert.lrf_output_ui import Ui_Form from calibre.gui2.convert import Widget -from calibre.gui2.widgets import FontFamilyModel font_family_model = None @@ -30,13 +27,6 @@ class PluginWidget(Widget, Ui_Form): 'header_separation', 'minimum_indent'] ) self.db, self.book_id = db, book_id - global font_family_model - if font_family_model is None: - font_family_model = FontFamilyModel() - self.font_family_model = font_family_model - self.opt_serif_family.setModel(self.font_family_model) - self.opt_sans_family.setModel(self.font_family_model) - self.opt_mono_family.setModel(self.font_family_model) self.initialize_options(get_option, get_help, db, book_id) self.opt_header.toggle(), self.opt_header.toggle() @@ -44,14 +34,4 @@ class PluginWidget(Widget, Ui_Form): self.opt_render_tables_as_images.toggle() - def set_value_handler(self, g, val): - if unicode(g.objectName()) in ('opt_serif_family', - 'opt_sans_family', 'opt_mono_family'): - idx = -1 - if val: - idx = g.findText(val, Qt.MatchFixedString) - if idx < 0: - idx = 0 - g.setCurrentIndex(idx) - return True - return False + diff --git a/src/calibre/gui2/convert/lrf_output.ui b/src/calibre/gui2/convert/lrf_output.ui index ecbe673c61..753ec6110a 100644 --- a/src/calibre/gui2/convert/lrf_output.ui +++ b/src/calibre/gui2/convert/lrf_output.ui @@ -176,13 +176,13 @@ - + - + - + @@ -202,6 +202,13 @@ + + + FontFamilyChooser + QComboBox +
    calibre/gui2/font_family_chooser.h
    +
    +
    diff --git a/src/calibre/gui2/font_family_chooser.py b/src/calibre/gui2/font_family_chooser.py new file mode 100644 index 0000000000..04d9dfdfb6 --- /dev/null +++ b/src/calibre/gui2/font_family_chooser.py @@ -0,0 +1,168 @@ +#!/usr/bin/env python +# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:fdm=marker:ai +from __future__ import (unicode_literals, division, absolute_import, + print_function) + +__license__ = 'GPL v3' +__copyright__ = '2012, Kovid Goyal ' +__docformat__ = 'restructuredtext en' + +from PyQt4.Qt import (QFontInfo, QFontMetrics, Qt, QFont, QFontDatabase, QPen, + QStyledItemDelegate, QSize, QStyle, QComboBox, QStringListModel, + QDialog, QVBoxLayout, QApplication, QFontComboBox) + +from calibre.utils.icu import sort_key + +def writing_system_for_font(font): + has_latin = True + systems = QFontDatabase().writingSystems(font.family()) + + # this just confuses the algorithm below. Vietnamese is Latin with lots of + # special chars + try: + systems.remove(QFontDatabase.Vietnamese) + except ValueError: + pass + + system = QFontDatabase.Any + + if (QFontDatabase.Latin not in systems): + has_latin = False + # we need to show something + if systems: + system = systems[-1] + else: + systems.remove(QFontDatabase.Latin) + + if not systems: + return system, has_latin + + if (len(systems) == 1 and systems[0] > QFontDatabase.Cyrillic): + return systems[0], has_latin + + if (len(systems) <= 2 and + systems[-1] > QFontDatabase.Armenian and + systems[-1] < QFontDatabase.Vietnamese): + return systems[-1], has_latin + + if (len(systems) <= 5 and + systems[-1] >= QFontDatabase.SimplifiedChinese and + systems[-1] <= QFontDatabase.Korean): + system = systems[-1] + + return system, has_latin + +class FontFamilyDelegate(QStyledItemDelegate): + + def sizeHint(self, option, index): + text = index.data(Qt.DisplayRole).toString() + font = QFont(option.font) + font.setPointSize(QFontInfo(font).pointSize() * 1.5) + m = QFontMetrics(font) + return QSize(m.width(text), m.height()) + + def paint(self, painter, option, index): + text = unicode(index.data(Qt.DisplayRole).toString()) + font = QFont(option.font) + font.setPointSize(QFontInfo(font).pointSize() * 1.5) + font2 = QFont(font) + font2.setFamily(text) + + system, has_latin = writing_system_for_font(font2) + if has_latin: + font = font2 + + r = option.rect + + if option.state & QStyle.State_Selected: + painter.save() + painter.setBrush(option.palette.highlight()) + painter.setPen(Qt.NoPen) + painter.drawRect(option.rect) + painter.setPen(QPen(option.palette.highlightedText(), 0)) + + if (option.direction == Qt.RightToLeft): + r.setRight(r.right() - 4) + else: + r.setLeft(r.left() + 4) + + old = painter.font() + painter.setFont(font) + painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, text) + + if (system != QFontDatabase.Any): + w = painter.fontMetrics().width(text + " ") + painter.setFont(font2) + sample = QFontDatabase().writingSystemSample(system) + if (option.direction == Qt.RightToLeft): + r.setRight(r.right() - w) + else: + r.setLeft(r.left() + w) + painter.drawText(r, Qt.AlignVCenter|Qt.AlignLeading|Qt.TextSingleLine, sample) + + painter.setFont(old) + + if (option.state & QStyle.State_Selected): + painter.restore() + +class FontFamilyChooser(QComboBox): + + def __init__(self, parent=None): + QComboBox.__init__(self, parent) + from calibre.utils.fonts import fontconfig + try: + self.families = fontconfig.find_font_families() + except: + self.families = [] + print ('WARNING: Could not load fonts') + import traceback + traceback.print_exc() + # Restrict to Qt families as we need the font to be available in + # QFontDatabase + qt_families = set([unicode(x) for x in QFontDatabase().families()]) + self.families = list(qt_families.intersection(set(self.families))) + self.families.sort(key=sort_key) + self.families.insert(0, _('None')) + + self.m = QStringListModel(self.families) + self.setModel(self.m) + self.d = FontFamilyDelegate(self) + self.setItemDelegate(self.d) + self.setCurrentIndex(0) + + def event(self, e): + if e.type() == e.Resize: + view = self.view() + view.window().setFixedWidth(self.width() * 5/3) + return QComboBox.event(self, e) + + def sizeHint(self): + ans = QComboBox.sizeHint(self) + ans.setWidth(QFontMetrics(self.font()).width('m'*14)) + return ans + + @dynamic_property + def font_family(self): + def fget(self): + idx= self.currentIndex() + if idx == 0: return None + return self.families[idx] + def fset(self, val): + if not val: + idx = 0 + try: + idx = self.families.index(type(u'')(val)) + except ValueError: + idx = 0 + self.setCurrentIndex(idx) + return property(fget=fget, fset=fset) + + +if __name__ == '__main__': + app = QApplication([]) + d = QDialog() + d.setLayout(QVBoxLayout()) + d.layout().addWidget(FontFamilyChooser(d)) + d.layout().addWidget(QFontComboBox(d)) + d.exec_() + From 728668f9808d7670ea60320c741cfb326dcf98a4 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 16:31:16 +0530 Subject: [PATCH 17/34] Fix Foreign Policy --- recipes/foreign_policy.recipe | 33 ++++++--------------------------- 1 file changed, 6 insertions(+), 27 deletions(-) diff --git a/recipes/foreign_policy.recipe b/recipes/foreign_policy.recipe index 0d6f9984fd..893d055a05 100644 --- a/recipes/foreign_policy.recipe +++ b/recipes/foreign_policy.recipe @@ -6,40 +6,19 @@ www.foreignpolicy.com from calibre.web.feeds.news import BasicNewsRecipe -class ForeignPolicy(BasicNewsRecipe): - title = 'Foreign Policy' +class AdvancedUserRecipe1349086293(BasicNewsRecipe): + title = u'Foreign Policy' __author__ = 'Darko Miletic' description = 'International News' publisher = 'Washingtonpost.Newsweek Interactive, LLC' category = 'news, politics, USA' - oldest_article = 31 + oldest_article = 31 max_articles_per_feed = 200 - no_stylesheets = True - encoding = 'utf8' - use_embedded_content = False - language = 'en' - remove_empty_feeds = True - extra_css = ' body{font-family: Georgia,"Times New Roman",Times,serif } img{margin-bottom: 0.4em} h1,h2,h3,h4,h5,h6{font-family: Arial,Helvetica,sans-serif} ' + auto_cleanup = True - conversion_options = { - 'comment' : description - , 'tags' : category - , 'publisher' : publisher - , 'language' : language - } - - keep_only_tags = [dict(attrs={'id':['art-mast','art-body','auth-bio']})] - remove_tags = [dict(name='iframe'),dict(attrs={'id':['share-box','base-ad']})] - remove_attributes = ['height','width'] - - - feeds = [(u'Articles', u'http://www.foreignpolicy.com/node/feed')] + feeds = [(u'Foreign_Policy', u'http://www.foreignpolicy.com/node/feed')] def print_version(self, url): - return url + '?print=yes&page=full' + return url + '?print=yes&hidecomments=yes&page=full' - def preprocess_html(self, soup): - for item in soup.findAll(style=True): - del item['style'] - return soup From 87dd3258422881f5f20d1a52b1a5914bfb50ad98 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 18:52:25 +0530 Subject: [PATCH 18/34] Fix #1058531 (When editing metadata, the 'year' field is displayed like '101' instead of 2012) --- .../ebooks/conversion/plugins/pdf_output.py | 38 +++++---- src/calibre/gui2/metadata/basic_widgets.py | 21 +++-- src/calibre/utils/fonts/utils.py | 84 +++++++++++++++---- 3 files changed, 106 insertions(+), 37 deletions(-) diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index 3019255270..e9046bcfeb 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -151,28 +151,32 @@ class PDFOutput(OutputFormatPlugin): oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log) if iswindows: - from calibre.utils.fonts.utils import remove_embed_restriction + # from calibre.utils.fonts.utils import remove_embed_restriction # On windows Qt generates an image based PDF if the html uses # embedded fonts. See https://launchpad.net/bugs/1053906 for f in walk(oeb_dir): if f.rpartition('.')[-1].lower() in {'ttf', 'otf'}: - fixed = False - with open(f, 'r+b') as s: - raw = s.read() - try: - raw = remove_embed_restriction(raw) - except: - self.log.exception('Failed to remove embedding' - ' restriction from font %s, ignoring it'% - os.path.basename(f)) - else: - s.seek(0) - s.truncate() - s.write(raw) - fixed = True + os.remove(f) + # It's not the font embedding restriction that causes + # this, even after removing the restriction, Qt still + # generates an image based document. Theoretically, it + # fixed = False + # with open(f, 'r+b') as s: + # raw = s.read() + # try: + # raw = remove_embed_restriction(raw) + # except: + # self.log.exception('Failed to remove embedding' + # ' restriction from font %s, ignoring it'% + # os.path.basename(f)) + # else: + # s.seek(0) + # s.truncate() + # s.write(raw) + # fixed = True - if not fixed: - os.remove(f) + # if not fixed: + # os.remove(f) opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0] opf = OPF(opfpath, os.path.dirname(opfpath)) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index 6e764e90d5..d009cc2182 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -9,10 +9,11 @@ __docformat__ = 'restructuredtext en' import textwrap, re, os, errno, shutil -from PyQt4.Qt import (Qt, QDateTimeEdit, pyqtSignal, QMessageBox, - QIcon, QToolButton, QWidget, QLabel, QGridLayout, QApplication, - QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog, QMenu, - QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox, QAction) +from PyQt4.Qt import (Qt, QDateTimeEdit, pyqtSignal, QMessageBox, QIcon, + QToolButton, QWidget, QLabel, QGridLayout, QApplication, + QDoubleSpinBox, QListWidgetItem, QSize, QPixmap, QDialog, QMenu, + QPushButton, QSpinBox, QLineEdit, QSizePolicy, QDialogButtonBox, + QAction, QCalendarWidget, QDate) from calibre.gui2.widgets import EnLineEdit, FormatList as _FormatList, ImageView from calibre.utils.icu import sort_key @@ -1371,7 +1372,15 @@ class PublisherEdit(EditWithComplete): # {{{ # }}} -class DateEdit(QDateTimeEdit): # {{{ +# DateEdit {{{ + +class CalendarWidget(QCalendarWidget): + + def showEvent(self, ev): + if self.selectedDate().year() == UNDEFINED_DATE.year: + self.setSelectedDate(QDate.currentDate()) + +class DateEdit(QDateTimeEdit): TOOLTIP = '' LABEL = _('&Date:') @@ -1388,6 +1397,8 @@ class DateEdit(QDateTimeEdit): # {{{ fmt = self.FMT self.setDisplayFormat(fmt) self.setCalendarPopup(True) + self.cw = CalendarWidget(self) + self.setCalendarWidget(self.cw) self.setMinimumDateTime(UNDEFINED_QDATETIME) self.setSpecialValueText(_('Undefined')) self.clear_button = QToolButton(parent) diff --git a/src/calibre/utils/fonts/utils.py b/src/calibre/utils/fonts/utils.py index 6822cbe4dd..f20f238481 100644 --- a/src/calibre/utils/fonts/utils.py +++ b/src/calibre/utils/fonts/utils.py @@ -18,23 +18,23 @@ def is_truetype_font(raw): sfnt_version = raw[:4] return (sfnt_version in {b'\x00\x01\x00\x00', b'OTTO'}, sfnt_version) -def get_table(raw, name): - ''' Get the raw table bytes for the specified table in the font ''' +def get_tables(raw): num_tables = struct.unpack_from(b'>H', raw, 4)[0] offset = 4*3 # start of the table record entries - table_offset = table_checksum = table_length = table_index = table = None - name = bytes(name.lower()) for i in xrange(num_tables): - table_tag = raw[offset:offset+4] - if table_tag.lower() == name: - table_checksum, table_offset, table_length = struct.unpack_from( - b'>3L', raw, offset+4) - table_index = offset - break + table_tag, table_checksum, table_offset, table_length = struct.unpack_from( + b'>4s3L', raw, offset) + yield (table_tag, raw[table_offset:table_offset+table_length], offset, + table_offset, table_checksum) offset += 4*4 - if table_offset is not None: - table = raw[table_offset:table_offset+table_length] - return table, table_index, table_offset, table_checksum + +def get_table(raw, name): + ''' Get the raw table bytes for the specified table in the font ''' + name = bytes(name.lower()) + for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw): + if table_tag.lower() == name: + return table, table_index, table_offset, table_checksum + return None, None, None, None def get_font_characteristics(raw): ''' @@ -154,13 +154,59 @@ def get_font_names(raw): return family_name, subfamily_name, full_name +def checksum_of_block(raw): + extra = 4 - len(raw)%4 + raw += b'\0'*extra + num = len(raw)//4 + return sum(struct.unpack(b'>%dI'%num, raw)) % (1<<32) + +def verify_checksums(raw): + head_table = None + for table_tag, table, table_index, table_offset, table_checksum in get_tables(raw): + if table_tag.lower() == b'head': + version, fontrev, checksum_adj = struct.unpack_from(b'>ffL', table) + head_table = table + offset = table_offset + checksum = table_checksum + elif checksum_of_block(table) != table_checksum: + raise ValueError('The %r table has an incorrect checksum'%table_tag) + + if head_table is not None: + table = head_table + table = table[:8] + struct.pack(b'>I', 0) + table[12:] + raw = raw[:offset] + table + raw[offset+len(table):] + # Check the checksum of the head table + if checksum_of_block(table) != checksum: + raise ValueError('Checksum of head table not correct') + # Check the checksum of the entire font + checksum = checksum_of_block(raw) + q = (0xB1B0AFBA - checksum) & 0xffffffff + if q != checksum_adj: + raise ValueError('Checksum of entire font incorrect') + +def set_checksum_adjustment(f): + offset = get_table(f.getvalue(), 'head')[2] + offset += 8 + f.seek(offset) + f.write(struct.pack(b'>I', 0)) + checksum = checksum_of_block(f.getvalue()) + q = (0xB1B0AFBA - checksum) & 0xffffffff + f.seek(offset) + f.write(struct.pack(b'>I', q)) + +def set_table_checksum(f, name): + table, table_index, table_offset, table_checksum = get_table(f.getvalue(), name) + checksum = checksum_of_block(table) + if checksum != table_checksum: + f.seek(table_index + 4) + f.write(struct.pack(b'>I', checksum)) def remove_embed_restriction(raw): ok, sig = is_truetype_font(raw) if not ok: raise UnsupportedFont('Not a supported font, sfnt_version: %r'%sig) - table, table_index, table_offset = get_table(raw, 'os/2') + table, table_index, table_offset = get_table(raw, 'os/2')[:3] if table is None: raise UnsupportedFont('Not a supported font, has no OS/2 table') @@ -172,7 +218,12 @@ def remove_embed_restriction(raw): f = BytesIO(raw) f.seek(fs_type_offset + table_offset) f.write(struct.pack(b'>H', 0)) - return f.getvalue() + + set_table_checksum(f, 'os/2') + set_checksum_adjustment(f) + raw = f.getvalue() + verify_checksums(raw) + return raw def test(): import sys, os @@ -181,6 +232,9 @@ def test(): raw = open(f, 'rb').read() print (get_font_names(raw)) print (get_font_characteristics(raw)) + verify_checksums(raw) + remove_embed_restriction(raw) + if __name__ == '__main__': test() From ed5b1a218277d80ef06683df7b3868fe38f87bc3 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 19:04:50 +0530 Subject: [PATCH 19/34] ... --- src/calibre/gui2/metadata/basic_widgets.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/calibre/gui2/metadata/basic_widgets.py b/src/calibre/gui2/metadata/basic_widgets.py index d009cc2182..36605c7584 100644 --- a/src/calibre/gui2/metadata/basic_widgets.py +++ b/src/calibre/gui2/metadata/basic_widgets.py @@ -1398,6 +1398,7 @@ class DateEdit(QDateTimeEdit): self.setDisplayFormat(fmt) self.setCalendarPopup(True) self.cw = CalendarWidget(self) + self.cw.setVerticalHeaderFormat(self.cw.NoVerticalHeader) self.setCalendarWidget(self.cw) self.setMinimumDateTime(UNDEFINED_QDATETIME) self.setSpecialValueText(_('Undefined')) From ee7c2ca0ecffd6fa6747000a32db74a5d04f8e9d Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 19:10:21 +0530 Subject: [PATCH 20/34] Fix #1059585 (Kobo Glo not recognised as device) --- src/calibre/devices/kobo/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index 065dac9250..b5568a24dc 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -39,7 +39,7 @@ class KOBO(USBMS): CAN_SET_METADATA = ['collections'] VENDOR_ID = [0x2237] - PRODUCT_ID = [0x4161, 0x4163, 0x4165] + PRODUCT_ID = [0x4161, 0x4163, 0x4165, 0x4173] BCD = [0x0110, 0x0323, 0x0326] VENDOR_NAME = ['KOBO_INC', 'KOBO'] From 92eb7e7ac1e290d2e71b3ea235307f93cd3ce78c Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 19:39:23 +0530 Subject: [PATCH 21/34] HTML Input: Guess mimetype correctly for references to image files without file extensions. Fixes #1059349 (missing images on html to mobi conversion) --- src/calibre/ebooks/conversion/plugins/html_input.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/plugins/html_input.py b/src/calibre/ebooks/conversion/plugins/html_input.py index b0f897a9b5..83e707dd75 100644 --- a/src/calibre/ebooks/conversion/plugins/html_input.py +++ b/src/calibre/ebooks/conversion/plugins/html_input.py @@ -7,7 +7,7 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import re, tempfile, os +import re, tempfile, os, imghdr from functools import partial from itertools import izip from urllib import quote @@ -247,6 +247,14 @@ class HTMLInput(InputFormatPlugin): if media_type == 'text/plain': self.log.warn('Ignoring link to text file %r'%link_) return None + if media_type == self.BINARY_MIME: + # Check for the common case, images + try: + img = imghdr.what(link) + except EnvironmentError: + pass + else: + media_type = self.guess_type('dummy.'+img)[0] or self.BINARY_MIME self.oeb.log.debug('Added', link) self.oeb.container = self.DirContainer(os.path.dirname(link), From 8aa1df125ee49959ebf3caee6ba5ae108bb37332 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Mon, 1 Oct 2012 20:55:09 +0530 Subject: [PATCH 22/34] ... --- src/calibre/ebooks/conversion/plugins/html_input.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/calibre/ebooks/conversion/plugins/html_input.py b/src/calibre/ebooks/conversion/plugins/html_input.py index 83e707dd75..f00ccb9d9b 100644 --- a/src/calibre/ebooks/conversion/plugins/html_input.py +++ b/src/calibre/ebooks/conversion/plugins/html_input.py @@ -254,7 +254,8 @@ class HTMLInput(InputFormatPlugin): except EnvironmentError: pass else: - media_type = self.guess_type('dummy.'+img)[0] or self.BINARY_MIME + if img: + media_type = self.guess_type('dummy.'+img)[0] or self.BINARY_MIME self.oeb.log.debug('Added', link) self.oeb.container = self.DirContainer(os.path.dirname(link), From 1ffee5eef850a9d7a23d8101c22206a512b484e5 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 09:43:39 +0530 Subject: [PATCH 23/34] Add API to add font resources to windows --- src/calibre/utils/fonts/winfonts.cpp | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/calibre/utils/fonts/winfonts.cpp b/src/calibre/utils/fonts/winfonts.cpp index 0d991c004e..dcf99683a8 100644 --- a/src/calibre/utils/fonts/winfonts.cpp +++ b/src/calibre/utils/fonts/winfonts.cpp @@ -106,6 +106,7 @@ static PyObject* enum_font_families(PyObject *self, PyObject *args) { // }}} +// font_data() {{{ static PyObject* font_data(PyObject *self, PyObject *args) { PyObject *ans = NULL, *italic, *pyname; LOGFONTW lf; @@ -157,6 +158,23 @@ static PyObject* font_data(PyObject *self, PyObject *args) { return ans; } +// }}} + +static PyObject* add_font(PyObject *self, PyObject *args) { + PyObject *pyname, *private_font; + LPWSTR path; + int num; + + if (!PyArg_ParseTuple(args, "OO", &pyname, &private_font)) return NULL; + + path = unicode_to_wchar(pyname); + if (path == NULL) return NULL; + + num = AddFontResourceEx(path, (PyObject_IsTrue(private_font)) ? FR_PRIVATE : 0, 0); + free(path); + + return Py_BuildValue("i", num); +} static PyMethodDef winfonts_methods[] = { @@ -170,6 +188,11 @@ PyMethodDef winfonts_methods[] = { "Return the raw font data for the specified font." }, + {"add_font", add_font, METH_VARARGS, + "add_font(filename, private)\n\n" + "Add the font(s) in filename to windows. If private is True, the font will only be available to this process and will not be installed system wide. Reeturns the number of fonts added." + }, + {NULL, NULL, 0, NULL} }; From a28847970c1f8bdaf7f38026d93a8e60dbc8b9af Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 10:25:08 +0530 Subject: [PATCH 24/34] Kindle driver: Do not place files in sub-directories when sending books to the device. The new Kindle Paperwhite apparently does not like files in sub-directories --- src/calibre/devices/kindle/driver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index 7821631e85..7db4f4be17 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -294,6 +294,8 @@ class KINDLE2(KINDLE): PRODUCT_ID = [0x0002, 0x0004] BCD = [0x0100] + SUPPORTS_SUB_DIRS = False # Apparently the Paperwhite doesn't like files placed in subdirectories + SUPPORTS_SUB_DIRS_FOR_SCAN = True EXTRA_CUSTOMIZATION_MESSAGE = [ _('Send page number information when sending books') + From 412c1f49af839a7e3944438ec98a988688df34e1 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 10:27:26 +0530 Subject: [PATCH 25/34] ... --- src/calibre/gui2/wizard/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/gui2/wizard/__init__.py b/src/calibre/gui2/wizard/__init__.py index d831307d9a..784b899464 100644 --- a/src/calibre/gui2/wizard/__init__.py +++ b/src/calibre/gui2/wizard/__init__.py @@ -85,7 +85,7 @@ class Kindle(Device): output_profile = 'kindle' output_format = 'MOBI' - name = 'Kindle 1-4 and Touch' + name = 'Kindle Paperwhite/Touch/1-4' manufacturer = 'Amazon' id = 'kindle' From 65620d2a8b942261aceddbc219ed831ba2729c24 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 11:27:53 +0530 Subject: [PATCH 26/34] Revert change to not use sub-directories on the Kindle --- src/calibre/devices/kindle/driver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/calibre/devices/kindle/driver.py b/src/calibre/devices/kindle/driver.py index 7db4f4be17..988df109fc 100644 --- a/src/calibre/devices/kindle/driver.py +++ b/src/calibre/devices/kindle/driver.py @@ -294,8 +294,8 @@ class KINDLE2(KINDLE): PRODUCT_ID = [0x0002, 0x0004] BCD = [0x0100] - SUPPORTS_SUB_DIRS = False # Apparently the Paperwhite doesn't like files placed in subdirectories - SUPPORTS_SUB_DIRS_FOR_SCAN = True + # SUPPORTS_SUB_DIRS = False # Apparently the Paperwhite doesn't like files placed in subdirectories + # SUPPORTS_SUB_DIRS_FOR_SCAN = True EXTRA_CUSTOMIZATION_MESSAGE = [ _('Send page number information when sending books') + From 5a4d164347b4e04d38d4ace5d9064d70d664180f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 13:36:16 +0530 Subject: [PATCH 27/34] PDF Output: Handle embedded fonts better on linux --- .../ebooks/conversion/plugins/pdf_output.py | 113 +++++++++++++----- src/calibre/utils/fonts/win_fonts.py | 17 ++- src/calibre/utils/fonts/winfonts.cpp | 54 +++++++-- 3 files changed, 144 insertions(+), 40 deletions(-) diff --git a/src/calibre/ebooks/conversion/plugins/pdf_output.py b/src/calibre/ebooks/conversion/plugins/pdf_output.py index e9046bcfeb..da66a9be0d 100644 --- a/src/calibre/ebooks/conversion/plugins/pdf_output.py +++ b/src/calibre/ebooks/conversion/plugins/pdf_output.py @@ -15,7 +15,6 @@ from calibre.customize.conversion import OutputFormatPlugin, \ OptionRecommendation from calibre.ptempfile import TemporaryDirectory from calibre.constants import iswindows -from calibre import walk UNITS = [ 'millimeter', @@ -138,6 +137,85 @@ class PDFOutput(OutputFormatPlugin): item = oeb.manifest.ids[cover_id] self.cover_data = item.data + def handle_embedded_fonts(self): + ''' + Because of QtWebKit's inability to handle embedded fonts correctly, we + remove the embedded fonts and make them available system wide instead. + If you ever move to Qt WebKit 2.3+ then this will be unnecessary. + ''' + from calibre.ebooks.oeb.base import urlnormalize + from calibre.gui2 import must_use_qt + from calibre.utils.fonts.utils import get_font_names, remove_embed_restriction + from PyQt4.Qt import QFontDatabase, QByteArray + + # First find all @font-face rules and remove them, adding the embedded + # fonts to Qt + family_map = {} + for item in list(self.oeb.manifest): + if not hasattr(item.data, 'cssRules'): continue + remove = set() + for i, rule in enumerate(item.data.cssRules): + if rule.type == rule.FONT_FACE_RULE: + remove.add(i) + try: + s = rule.style + src = s.getProperty('src').propertyValue[0].uri + font_family = s.getProperty('font-family').propertyValue[0].value + except: + continue + path = item.abshref(src) + ff = self.oeb.manifest.hrefs.get(urlnormalize(path), None) + if ff is None: + continue + + raw = ff.data + self.oeb.manifest.remove(ff) + try: + raw = remove_embed_restriction(raw) + except: + continue + must_use_qt() + QFontDatabase.addApplicationFontFromData(QByteArray(raw)) + try: + family_name = get_font_names(raw)[0] + except: + family_name = None + if family_name: + family_map[icu_lower(font_family)] = family_name + + for i in sorted(remove, reverse=True): + item.data.cssRules.pop(i) + + # Now map the font family name specified in the css to the actual + # family name of the embedded font (they may be different in general). + for item in self.oeb.manifest: + if not hasattr(item.data, 'cssRules'): continue + for i, rule in enumerate(item.data.cssRules): + if rule.type != rule.STYLE_RULE: continue + ff = rule.style.getProperty('font-family') + if ff is None: continue + val = ff.propertyValue + for i in xrange(val.length): + k = icu_lower(val[i].value) + if k in family_map: + val[i].value = family_map[k] + + def remove_font_specification(self): + # Qt produces image based pdfs on windows when non-generic fonts are specified + # This might change in Qt WebKit 2.3+ you will have to test. + for item in self.oeb.manifest: + if not hasattr(item.data, 'cssRules'): continue + for i, rule in enumerate(item.data.cssRules): + if rule.type != rule.STYLE_RULE: continue + ff = rule.style.getProperty('font-family') + if ff is None: continue + val = ff.propertyValue + for i in xrange(val.length): + k = icu_lower(val[i].value) + if k not in {'serif', 'sans', 'sans-serif', 'sansserif', + 'monospace', 'cursive', 'fantasy'}: + val[i].value = '' + def convert_text(self, oeb_book): from calibre.ebooks.pdf.writer import PDFWriter from calibre.ebooks.metadata.opf2 import OPF @@ -145,39 +223,16 @@ class PDFOutput(OutputFormatPlugin): self.log.debug('Serializing oeb input to disk for processing...') self.get_cover_data() + if iswindows: + self.remove_font_specification() + else: + self.handle_embedded_fonts() + with TemporaryDirectory('_pdf_out') as oeb_dir: from calibre.customize.ui import plugin_for_output_format oeb_output = plugin_for_output_format('oeb') oeb_output.convert(oeb_book, oeb_dir, self.input_plugin, self.opts, self.log) - if iswindows: - # from calibre.utils.fonts.utils import remove_embed_restriction - # On windows Qt generates an image based PDF if the html uses - # embedded fonts. See https://launchpad.net/bugs/1053906 - for f in walk(oeb_dir): - if f.rpartition('.')[-1].lower() in {'ttf', 'otf'}: - os.remove(f) - # It's not the font embedding restriction that causes - # this, even after removing the restriction, Qt still - # generates an image based document. Theoretically, it - # fixed = False - # with open(f, 'r+b') as s: - # raw = s.read() - # try: - # raw = remove_embed_restriction(raw) - # except: - # self.log.exception('Failed to remove embedding' - # ' restriction from font %s, ignoring it'% - # os.path.basename(f)) - # else: - # s.seek(0) - # s.truncate() - # s.write(raw) - # fixed = True - - # if not fixed: - # os.remove(f) - opfpath = glob.glob(os.path.join(oeb_dir, '*.opf'))[0] opf = OPF(opfpath, os.path.dirname(opfpath)) diff --git a/src/calibre/utils/fonts/win_fonts.py b/src/calibre/utils/fonts/win_fonts.py index bcfa40758b..747580d45e 100644 --- a/src/calibre/utils/fonts/win_fonts.py +++ b/src/calibre/utils/fonts/win_fonts.py @@ -7,11 +7,11 @@ __license__ = 'GPL v3' __copyright__ = '2012, Kovid Goyal ' __docformat__ = 'restructuredtext en' -import os, sys +import os, sys, atexit from itertools import product -from calibre import prints -from calibre.constants import plugins +from calibre import prints, isbytestring +from calibre.constants import plugins, filesystem_encoding from calibre.utils.fonts.utils import (is_truetype_font, get_font_names, get_font_characteristics) @@ -97,6 +97,17 @@ class WinFonts(object): return ans + def add_system_font(self, path): + if isbytestring(path): + path = path.decode(filesystem_encoding) + path = os.path.abspath(path) + ret = self.w.add_system_font(path) + if ret > 0: + atexit.register(self.remove_system_font, path) + return ret + + def remove_system_font(self, path): + return self.w.remove_system_font(path) def load_winfonts(): w, err = plugins['winfonts'] diff --git a/src/calibre/utils/fonts/winfonts.cpp b/src/calibre/utils/fonts/winfonts.cpp index dcf99683a8..f678d021c4 100644 --- a/src/calibre/utils/fonts/winfonts.cpp +++ b/src/calibre/utils/fonts/winfonts.cpp @@ -161,21 +161,49 @@ static PyObject* font_data(PyObject *self, PyObject *args) { // }}} static PyObject* add_font(PyObject *self, PyObject *args) { - PyObject *pyname, *private_font; + char *data; + Py_ssize_t sz; + DWORD num = 0; + + if (!PyArg_ParseTuple(args, "s#", &data, &sz)) return NULL; + + AddFontMemResourceEx(data, sz, NULL, &num); + + return Py_BuildValue("k", num); +} + +static PyObject* add_system_font(PyObject *self, PyObject *args) { + PyObject *name; LPWSTR path; int num; - if (!PyArg_ParseTuple(args, "OO", &pyname, &private_font)) return NULL; - - path = unicode_to_wchar(pyname); + if (!PyArg_ParseTuple(args, "O", &name)) return NULL; + path = unicode_to_wchar(name); if (path == NULL) return NULL; - num = AddFontResourceEx(path, (PyObject_IsTrue(private_font)) ? FR_PRIVATE : 0, 0); + num = AddFontResource(path); + if (num > 0) + SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0); free(path); - return Py_BuildValue("i", num); } +static PyObject* remove_system_font(PyObject *self, PyObject *args) { + PyObject *name, *ok = Py_False; + LPWSTR path; + + if (!PyArg_ParseTuple(args, "O", &name)) return NULL; + path = unicode_to_wchar(name); + if (path == NULL) return NULL; + + if (RemoveFontResource(path)) { + SendMessage(HWND_BROADCAST, WM_FONTCHANGE, 0, 0); + ok = Py_True; + } + free(path); + return Py_BuildValue("O", ok); +} + static PyMethodDef winfonts_methods[] = { {"enum_font_families", enum_font_families, METH_VARARGS, @@ -189,8 +217,18 @@ PyMethodDef winfonts_methods[] = { }, {"add_font", add_font, METH_VARARGS, - "add_font(filename, private)\n\n" - "Add the font(s) in filename to windows. If private is True, the font will only be available to this process and will not be installed system wide. Reeturns the number of fonts added." + "add_font(data)\n\n" + "Add the font(s) in the data (bytestring) to windows. Added fonts are always private. Returns the number of fonts added." + }, + + {"add_system_font", add_system_font, METH_VARARGS, + "add_system_font(data)\n\n" + "Add the font(s) in the specified file to the system font tables." + }, + + {"remove_system_font", remove_system_font, METH_VARARGS, + "remove_system_font(data)\n\n" + "Remove the font(s) in the specified file from the system font tables." }, {NULL, NULL, 0, NULL} From 1c7330d465892cbfa81737436e31ad43dee52ed8 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 13:41:29 +0530 Subject: [PATCH 28/34] ... --- src/calibre/utils/fonts/__init__.py | 5 +++-- src/calibre/utils/fonts/fc.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/calibre/utils/fonts/__init__.py b/src/calibre/utils/fonts/__init__.py index c847718153..a5563acd4e 100644 --- a/src/calibre/utils/fonts/__init__.py +++ b/src/calibre/utils/fonts/__init__.py @@ -26,7 +26,8 @@ class Fonts(object): def files_for_family(self, family, normalize=True): ''' Find all the variants in the font family `family`. - Returns a dictionary of tuples. Each tuple is of the form (Full font name, path to font file). + Returns a dictionary of tuples. Each tuple is of the form (path to font + file, Full font name). The keys of the dictionary depend on `normalize`. If `normalize` is `False`, they are a tuple (slant, weight) otherwise they are strings from the set `('normal', 'bold', 'italic', 'bi', 'light', 'li')` @@ -40,7 +41,7 @@ class Fonts(object): pt = PersistentTemporaryFile('.'+ext) pt.write(data) pt.close() - ans[ft] = (name, pt.name) + ans[ft] = (pt.name, name) return ans return self.backend.files_for_family(family, normalize=normalize) diff --git a/src/calibre/utils/fonts/fc.py b/src/calibre/utils/fonts/fc.py index a79b0e1963..b6a4b1f906 100644 --- a/src/calibre/utils/fonts/fc.py +++ b/src/calibre/utils/fonts/fc.py @@ -75,7 +75,8 @@ class FontConfig(Thread): def files_for_family(self, family, normalize=True): ''' Find all the variants in the font family `family`. - Returns a dictionary of tuples. Each tuple is of the form (Full font name, path to font file). + Returns a dictionary of tuples. Each tuple is of the form (path to font + file, Full font name). The keys of the dictionary depend on `normalize`. If `normalize` is `False`, they are a tuple (slant, weight) otherwise they are strings from the set `('normal', 'bold', 'italic', 'bi', 'light', 'li')` From db13abc1d5ca2e3f62997405e55b069ed7379824 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 14:52:57 +0530 Subject: [PATCH 29/34] KF8 Output: Add the css passed in throught the extra css conversion option to the generated inline ToC. Fixes #1052343 (full justification in TOC) --- src/calibre/ebooks/mobi/writer8/toc.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/calibre/ebooks/mobi/writer8/toc.py b/src/calibre/ebooks/mobi/writer8/toc.py index 313c454535..a6a089b402 100644 --- a/src/calibre/ebooks/mobi/writer8/toc.py +++ b/src/calibre/ebooks/mobi/writer8/toc.py @@ -22,9 +22,10 @@ TEMPLATE = ''' li {{ list-style-type: none }} a {{ text-decoration: none }} a:hover {{ color: red }} + {extra_css} - +

    {title}

    @@ -64,7 +65,7 @@ class TOCAdder(object): self.log('\tGenerating in-line ToC') root = etree.fromstring(TEMPLATE.format(xhtmlns=XHTML_NS, - title=self.title)) + title=self.title, extra_css=(opts.extra_css or ''))) parent = XPath('//h:ul')(root)[0] parent.text = '\n\t' for child in self.oeb.toc: From f56b251b9fb5e2cc542e76071b9359abce530c38 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 14:54:40 +0530 Subject: [PATCH 30/34] Fix #1053466 (feature request for viewer) --- src/calibre/gui2/viewer/main.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/calibre/gui2/viewer/main.py b/src/calibre/gui2/viewer/main.py index ffc5ae2ac7..35bbdcca22 100644 --- a/src/calibre/gui2/viewer/main.py +++ b/src/calibre/gui2/viewer/main.py @@ -21,7 +21,7 @@ from calibre.gui2 import (Application, ORG_NAME, APP_UID, choose_files, info_dialog, error_dialog, open_url, available_height) from calibre.ebooks.oeb.iterator.book import EbookIterator from calibre.ebooks import DRMError -from calibre.constants import islinux, isbsd, isosx, filesystem_encoding +from calibre.constants import islinux, isbsd, filesystem_encoding from calibre.utils.config import Config, StringConfig, JSONConfig from calibre.gui2.search_box import SearchBox2 from calibre.ebooks.metadata import MetaInformation @@ -209,9 +209,7 @@ class EbookViewer(MainWindow, Ui_EbookViewer): self.view_resized_timer.timeout.connect(self.viewport_resize_finished) self.view_resized_timer.setSingleShot(True) self.resize_in_progress = False - qs = [Qt.CTRL+Qt.Key_Q] - if isosx: - qs += [Qt.CTRL+Qt.Key_W] + qs = [Qt.CTRL+Qt.Key_Q,Qt.CTRL+Qt.Key_W] self.action_quit.setShortcuts(qs) self.action_quit.triggered.connect(self.quit) self.action_focus_search = QAction(self) From e5d0bd2a890a36f239b72cafbd12a86beb0cadac Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Tue, 2 Oct 2012 15:14:10 +0530 Subject: [PATCH 31/34] Sending books by email: Allow sending to multiple email addresses at once separated by commas. Fixes #1052332 (Multiple email recipients not receiving ebooks) --- src/calibre/gui2/email.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/calibre/gui2/email.py b/src/calibre/gui2/email.py index ece6d54e26..4ecf28e519 100644 --- a/src/calibre/gui2/email.py +++ b/src/calibre/gui2/email.py @@ -101,8 +101,10 @@ class Sendmail(object): from_ = 'calibre ' with lopen(attachment, 'rb') as f: msg = compose_mail(from_, to, text, subject, f, aname) - efrom, eto = map(extract_email_address, (from_, to)) - eto = [eto] + efrom = extract_email_address(from_) + eto = [] + for x in to.split(','): + eto.append(extract_email_address(x.strip())) sendmail(msg, efrom, eto, localhost=None, verbose=1, relay=opts.relay_host, From 9562e7fc7ad748420eb46509aa5ea82211b7072f Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 3 Oct 2012 08:50:43 +0530 Subject: [PATCH 32/34] Kobo Mini USB id --- src/calibre/devices/kobo/driver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/calibre/devices/kobo/driver.py b/src/calibre/devices/kobo/driver.py index b5568a24dc..9931918a18 100644 --- a/src/calibre/devices/kobo/driver.py +++ b/src/calibre/devices/kobo/driver.py @@ -39,7 +39,7 @@ class KOBO(USBMS): CAN_SET_METADATA = ['collections'] VENDOR_ID = [0x2237] - PRODUCT_ID = [0x4161, 0x4163, 0x4165, 0x4173] + PRODUCT_ID = [0x4161, 0x4163, 0x4165, 0x4173, 0x4183] BCD = [0x0110, 0x0323, 0x0326] VENDOR_NAME = ['KOBO_INC', 'KOBO'] From fb672d32641eed4f6e709d7ee3a9d0c4c5a2d9de Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 3 Oct 2012 08:51:48 +0530 Subject: [PATCH 33/34] Fix #1060472 (Enhancement: [OK] hot on metadata book cover screen) --- src/calibre/gui2/metadata/single_download.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/calibre/gui2/metadata/single_download.py b/src/calibre/gui2/metadata/single_download.py index a59b7fb57a..11f1b4a339 100644 --- a/src/calibre/gui2/metadata/single_download.py +++ b/src/calibre/gui2/metadata/single_download.py @@ -931,6 +931,7 @@ class FullFetch(QDialog): # {{{ self.bb = QDialogButtonBox(QDialogButtonBox.Cancel|QDialogButtonBox.Ok) l.addWidget(self.bb) self.bb.rejected.connect(self.reject) + self.bb.accepted.connect(self.accept) self.next_button = self.bb.addButton(_('Next'), self.bb.AcceptRole) self.next_button.setDefault(True) self.next_button.setEnabled(False) @@ -978,6 +979,7 @@ class FullFetch(QDialog): # {{{ self.log('\n\n') self.covers_widget.start(book, self.current_cover, self.title, self.authors, caches) + self.ok_button.setFocus() def back_clicked(self): self.next_button.setVisible(True) @@ -988,6 +990,8 @@ class FullFetch(QDialog): # {{{ self.covers_widget.reset_covers() def accept(self): + if self.stack.currentIndex() == 1: + return QDialog.accept(self) # Prevent the usual dialog accept mechanisms from working pass From 030f98bec9a140f19174dc17fc7cac3cb0d95008 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Wed, 3 Oct 2012 10:34:29 +0530 Subject: [PATCH 34/34] Fix #1051135 (Cursor position after multiple deletes) --- src/calibre/gui2/actions/delete.py | 17 +++++++++++------ src/calibre/gui2/library/views.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/calibre/gui2/actions/delete.py b/src/calibre/gui2/actions/delete.py index 135591aa10..87bbc1928e 100644 --- a/src/calibre/gui2/actions/delete.py +++ b/src/calibre/gui2/actions/delete.py @@ -286,6 +286,14 @@ class DeleteAction(InterfaceAction): current_row = view.row_count() - 1 view.set_current_row(current_row) + def library_ids_deleted2(self, ids_deleted, next_id=None): + view = self.gui.library_view + current_row = None + if next_id is not None: + rmap = view.ids_to_rows([next_id]) + current_row = rmap.get(next_id, None) + self.library_ids_deleted(ids_deleted, current_row=current_row) + def delete_books(self, *args): ''' Delete selected books from device or library. @@ -325,16 +333,13 @@ class DeleteAction(InterfaceAction): 'removed from your calibre library. Are you sure?') +'

    ', 'library_delete_books', self.gui): return - ci = view.currentIndex() - row = None - if ci.isValid(): - row = ci.row() + next_id = view.next_id if len(rows) < 5: view.model().delete_books_by_id(to_delete_ids) - self.library_ids_deleted(to_delete_ids, row) + self.library_ids_deleted2(to_delete_ids, next_id=next_id) else: self.__md = MultiDeleter(self.gui, to_delete_ids, - partial(self.library_ids_deleted, current_row=row)) + partial(self.library_ids_deleted2, next_id=next_id)) # Device view is visible. else: if self.gui.stack.currentIndex() == 1: diff --git a/src/calibre/gui2/library/views.py b/src/calibre/gui2/library/views.py index f8dc83273c..5ad6a2632f 100644 --- a/src/calibre/gui2/library/views.py +++ b/src/calibre/gui2/library/views.py @@ -867,6 +867,35 @@ class BooksView(QTableView): # {{{ break return property(fget=fget, fset=fset) + @property + def next_id(self): + ''' + Return the id of the 'next' row (i.e. the first unselected row after + the current row). + ''' + ci = self.currentIndex() + if not ci.isValid(): + return None + selected_rows = frozenset([i.row() for i in self.selectedIndexes() if + i.isValid()]) + column = ci.column() + + for i in xrange(ci.row()+1, self.row_count()): + if i in selected_rows: continue + try: + return self.model().id(self.model().index(i, column)) + except: + pass + + # No unselected rows after the current row, look before + for i in xrange(ci.row()-1, -1, -1): + if i in selected_rows: continue + try: + return self.model().id(self.model().index(i, column)) + except: + pass + return None + def close(self): self._model.close()