From ebb462fed051673b39ea4f823156c11b88f10d9d Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Fri, 17 Jul 2026 21:48:28 -0400 Subject: [PATCH 1/2] fix(presentation): render PDF transparency-group soft-mask images (#23953) Some PDFs embed a figure inside a transparency group whose image uses a soft mask (SMask) with an ICCBased colorspace. poppler's cairo backend fails to composite this construct, so both the primary `pdftocairo -svg` conversion and the existing `pdftocairo -png` raster fallback produce a blank result. The client reports the conversion as successful, so the slide silently shows nothing (e.g. a black rectangle that never appears). Fix, in bbb-common-web: - Trigger the raster fallback when the generated SVG contains a cairo transparency-group soft mask. The blank SVG otherwise passes all existing thresholds (image/path/use counts). We gate on the count rather than : cairo emits a (alpha->luminance feColorMatrix) only for the blanking transparency-group construct, whereas plain alpha images emit with no and render fine -- so gating on would needlessly rasterize normal images. - Rasterize the fallback with pdftoppm (poppler splash backend) instead of pdftocairo -png (cairo backend); splash composites the construct correctly. `-singlefile` writes ".png", matching the existing temp-file handling. Adds a configurable `filterTagThreshold` (default 0) wired in doc-conversion.xml / bigbluebutton.properties. Also adds a Playwright regression test that uploads the reported PDF and asserts the rendered slide is not blank (dark-pixel ratio of the slide image rasterized onto a canvas). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../handlers/SvgConversionHandler.java | 28 +++++++++ .../presentation/imp/SvgImageCreatorImp.java | 36 ++++++++++-- .../playwright/core/elements.ts | 1 + .../playwright/core/media/blurImage.pdf | Bin 0 -> 39361 bytes .../presentation/presentation.spec.ts | 11 ++++ .../playwright/presentation/presentation.ts | 29 +++++++++ .../playwright/presentation/util.ts | 55 ++++++++++++++++++ .../grails-app/conf/bigbluebutton.properties | 7 +++ .../grails-app/conf/spring/doc-conversion.xml | 1 + 9 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 bigbluebutton-tests/playwright/core/media/blurImage.pdf diff --git a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/SvgConversionHandler.java b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/SvgConversionHandler.java index 3d2f7a4953ca..7d086412f08b 100644 --- a/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/SvgConversionHandler.java +++ b/bbb-common-web/src/main/java/org/bigbluebutton/presentation/handlers/SvgConversionHandler.java @@ -18,6 +18,9 @@ public class SvgConversionHandler extends AbstractCommandHandler { private static String USE_TAG_OUTPUT = " imageTagThreshold || pHandler.numberOfPaths() > pathsThreshold || pHandler.numberOfUseTags() > useTagThreshold || + pHandler.numberOfFilterTags() > filterTagThreshold || rasterizeCurrSlide) { // We need t delete the destination file as we are starting a @@ -274,9 +276,13 @@ private boolean generateSvgImage(File imagePresentationDir, UploadedPresentation log.error(" --analytics-- data={}", logStr, ioException); } - // Step 1: Convert a PDF page to PNG using a raw pdftocairo - NuProcessBuilder convertPdfToPng = createConversionProcess("-png", page, source, - tempPng.getAbsolutePath().substring(0, tempPng.getAbsolutePath().lastIndexOf('.')), false, + // Step 1: Rasterize the PDF page to PNG using poppler's splash backend (pdftoppm). + // We intentionally avoid pdftocairo (cairo backend) here: it fails to composite + // PDF transparency groups that combine a soft mask (SMask) with an ICCBased + // colorspace, producing a blank raster. The splash backend renders them correctly. + // See issue #23953. + NuProcessBuilder convertPdfToPng = createRasterizationProcess(page, source, + tempPng.getAbsolutePath().substring(0, tempPng.getAbsolutePath().lastIndexOf('.')), convPdfToSvgTimeout); Pdf2PngPageConverterHandler pngHandler = new Pdf2PngPageConverterHandler("pdf2png-" + pres.getMeetingId() + "-" + pres.getId() + "-" + page); @@ -412,12 +418,30 @@ private NuProcessBuilder createConversionProcess(String format, int page, String rawCommand += " -q -f " + String.valueOf(page) + " -l " + String.valueOf(page) + " " + source + " " + destFile; if (analyze) { - rawCommand += " && grep -oE '.png" (no page + // number suffix), matching the temp file the caller created. See issue #23953. + private NuProcessBuilder createRasterizationProcess(int page, String source, String destFileRoot, long timeout) { + String rawCommand = "pdftoppm -q -png -singlefile -r " + this.svgResolutionPpi; + + //Resize png resolution to avoid too large files + if (this.pngWidthRasterizedSlides != 0) { + rawCommand += " -scale-to-x " + this.pngWidthRasterizedSlides + " -scale-to-y -1"; + } + + rawCommand += " -f " + String.valueOf(page) + " -l " + String.valueOf(page) + " " + source + " " + destFileRoot; + + return new NuProcessBuilder(Arrays.asList("/usr/share/bbb-web/run-in-systemd.sh", timeout + "s", "/bin/sh", "-c", rawCommand)); + } + private NuProcessBuilder createDetectFontType3Process(String source, int page, long timeout) { String rawCommand = "pdffonts -f " + String.valueOf(page) + " -l " + String.valueOf(page) + " " + source; rawCommand += " | grep -m 1 'Type 3'"; @@ -494,6 +518,10 @@ public void setUseTagThreshold(long threshold) { public void setPathsThreshold(long threshold) { pathsThreshold = threshold; } + + public void setFilterTagThreshold(long threshold) { + filterTagThreshold = threshold; + } public void setSlidesGenerationProgressNotifier( SlidesGenerationProgressNotifier notifier) { diff --git a/bigbluebutton-tests/playwright/core/elements.ts b/bigbluebutton-tests/playwright/core/elements.ts index 77b27a79cc72..ed547e2f1213 100644 --- a/bigbluebutton-tests/playwright/core/elements.ts +++ b/bigbluebutton-tests/playwright/core/elements.ts @@ -339,6 +339,7 @@ export const elements = { uploadPresentationFileName: 'uploadTest.png', presentationPPTX: 'BBB.pptx', presentationTXT: 'helloWorld.txt', + blurImagePresentationFileName: 'blurImage.pdf', startScreenSharing: 'button[data-test="startScreenShare"]', stopScreenSharing: 'button[data-test="stopScreenShare"]', managePresentations: 'li[data-test="managePresentations"]', diff --git a/bigbluebutton-tests/playwright/core/media/blurImage.pdf b/bigbluebutton-tests/playwright/core/media/blurImage.pdf new file mode 100644 index 0000000000000000000000000000000000000000..f93adcb7813bbd0785221018ed35ee5e60255026 GIT binary patch literal 39361 zcmeFaXIN9)w=WC`(xiyetD>ONJ4jOyA|fCny#%E9-b+MKqzed07ZK^bx6q^+X(9=o z(1p+fgcb-lx^-{=XLHVbpZk8g_u&f(Su1nSQGR2NHdn4cx~Iq^z$-*@{nNtM0wQPo z-N2_#5+OEzHpiznBvMjre2Ug~Zsso7S346o^Lyr|j%Ma;d=JeXEZwZw1VqHyWMoKO z-CWE~>`A*Cc*i!w$~Ir=#}N#@y8H=M_bJ0XEU!-}-0fA0Pdm`BaJi#em-< z@oBg}b^FN@MMoEVtnjgaf8HU$#wYiaRuNHNQ3*i_K{jCl0bWrN2?;h4A#q+2aWU+o z1x2xM1hJI=!91~_YvKdAIJ!IiT&M;c-xC)T2UjN(EVoU)vGi*EB9~8z&CSIfD+pya z3llq6^B*eqTjRw4i^4@-p?b!D0bjbLmNwYvzWkg(_brwdHc|}?U#>ZHebwWMn;9(y z*^8K`EaY6btgqj-xI#PIs9{OtI`jF{^sTnX)0mmBJe{&9%IS*LD6p|{PEfEq5nG} z{G5V~Putq;2k68EMA`W6n_F92xv>fJOJK*wDx`~(;}3xTU{|#dyesPi0fsS40-{4*w5Fo?B!D)Z@t$8_VgqZ5a1v-A?mS?!< z&Y!(GW5SsxoT1TFfNr?4;?VN1MGB(gn~eB7lwJ-So_8@1f< zht=aeI(m-Z=B9)DOgK~L@;ai`fa((D(;gljB`&!RZp(~?1wLT^9^SX3Ch4T4-20ss zyX(Bq%o@hIn;&twLQg<3SH79c_%9~`zbtWN+Jy=#T&7DSH($kL@ zVQ;M$zIR(3A}cfwo(gciX;+zf<@Ko3>$>(yRxxWuOlxbp&_%0* z=Fw@E0F7hmZXdKJl4G?${cc3M?zG)%Azu>>&h9n66y^$Wye3Yi{rNi!Glgm!Y&cs4 zIF(bW+bl2bjAB2{Uf?7RUtP6x#m%tg<@zp$&Uo(lx%s8UUJ_&-ttI)9RyL4biCV~9 zVc}!WN9B88zFZ(r>^D2!qdb{wk+AOVY3EMOXrV>E_^zq`{S9$&s8z!YsX(3Ayk;c9 zpy3%1ohTXQQQ(OhkY-2>hNGbt)lk%lOQL zS{fPhnl{=Q#!VdV7r?eSqC4G0=LQvZh!~Ogmu*R zIM1IuAChVEmX4;AelA2eV&Nw^50SLpAQQ(#vbKkFVWKstkOhWOLP!Lz<)MN?gxvGk^=7?j0kw z;%gaf!0QyTvX9=};JB>zfJs4aR8`_Z-5u{c-tyUx$#NaO>EyqyPt#Lty66AEpAQfx zl(?NZ*HaW%5MvvUNc6p~_6C;dnF!>~-~?U)J-~cCs>M@pz0;}@m{qEksW?;FCjCN5 zA^Cm$(^pTEpN2jSnic6#zup)d7+XW7mUm~l^y~HPD-3c&+1_7kD^)6QkC~70GXumv zpoij?J9b#VH|-FcUpLoZxUs;rz_3v9`SJNSe&c(c8j{b^UYwYzCF&cQGU^#c86TPf&HA9vEdy+0#^HBUT5~hQGMi1(O|oW8+4Exx25)8QWl465 zn^rrAA!X+DIZ8v*lwy@4dsH+=zD+$buJ5nxzhJv)#UWfE($Mw3bV66RP!al0qFt)n zFUvdSfQwW*Tqit@W|9WTy~eGy{Su}V_&&ade}bpDcRQ0)W7Sc&75gP-^aI)rS6E^4+Uw4zvKFbck0-6ZzhVo5ziYVE^HZi0?DCeaMJ8 zLQY@KvLU6M=``KGn|)+`S)>GQVk#$Is~@4!@|t zB9}Zq*?*cGksdv+MRQRoQ^WwmyWZr)Eiv?y8of#^(T>qBsfpctx?!`CBXe6OR3=j9 z64(P=;Lo>ja*TKEaX7gR+gv(?q1Euk@e7H+lYGEe#MdH}Ar&O1B5=oVZ^dnGc+SfA z6?o0y8x5Y^k-WzXDPoZehSZ0ybiCZcg&eEiwVGZ5UY__oPZjRTe`>fN-Tk#25FSaR zOyjB81L#l~2B;|bDkLf>C+cwPR?I6#=tZ!7l%`>2r1rh$;gYxaby7KGZgrMqZk<=i zAfaM)%%y4}57J-!8rZ7qe2ayVm3f--B=k~S-onG;9bld30zNFRAp{+})XB9Ji8S0d zKZ?Om3@#5IyqEaNFw|73SczI`4o~fF72LfXxD_~`_vZDRteZl4gL%Ey!Ik@!Lvq(m z!b-Ueg`_!~_}YcD_~q+ATMw^mUV49LFy`S6S6z#Sw^5jfsQVGuQmzN-l2+K1Pgw%Y zGd`<-xwyD9pFHmXVeZ-PK?jIfhr_1rTkB^~U_`V% zoBjRKO^6~S(wuSHA#CcK`aAVfu(4Ie#$u;jot*ki6HlK$xm|}HwcXmJMMaU#c;Z!~ zLDW;oY}l;Y^@&)-w64GQ!Q_|S;I*dg-E6cc+=Iw-Vsg2DTPNwMuF>c~G2F0d;mg+h zt(peyRj`(czOylfb+lP@rC@G((}NNlz;MpWhm}Bv=;Xzw72le*w6U&nSo5?!Xb3m* zTnC{H%VnlUQ1{*=+oHSn9VQbqh3)-__bc_ymLMSL-r9X+Oh4cSAiJwA+I?8^8%!)$ z?2Tw!WNc^IoO74sF&riDd#Pagkx*9MKEC)B3FK_oKI=yM1*uW*F#r5z&f|>T>H)nl zy-er?G_?AIsI~7y-|6LNLut%?%sjFwCw@B-yGh9vpDmU9WBRC5QzSAx1GeNkUm(Ln zX+v&nyUEm7&VNlSE-jvd1SXakA?sZ7#+%Sbnu!IL#S(^+{%QVAYcG!@p)3vQ3Bc6= z>*cBq`=({5isOX2q9`zd|I(o$nr~{i-hUzLB#rT0`1!+iUa;?p(S-D}|J3myVIGB% z>=z&LW28=;7<}<58_}!q$A!j|vn}d_?{gIj zr~_}Shik2L7V&+1d^fK{&p^jNBjs7!IA&@C9ROV4a5`C%nA1Jko;W8rrtkIGnwQ|p z8_t_=UT`{n{g|Cy@qO>&Ir^(A^I^i?M|d|md3hN=vMGV@-3NDJjHd3<-K<-eTNm+I z1+A(RwBgBoxlK@X?@>#?JW)ynWqUARJoVKNf@Jg1ZIbM3yyprNpdB;onn_ZnzXt!yopfzSU`YM|$!cNSR7Lg6ouWzx%qY#ku`Z zZrQ-pxmT2}mx<)%aD$kGYieJo2uE;{hEXX=j3`aqpeBg(c+Q*eBg!X8Udn7NY2qNB zN4uM;s7yN7EuE>x6FcAey8by%1})8vqZcxf>Ng&Q;nYdOKgj7))zPeGYFdP~*FI~< z*`V&ZdS7B+nep}FEwA$enGIWJnKNu5ZwJE7x9|nY;q3m2k76HoUJ{oBy?V^gLiOHH zm=LoSgYV8xbL-C2Cm-(Uz6#lTSJr}OX*I*IjHhy5h~yD@cLmO#9}_2fp2Q`RG`3Ag z=5ugeU8&2xJ~_ORyLGV>*I5}x**(`aSHG}AzmqTo^)@TI(9vcF(9z;1ht*$>eOk&- zaoEZh->yRgAJm%&R9e5GYEty|yi4l0uVRJQS+$5R*S|sBmyNF`2 z{<+G-;6=c_OBj6pOzc7)ybhO>(Y?Yrc-Z6mwFVrmxAeRytBIAlEapbWOE2oSzB+iO znYavraj#^lq@@=s$Lf}Fv0#kk{cI7g*}96AR-?^E15y1i&5bVanpWRld^%|mu!8q= z4Q3-zvKbO_p&-sG-Py3IX|>r5r6CHiV@w$y8EjPNSK1Vtdd9mVvO>Kgze4uu`5xal zRU5u{zTeQB69-oPIZUrpse%l8h_8?D(-pr}Dt~=0rbjbk@vd<9Lt<)gk`Q;PSB|*% z)W{pzUDxm~(4~c!UAW@OD6J|emnTiStGO#9kY4io^@S^_IJ*O1KMWtI*|&r4H-&;R z4=jfh$%wN*AH~#N;*IpC5Bi)oN;rDFYPfp5O1l~ocH_oY7q|lxIoUIX%dUp3+P`=t zKJ~n2miZMylh%>&)cZSVr0J#~+Ie#1Qf35l=PkBlfMxW6F-Eb%k$3O}SnPqQf*kS6b8yEI$N%B#dkS^fqC$RwxKZ zSE%Ui;6r)3NaHaf`L}a2ou=1J>)5W)-xJ)pL>uqUM^?yW!myk{RL-aIL}IG6`DB^h zHX_lC;BvH#<=gYAX4xF(Wb}g)RN4wJyPgb~4O~;)ZXu`t6kkYAl`CB+;MzT=qQzq@ zC2APc4uaq4O|(!W%^q#nGp~9&C}(xClK<2Dx)_%+e7$SNtS&Srs}smAPGy%|U;9l*|h!*p16H?fwLra6jwN zhpE?ZnK{EPu9-Ps_tUmakQ^riznYg0PYoIf_v$z%7Wxi{m@7P>zYEU2PrYvJ(@mGe zZo8k~@T|zFn*v;s>E!rILFoorFCOCFYi0UR)faf~#^*8=$sq!ijGu%&o7LmEpla6+_=i^f~DLK zpZAmrt_l@0=m*(%dKi<9)aa0UFf=?f>{vIR)M4Ev+)a3{@$IJ1WuY);bG(j}w#<9# zu{VTn>b|La^Y{UQIe|HQc2s%B*LxBwb@!J)xT&W#&5dqOg(&g<*HH@0n+R=8e~ihuo6BC7(Z*znhQASIQsFSAHUvFPfjO z(NuCMr=rsPpclYnq*7R|W1ls4w`$vF(#G+d`#0v{pkd2g$gp zIUCwe8XJPwE$bWH`j>aV5ALj@YJEn1hJB{L8-8URfDLAR$aoJd1Qvh{njtu1r9yCq zd4@t^aWkhOcu{83m99rtwO>jHHyXxAk1Z zlBO7G82DR2#<4#kS;(D`I&yb%vCg4R-{^k&X7?j%Kv-5tR(BZ`S$?UbXGm$aa(Fpk zXn3?sch+FZ527&nV(#&Ld4Kl!aEr)T)dVURQ9&$v=__Yp`hd)g^mtGj>2!^+!)=Ec zhXscs2T-kK{r05oq~pVHZ|Ovx2R|)__H#<8P^pfIE?0*XcRh*DX0c}hdHZ;OmSU7f zH`+A{HdcF0Zb7%BwvE>Pr$SbQIxJ)Q1v13I>EIXO5I@kKD-wbnN5CXU1InGYihH7u zijKT7l1B}AI=J+>Kms=cz4Kdm2s{vZIXM;nFroBf#FClou4}rAiCT<`gy4c^spIBz zxW*N)`(F1oyR#pq4a^VpW>T~lww1T-z90+s4eqCqrM?^aDolsjIo!5$!IiQ0T#Qi+ z>D7M4Jiv8D`=~>SF#$cA$=mUQA}%Xqd3&aIrq=`jrs~{c9`>t2;^^8s=jpDY*|}NU zIgpBz@(pDxwQnk^X|-ZV^RI6{#4GK^GRH3T*xyXPDfF1(u~GKR55-Dr$)YA>&#h^P z9)A5~-MhTA^JeF8r*%1V2&T4?G$WAZ(%GA|D->>N^wwOE%M*$mO|I~+ps;)-^jK)u zqH(TcYsD+bwbr%z3q720TWm9fB^8#CfTYA@S$1?Sd`fVT09u++1w9c-yE#8<=Lh{%b=P?XxHOa6s--%u-){#`n%vvThpD+ zMHSH$Eqk9D-O9QOj|XdkJRkKF?7gcrOHpIh@9ZPTd#kk_LFoAXf^o?o!63#)mCA8;B&k1p}Mw58&j zr92mP?mS^W#rvSGAT@CB{_bLCuDpFn6NOlSC3-S}y&jOCT>3HgV_??%EW=*wUTyuW z`eS;>deMFxhsW!t30*eP2v(rC9O_<2CyYPGNB$Fw?UQOs(# ztNK#k3FU5kl7}j~XB)E=ZMfMqu$8qJ4dC-JvRm2wF1{7lr3T>jHnOQ-b+0=J>yL*P z#FeDi(s-@ypk5AD&yaPVm*M`y=ZXkL zC{$RX$F@h!-!SOCEw`%=yc5EO-Ms}7*Hgj$lDHCn+a>$(*f#C$8!#4 z>`2ASFt3yxsi13k`kB)G_k0I7n9mKJ_eqDOgZ51iNatu9PEvO)$Bif8?TOxeAx4@_ zq5(}yk$Wk-HF=J#6urIxPz|yK3914|`+!d5r$Q)B&J$DYKmB(I_p=e|UoqTY9d`f3 za6ifuzY7`vKO5!#?i?f$_|+=-SL@u5jxTHzml(Enjvw14refmeV*R6gkXKNgUrbc! zN2?w`8_z%g{@rT#M`PXp+(-ATRPtA}`R{JJzavm#fgjC@e|LCsJt2B>17A+wRo<20 z9NRY$0ep2fK?&`<(?YQOt=Cy|Z&2cNG7{^Y!rDI@M*mOy;{@5TeQp2lkNdN8@qf2J zPVArlIBd_||J)zQchQODKiXpdqDtvsR1H4*1!ux%GWcZzwNdunn;0a<+W8N8{W&>g^f88|- zwqJ8FDyvqI)4!6hIqaLB)-I{> z0}EZ~9F5gs3d}p<^ZH|u+Sj~mwbi00HKup=>Wi_&X zk3VPlJ?A0bM)OJQm3&5JROd@mfBU-7jV;s|GA%zfCu|PBHzCN`#LdApZM!*_Hc4{Y z0N{!{cr}+pG>Qfdw2;?7NpkpR$uNE8**jMvD-czfF|LO6GN{`qYE~kr{)52Z(-R33 z81X^8qA~JQ9yj{KcZ$=LNNboJC&7gNAwi7h%pzM&(iC6R48j;%4unm8BagNY7gIpq zmri4W7M^c?bwt*dJ6|l*I^%A$uaVf(`oqSFcBgB>zS?^muLvnd;XZK8rMYXUOF8_T zi{F5L$ndep8B?b(Eb7X*_EhX1K+8vQZO#0cvvd>zF923&c1B?`tt_KE+XRS6hsS3 z^CFOwr7*n_z^_BNj)aSA_!lodyrp9Tzd~FnO~0*{u&@MNsW*~MrC$swG zRAp)+SxqugtuMayB~kiH<9vvIx!Bh>5u_wH`In2Zr^@88>M|=QH3ERVU`wGPQ;uQ! zD}33*?F`mVbgGyUNLAs#X!8$Yx0vGDD%Lc?(tNvj2Me@8b=ETXUPPC07Gh#39biO2 zRrGhfHg4v|hUD5l=brOj{sc7w5j^e(o5(7~=i_@w?+JHcvu|?`&1sD7!`nz}hSrGr zl6a18e?M%pr=Phg#AJ(ya8<)o<781L+_T$cP-RJpf?STqjur7`QylJ$ZZ$1Q{!iHb z)XKnw7B$eJEM#kL+mH~^@_8D{=0LH)o8u#Vt??v@2gjg95IpMsija#@3at2Dj78Be zuWNnVrc?P=ZKU_X76ini=3FRywCOnKKatDvUy+TS;vMIsz!@ZJk!fhS7*_RVV^I!% z%n{6f=!*@EUlG=97fbt|+@$zY;?TUW4tD5pe-e+e*O!qNK62LWM3<^4oyOfcJnE0k zK-+KCT@Is0iR&^xgw4*%Er7Tt5jsCN+?gvZ1{JM5ER9D4SF9WDB)AO)7 zxBD`x`Ad_zZ!rX{D7Q&cDEz^nQrTQvcZw_e>E~QryRCHFTh?xNqC7tZA$BciTiy2# z^-^lGgJ6Y@kn^lIXrgO-1)jy1;fHp4ZTj^M zN<=?PWd-e0?Xnp=+_Pix%STNy6dx->^M zhxRIE*czFgizM75qyetquFbGKu0PR|mbg9&2cKXzqSwWW^kpz3DW_pfKhQS0n$TA5 z-isXbU|1G(#fKSLElo{^4rCXCx8qaEzcTHBL)3GIzm0tDFB_wkM+BN~dwkac@NY$b^l^cZ} zmvqIV8ps_+;a)OZUX(rc>6?g>8u_j+?_AJ+d$s!ok0V%h|MZas@-+Oy72kcWoIZ9? zA%Yp(&s&GA+817%a8&kcuybr)-ScVj@JG5Ljd_?K>b(sQ?~3n0SM^YzhtfmoN{y%~ z9K@swdHA{#05V;0M~`nO9pR_3XNiC+t0}kPX!3gSQk6G4DWFdJuGfjjL5lk`lr-wb z)RSwqh8m4Vx_(iFu$QJ%wX$#KsVI1rrDLvHHXL*uBf{Oy;maZzjf0Y5aDLON%%Y`R z9!)PPQrD-sw9dHK?7r#cU&=j~JdL^q_cz6G4EXZ0Ow9(iKdG^lN;mF{Vn?(!ecXzY z>RO7ONOjQvC>~Jo($~vAsM%0hd^ftY(lITBJ^oLb{@&+T@w#?aIdlpVg6;Itn1`=3 z)(Dpz4M~rjYI*nF4fz^7g4jnJj0_r?OFM(=kZ|yI?Mtwip3O&F8RNTMvYlUBogts9 z=JtVf!0o#A-K(b{2yTzxmj|x^L@m(*+nbPXu>n$EjY#;eyl$d+pMkaj3?cJ+LNq$M zhHh=<#`V)k1_Z`bEC^9>N$eK7OSkQt^UxY3Ot1Sou-Y^_p6m_>=}W!6?=;cRZ({0S z1kBT}Ho#)WUdnHRGk8y*eC@$$8{2KXIi>5-9?(eKY40VqDqB!&w?C}rQ_wg;H!N!x z5(g`FPW9GZxj?M|9x1T}iGZr zOnYv3Iai`vc+b%RE1cadOu@HKvz)J5($}RiHDfCxGHT4!6j0kHeK_Xb4C@URKTmWf zqU-R>tWP8oD7L}al`vNd$Wc3u{B$buMY~~FqEVS6rxTxJ$#m*1r^!;SX=xUzjHP}1 z;MZL|zWn0nB`}gzvZgHV;HdNe&Hfhwx3`lGae}q1b-4I8G*@k#AEQ?ardi1kugtc# zFYmed>wWebF3s>>kkyxV*DU{UcAKsXYlD^cc8!x&G6r7yy!+$k2W|yjFlBE)VWp+1 zAT+%ms00aD@qKwa3@KYwKlR^noeq!vVL>iWM;Qxamn8}aV+6jF?PU=eXfXH6ZRATYv{sUsYdeNlLYiU5Q$p@@do7kX$=d7Qm~CsnGPV zIA-XQ=7s=Zjlxs<0%nnydq3-8#{%8UwB_X;eX-fv@0PJaEiLB{6&$#dxWI69R<_TP3W@Z(<;aioc!GH?ayPiCb0t43!0d*ivI@slFsI90||1|I#38GSN2 zoofG`c7>0Fdnr zy?k2`VZTK&U|GuP_RjMkkq*Xqo*I;;2o)Pb|C)$c6Lj}*mO z*6JU3;>@-BhZk7gICHK3aVO4PtABWL=34z<<60dNb_8uimZi!F^<5S_S6tG2(r{V} z)X4%Kb$Mj!+S_Vog!w@`YeKZFjDj0vs~ikM4R28B19LF*aTz%QC&v^rCr2fGMsKzb z^L=!uBI;$D_h)G#F4Fwt88-vAQ#d^qLFx)kCy<LdNx){M#HopeiR)CED6IoZb<2hFSqyC;9-fByE~9^uWQp7d$?_BVhrK1`eC zn4^@rR(FCqx`!{{T%9ZAnM>;_D#<(bTn!U};+W@`(ar>Xhl6@*eX0GpB0uqsIwSU@ zCL;sC2A@qIXXNo)0RIgNslL;WL9}S#^$guwZcrNUEJm-{a_l>X6J~e`_c+^`j?f>C zXl5Mn{dcP_weTP3LXMdNS$&0-P{%&HG7M|uOB9tpo0G^(8U7yodA9%;yrw)3L_H3! z{Y~pXZ{D#xT`C{GGFL^-8c+*rI!14V5;}rVp3RbGL31F_aTgup(Sr@dm4CkR8?Kwv zH~F7eAwebeVT~-4Z<}n9P$ahBw906^2bOViRLZ?^ypwZM;d$ht8z8X)Ix~E zNXi-_rC;_0L3AEbv9dF9SzoI58jF+^jfDI}bS@eF(qv!N@?a!-5Odmzn^F=f>MZH% zrge;`ZbuD^1Qd1b%)?hw^NMg91d62DgQaMf1XK;rslJ@6<3#suPh)i!)L%3AEZUvn z@}DK!--)vcQl|FG{C=SAvaxjHd?-wZX}`OV54_(|Q9w97@hJ$M^&0Vn0=+CV8CD-~ zaJV@;o)foP6n!F%0V=M={m0Ju7ybjYS_E_V{=x7n1 z2`DbO*vo0$&DR+T7O0!`iB*$1IH+!lYBEZZs^*61nAI9V{9MtkkaG`gy6F1ojso5` zpSEs4K%g|I!N%>{E{BkKUsIm}oLSh^ElQEIQLFhF?TX`0vqvc(<*n05(!pzDXs?t=%lx%~_82V9Q z%YA8=;Uo=xSU%4Br6RrUQMGuZH#L5aHjxpK{mYXs_ddX!<`a?=Kw}Cyuypf6so-RTnCWmfrIb zUoEtaEK#Evtp>_BBT?J^C&>yyn%`i@`7;RXC?wn+@mNg}Y|!R6;-$@ULh31L|64_V zFZ0JNaHYVQq9SEnzQpgvu*(~a{eZ|KF%#pCAczi1EO`$QS-402!&G40LlWOUt70VT9x|)?qe0fW=hRSO*U_}EWoYNh7RGQaQZnu+CU^5d;yRc zmlo8D2G&ko*erG19I4<47*B3NZpIu|%Osmk*g1U)%Ib6PLgTe5KdDVa=bVm?^ zhXhdtjFkcp6T{Z9Q$C}QKiAS8g>-riLuO)#YxGGb3kmFTOihUG2MO8h&vhhb9BVQ;^a@*L4U&054&Zdp z!&7>j;mw9{lW=^*U<dL}p~4HUh7(>aJw8#}pF78#4D@RQiV!iUANRR95?e_!q?n!npel(m zUPo!8CJ}F%(^zje?yf5n!Ui@tSiYDL->5AbYt7nng-2WKsF2z`k;^=$Dv!7VlA57pWB?upG02wKi}1`Ri884 zItB&;{p!wpH-FyW1vmy2U>VymyT400ecDRBxB`#qTcerWqUNSjOOA>#?sdj(O=dy* zhn``C(K##-BJ8m|v(T=W&SPcfCmpDeC>Wa~5ahx#Ro#24lpNXlwHEBv^?5Ez8Fe-o ziAyDHt_e)~OdSUVB|^e#GW6yemNZ(6?2pAegWNG|x$eGCJ5i9`F_6`<;Q3Or#lc)B zCg&3g3_#|z<%*L`a9W!xzX(8(;W+ZHGNX4J9qdPd)X=o@);W<5-~P2}tTLn_X4th0 zKca*Axz6upfYWbntmUNX&Sbs&iFmNJ4$4OefdZ8V7?l`Fqxl3uIh1#BaPcYk$MI8FL60ZvOE7nQ z>6)%oVkouxF8}y44jw4$S@=y$OHNKih^FHO9t05jA>|<6;yyyGq4x35JBW^?@f;i+ zii>wQ%fRB2Ftf)hZcX*K_Qw}mle=A9@+}p#RE+BC8hju9x}66vJw2U;Wqp0U`9-4p zyidGARUXRMDhXuL5g)HpTWN=OT*nZCF%PXlP_)WO(LZGH@!q<5t;|_UhH< zW-0uR>uh<_x2+u~_c90Dr0wB?fpV5Uel=jNuMb);rw~4{*?l-z=8Po%82j)#XU>Of z8h^1?{87s%m8b7}(rRmK-=0Ucw-;?EWYpdNzHY%>WwUOZ6qn#W;Ns%qULgosbmS#{ z2@ri1s(k;x!<*!UgoO0;n3$xbQQ;uDA3AZTB@=IXS>@|vjkvfts#(*lthRPJyrOUA zeR{i`M7`<|5*~ELI!9%r-$RQRFBR@}baGx3Rke>z8ZPwSrs2_>pRcqV6%6@%ES~a; z6fwx>`;K45C9cTu<_CD?D)G364orT~>SYfW$uv)QuOZh9z}&qOqb}eadNWU@JlT9{ zezD4CxY)44@0V1&NGol}>otvxRKr&-=<{wWIXe%=nB2vPxHGGbSO+VP}6wWO^enjc-Ulpcqy<*m05SkYTafu!?z^P9CV>Qz6RKoGM3w4zB&COnULfHy`a}X3&wwXWvHT7-z22E zZdBL0hqx@pq!j~2>n$?R`Jim%Afv$vb(z#mEYyjwlatcMKfZI_n@#s8Y?SsI>Go8N zWRdLq=JofO5%fYn-@0eyLoTdL=X|JD4mp~jQPGHFR0IU$?|bW87EQ}T{HR`*@Z8`Q z6wOrE@+de`HB|Q5V-@!M_SQM>fpQL5(*I>%(l3mXUw$jPHkp?l#~HyY?b6)>mTsV3 ztg#ss=DAsXSLd~N&xlkwQs1hs*2nAcAmpC#D>dubv=rb-(TC}G7qd7MRMTW^Rdq+v ztY1=Or;ikVu~IycLdL7vuCFj@NSQN|Ug86&WVUUWqw4Xc*{gEr>=tz+rsW_#9lb-4 zMOFrSQ(p3FyP-^5rRgi|U1~|9wtW)>;WCF_$k=R10E%Wrm)78*8K42NZh%cjeELL1_`E-+g4=^fPRMqV;S=mU5tw9yN9u@v+{Au!sW}m-zCj<-dMX%0V~Z8R<_i{9L5^W5!FTdHR`{?Yz#^07cJ%kR@jVo zZ9^;)qL?M#d8&7b@(40?h!9b1?IDk(D-EO@{hznJpN;)m;Yu3rL*Zljb!QZdK*K7J zcFXZw)!G5z--vF-%h1-+vKy^5nM|e*r+w3#D(K*J65F?!8&AV(DhH_%tz3Z$v4+g7 zo05`8t6EJoYNFd_aoIan(u6=gVOx>LleRu=l1c0}=krJTNhFS|6IcH8}M=jLxwk)+Ca z&+ZS(1J_3D{7nx&cbE%tL^C}4n(YrMeDhM>B(OxTAPS3ko2joP;StJx-93EV^!)WZ zP2S2{j`<^HH7f&ymvZ`3k2C8Jj}C198TMpmbuUMiNv71(vZHeg#Qj#<{RloaDLF6V z_=b|1#cX@G639|12kC{-P_j^Gd=;*Fr(9Cg1(vj&3EQ%dZzupucx<6pwrTkc>mK|9 zfKDKUw&28jeFxc2xa$W-4GpvQrU^KY=yx!0C+Bb^a|&Q6^`QECWDKGX(0tPxnV_!z zw`gRtTFu6U=l7AGijzjag)vl0&5gfyIVLWrxs`?RrT(dT!wL7xv{t8najBF?4#E)|f$J>rwkc!YO4- zP_18kb((LpOu*s93$7Z6>82C-OnXT5?`7WD+{8?dii8d#8bN)chVrs>nSDi#> z#>Y(dHR?%OjG@C}cEuBWs2WQ?gC_Tl!DzMg=KX_??Tp{7yScfV(?zji)bPara8tXX zI-XCT-}mcgKnFn)X6FO-#gx>+_l7k_@?CvzYs}W5%;;tb>tgaLZ(~8k5-0}ti(nFTP=304#*h^@svrun)(6|SaxJ&-wRoEDINUOtbwZ;qV4+PyLy`RZAhKVcOHlN>5NEUR#Ux zIY0nK5%8o#Df}e;r~P2!-rUF(@!fhA(7}v3N{1pc%_oP-c`2Z&m9gjL@^CV<-2@_jy3A*vpYh>L zAI@-bW(Usfz?mI5vjb;#;LHx3*?}`VaApV2?7*2FIL!`V#C`?%P=`rpcz50km)M51 z&+aVkLFDp}A`%hXrZ#D#A7&?qu%XU1O&O>^`Hqe9n=!+#$m74VkCb@Ljb6J$ z?c=cDc@Gf1-dxeZSoJo1aA>Jy;zwejBBN;FYHctt*^ksfVDpofU}#)OO$kukb+u)E z4WZ>Hj&2sWCSyIBdTzczT_Oibtt2XHcDv~(^z#d{w7JAZwzT~$lBJgD!+25)HHN8@ zTKNJoAWNZy_2`DRA=rCkrecZj$MgX+Qc035P>-XtCl(>`j=}(Dp+b>iH(*2NAO`I}|ja@`G)E zAjo&OZCHOVz;A1HDUlDTC;K~D#<97*%_%F>=zIJawcHL|8`A<8a#W5rOH`hZPT$4M ziezX8cyE46+SYcM^55U@si5Uc|6MP7(!x~9B3Uzd(jTP_F=(J4#LMKbqlPnWz^;1AEKIS zo&IU0)_(~s-=&sb1a?7n7D;+KOoQeBt^i?Emxgt2m@l4yd~oi>WAJMGX!gd18#nES zuJYUzmHMt}l@#sl;*8qarYK-dj7`!X$nJ+Dhk8|tQ2Jb++WxLb-;!2HvA>tt9I!U# z{x<^*8eEWasL_omW?{l2l??9my{R`Bk9{sz_o=ExkW*B8H;ivs59e8?f8x)iX1JL0 zCOr-~;tGG8S3!GZy7tjta#xG__U_uUkn9s+tyB81OlBG0iInH!PE80!KwZIXg*z8b zf=Y&XrJ8zI<6iE-eM{F>Elk>8!V5{D4edu z7J|aU*2cMhW1OqheLDvMEk5X)rC92_#eCA&BBPKtW|mS@$?%qQ%3sRBuy7#XR#mT< z<$o1+B~VQrYxvctXsNcZF0DRWaxIIZklcH-BjTYf3W9?n%}N)-Yr2+AT*t58KnQG!&_Hxm*{(ASrf^Pidj{`1ej%s)3LGv|C= zRz+8y={^4eSNuHRZdaE9JM!kswt=Wu&kxVbzMYioU0HQ&yJkhL(w8lJ7t}I%D(*`K zC5Dde$uh|ZYytDTK@4{fI*EsCh17r<>X>>#B3?qwA|A#lCv$dcvXCc z$leDdLmz^Bz1olzY3RaKRZkjS*c*zv#cKk03J zofWDAr-6=(;}>9mmXJuhgefPdlgMysIUy)p|n%8#_;B@$0w`O`rezTUBosVPGIR zzbSX^6NkF$=DO{bEQ>|#7E9}t?sMMTa*OpoUBG?NTI1m6=&00|nw+*2^1IK;LtGOMXOwq6>d37;7x~Ze%Je^RT9}z( zyyCzm%^7umy#4pR_xx99Rc01P^o)XQ71CkwC;rIv{j2P1jTwTb0y{xAV`N&o{_?P= zt>tI$9371tnxkDsGv>~lCmm=nl?Pf!rn#4z=QTC2pB=C;W%$4-->NrydQKnuICF(Z zamG;8;%!XI{Em8lPVCu|eKqzEozsH*?iUG~i+m#^t*5)GD6Q+Xo-cB7aZy@T^84K* z%=6QN3EEo6_=IyLf#0u#FA6{PYUnl3xzrjxUlr4(@HH2bzVr6qygGb?lTYH|hO;!m!#V`pvHLoaJ5(Yg?`R zH*RkEs!-N=Z3ZnS=g(R9{%vAHcjO;dUyW#ydFGpGoR0Y<=vbL!$kF3RQ~q@uS^5o) z^^4EKm$DmXFScw;Z&#l0yC0f?NWz9+UTk|4ZS!rl;li4l>vX5pPIzL|S=XQEj;k?R zY;0syQ+xe7Ep}$UL6y9_F|TryPou@=mVHSPk;=A9xevCyB^mc#clY<->Jc6uv_TSj zeB<+3&eyc>a4&>RW|&wk#qlO1_pQ4XT)$-(c5Rh|v)mLstk5a-mPkDO?Ddt z(_;3e+&>VDR<`)Ljvw8}#>dCbC|+9c^TkXw#w;x}p*Z=>kVC2JwWqsZ>TrSIeo+6? z{mQzIj@m{aBiXDAN_l%;lwrl#FS{OOk6fu#9y-+1lZEu>Wo2e2%=3II5HzUj@!NXN z;LI1qGd~HSfrog870G%Ahw@h?NBu){IjMV5k1(;+XtRNc89_97JQeh&2fP+Xw~ord zLDTRp(Z#)WcdK`P@sk5tNFtFc+Xqi+wwbKmnIm1j5+ZShywm5&kHp=|?TMb?_Q$dc zS#{Epp}9YOZ!Yc`L?7^OzU@$a+25z}_TX&P^8ry&>_7?=*Wi1In-<*ExIHSj){w>K zuUB^De%{)f^f7taC0X`~o!B!tZqQNX@HFWZR@(_U!#UzI%4vo0H%NUiMl= zxp{vLanOfOBv@!;68_N~OOq%kJ(8%G*AHl1c(riqmARgyiA;i`K zA}kt4X0g~TgwCduaVDJsu(xc4Qs6&8YF@(&5(C%Ato4PcSeh{65ad}`yA4Oe2^9td z+tkyHMT_E2h{Qah0HI)H8U*zcg*XVfLIh$#!w$fU21zAgEGd)HtBL(zbHeDsDYf<> z6c0}?z|$HIn&W_F_(ag@4LnIGfh-9Km5O+LU>t4=j1&ojq=AG8S@QOs4~n9ENH~xL zLb47LH%JgviM~Ng&_479>|&76At6J;1cNaYZASai8?cWA4ij9ciNQA&6bpn8oY6b|t zEk^>}x3$1goQ|?_Du#nRv%FArG57*GXJOFai%dv-xQO8DqX^E>2UBZ42!l?i(h)AA z<>7RYefU8H8XlE}vcQtj;eie;oJl+?13;VU@)+#*^noC=zDXkOF_iVbA2gf__70t~D1-f;%^1ptD0n(@;4S~& zyi_KO4wj;h9F+-R@h0)cBy{CI#G^9VAY;06AV(jFAI6*<7b=TNoxqcbIJ{6oqz_Bl zjkg_axeSD&-k{Z+1l)>Y#j7h)y)T0e#WE<6&A|ik06I { await presentation.uploadOtherPresentationsFormat(); }); + test('Upload PDF with embedded soft-masked image renders (not blank)', async ({ + browser, + context, + page, + }, testInfo) => { + linkIssue(23953); + const presentation = new Presentation(browser, context); + await presentation.initPages(page, testInfo); + await presentation.blurredImagePresentationRendersTest(); + }); + // https://docs.bigbluebutton.org/3.0/testing/release-testing/#uploading-multiple-presentations-automated test('Upload multiple presentations', async ({ browser, context, page }, testInfo) => { const presentation = new Presentation(browser, context); diff --git a/bigbluebutton-tests/playwright/presentation/presentation.ts b/bigbluebutton-tests/playwright/presentation/presentation.ts index 05a25a49e34c..200a28746e67 100644 --- a/bigbluebutton-tests/playwright/presentation/presentation.ts +++ b/bigbluebutton-tests/playwright/presentation/presentation.ts @@ -14,6 +14,7 @@ import { checkSvgIndex, expectSlidesEqualBetweenPages, getCurrentPresentationHeight, + getCurrentSlideDarkPixelRatio, getSlideOuterHtml, uploadMultiplePresentations, uploadSinglePresentation, @@ -334,6 +335,34 @@ export class Presentation extends MultiUsers { } } + // Regression test for https://github.com/bigbluebutton/bigbluebutton/issues/23953 + // The PDF embeds a soft-masked (SMask) image inside a transparency group. poppler's + // cairo backend (pdftocairo) fails to composite it and produces a fully blank slide, + // while the client reports the conversion as successful. The slide must render its + // content (a large black rectangle) instead of a blank page. + async blurredImagePresentationRendersTest() { + await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME); + await this.modPage.waitForSelector(e.skipSlide); + await this.modPage.closeAllToastNotifications(); + + await uploadSinglePresentation(this.modPage, e.blurImagePresentationFileName, UPLOAD_PDF_WAIT_TIME); + await this.modPage.closeAllToastNotifications(); + + const modDarkRatio = await getCurrentSlideDarkPixelRatio(this.modPage); + expect( + modDarkRatio, + 'the uploaded PDF slide should render its embedded figure (a black rectangle), not a blank page (issue #23953)', + ).toBeGreaterThan(0.02); + + // The attendee must receive the same non-blank slide. + await this.userPage.closeAllToastNotifications(); + const userDarkRatio = await getCurrentSlideDarkPixelRatio(this.userPage); + expect( + userDarkRatio, + 'the attendee should also see the embedded figure rendered on the slide (issue #23953)', + ).toBeGreaterThan(0.02); + } + async uploadMultiplePresentationsTest() { // wait for whiteboard to load and no notifications await this.modPage.waitForSelector(e.whiteboard, ELEMENT_WAIT_LONGER_TIME); diff --git a/bigbluebutton-tests/playwright/presentation/util.ts b/bigbluebutton-tests/playwright/presentation/util.ts index 44e48dd5aa26..80ffcaf574d1 100644 --- a/bigbluebutton-tests/playwright/presentation/util.ts +++ b/bigbluebutton-tests/playwright/presentation/util.ts @@ -57,6 +57,61 @@ export async function getCurrentPresentationHeight(locator: Locator) { return locator.evaluate((element) => window.getComputedStyle(element).getPropertyValue('height')); } +// Rasterizes the current slide image (the server-generated SVG referenced by the +// tl-image background) onto an off-screen canvas and returns the fraction of +// near-black pixels. Used to detect slides that converted to a blank image, e.g. +// when an embedded figure is silently dropped during conversion (issue #23953). +export async function getCurrentSlideDarkPixelRatio(testPage: Page): Promise { + await testPage.waitForSelector(e.currentSlideImg); + const slideUrl = await testPage.page.evaluate( + ([selector]) => { + const el = document.querySelector(selector) as HTMLElement | null; + return el?.style?.backgroundImage?.split('"')[1] ?? null; + }, + [e.currentSlideImg], + ); + expect(slideUrl, 'should resolve the current slide image URL from the whiteboard').toBeTruthy(); + + // Fetch the SVG bytes through the authenticated browser context, then rasterize + // it in-page via a same-origin blob URL so the canvas is not tainted. + const response = await testPage.page.request.get(slideUrl as string); + expect(response.ok(), `should fetch the current slide image (HTTP ${response.status()})`).toBeTruthy(); + const svg = await response.text(); + + return testPage.page.evaluate(async (svgText) => { + const blob = new Blob([svgText], { type: 'image/svg+xml' }); + const url = URL.createObjectURL(blob); + try { + const img = new Image(); + await new Promise((resolve, reject) => { + img.onload = () => resolve(); + img.onerror = () => reject(new Error('failed to load slide image')); + img.src = url; + }); + const width = 400; + const height = Math.max(1, Math.round((width * (img.height || 595)) / (img.width || 842))); + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const ctx = canvas.getContext('2d'); + if (!ctx) throw new Error('failed to get canvas 2d context'); + // Composite over white so transparent regions read as white, matching the client. + ctx.fillStyle = '#ffffff'; + ctx.fillRect(0, 0, width, height); + ctx.drawImage(img, 0, 0, width, height); + const { data } = ctx.getImageData(0, 0, width, height); + let dark = 0; + const total = width * height; + for (let i = 0; i < data.length; i += 4) { + if (data[i] + data[i + 1] + data[i + 2] < 150) dark += 1; + } + return dark / total; + } finally { + URL.revokeObjectURL(url); + } + }, svg); +} + export async function getCurrentPresentationToastLocator(testPage: Page) { return testPage.page.locator(e.smallToastMsg).filter({ hasText: e.defaultCurrentPresentationLabel }); } diff --git a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties index 930f5c04f6f7..dbea89b6c662 100644 --- a/bigbluebutton-web/grails-app/conf/bigbluebutton.properties +++ b/bigbluebutton-web/grails-app/conf/bigbluebutton.properties @@ -72,6 +72,13 @@ imageTagThreshold=800 # Maximum allowed number of tags in generated svg, if exceeded the conversion will fallback to full BMP (default 10k) useTagThreshold=10000 +# Maximum allowed number of tags in generated svg, if exceeded the conversion will fallback to a rasterized +# slide (default 0). pdftocairo emits a (alpha-to-luminance) for PDF transparency groups used as soft masks +# (SMask), which some browsers render blank; rasterizing those slides keeps them visible. Plain alpha images use only +# (no ) and are unaffected. Raise this if such slides should stay vector. +# See https://github.com/bigbluebutton/bigbluebutton/issues/23953 +filterTagThreshold=0 + #------------------------------------ # Number of threads in the pool to do the presentation conversion. #------------------------------------ diff --git a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml index acfadaefc11d..0c4f79291666 100755 --- a/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml +++ b/bigbluebutton-web/grails-app/conf/spring/doc-conversion.xml @@ -107,6 +107,7 @@ with BigBlueButton; if not, see . + From e17f7eb92c49ed07b5ce9c20f5f3186b853380f9 Mon Sep 17 00:00:00 2001 From: Anton Georgiev Date: Mon, 31 Aug 2026 20:54:42 -0400 Subject: [PATCH 2/2] test(presentation): wait for the attendee slide to change before measuring uploadSinglePresentation only waits on the moderator page, so the attendee-side dark-pixel assertion could measure the previous slide and pass on its content. Snapshot the attendee slide before the upload and poll until it changes. Mirrors the review finding on the 4.0 port (bigbluebutton#25718). --- .../playwright/presentation/presentation.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/bigbluebutton-tests/playwright/presentation/presentation.ts b/bigbluebutton-tests/playwright/presentation/presentation.ts index 200a28746e67..15dae4e2986e 100644 --- a/bigbluebutton-tests/playwright/presentation/presentation.ts +++ b/bigbluebutton-tests/playwright/presentation/presentation.ts @@ -345,6 +345,11 @@ export class Presentation extends MultiUsers { await this.modPage.waitForSelector(e.skipSlide); await this.modPage.closeAllToastNotifications(); + // Snapshot the attendee's slide first: uploadSinglePresentation only waits on + // the moderator page, so the attendee assertion below must wait for the new + // slide to propagate before measuring it. + const userSlideBeforeUpload = await getSlideOuterHtml(this.userPage); + await uploadSinglePresentation(this.modPage, e.blurImagePresentationFileName, UPLOAD_PDF_WAIT_TIME); await this.modPage.closeAllToastNotifications(); @@ -356,6 +361,12 @@ export class Presentation extends MultiUsers { // The attendee must receive the same non-blank slide. await this.userPage.closeAllToastNotifications(); + await expect + .poll(() => getSlideOuterHtml(this.userPage), { + message: 'the attendee should receive the uploaded presentation slide', + timeout: ELEMENT_WAIT_LONGER_TIME, + }) + .not.toBe(userSlideBeforeUpload); const userDarkRatio = await getCurrentSlideDarkPixelRatio(this.userPage); expect( userDarkRatio,