From 372388b7ede7df4eddb4a05c4a80d66723c4bb34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jacobo=20Aragunde=20P=C3=A9rez?= Date: Sat, 17 Jan 2026 13:47:09 +0100 Subject: [PATCH] Support rendering transparent bitmaps in presentations. (#990) * Support rendering transparent bitmaps in presentations. Add PictureShape.getAlpha() method and implementations for HSLF and XSLF. Then make use of it in DrawPictureShape to apply the right alpha value to the picture being drawn. Fixed a bug in BitmapImageRenderer that considered alpha value 0 as "fully opaque", when it means "fully transparent" instead. Finally, added a test for this feature in TestDrawPictureShape for XSLF. A test for HSLF could not be created because it was not possible to generate a test file with today's tools; MS Office removes the bitmap transparency effect when saving as .ppt, and LibreOffice blends it into the bitmap. * Address reviewer comments. * Add comment about default alpha value. * Prevent NPE in XSLFPictureShape.getAlpha(). * Change wording in comments to avoid the word "percentage". * Use static vars for extreme alpha values. --- .../poi/xslf/usermodel/XSLFPictureShape.java | 8 +++++ .../sl/tests/draw/TestDrawPictureShape.java | 32 +++++++++++++++++- .../poi/hslf/usermodel/HSLFPictureShape.java | 6 +++- .../poi/sl/draw/BitmapImageRenderer.java | 3 +- .../apache/poi/sl/draw/DrawPictureShape.java | 7 ++++ .../apache/poi/sl/usermodel/PictureShape.java | 11 ++++++ test-data/slideshow/picture-transparency.pptx | Bin 0 -> 11793 bytes 7 files changed, 64 insertions(+), 3 deletions(-) create mode 100644 test-data/slideshow/picture-transparency.pptx diff --git a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java index 80b5d4a6ce..aec5faccf6 100644 --- a/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java +++ b/poi-ooxml/src/main/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java @@ -225,6 +225,14 @@ public class XSLFPictureShape extends XSLFSimpleShape POIXMLUnits.parsePercent(r.xgetR())); } + public int getAlpha() { + CTBlip blip = getBlip(); + if (blip == null) { + return FULLY_OPAQUE_ALPHA_VALUE; + } + return blip.sizeOfAlphaModFixArray() > 0 ? POIXMLUnits.parsePercent(blip.getAlphaModFixArray(0).xgetAmt()) : FULLY_OPAQUE_ALPHA_VALUE; + } + /** * Add a SVG image reference * @param svgPic a previously imported svg image diff --git a/poi-ooxml/src/test/java/org/apache/poi/sl/tests/draw/TestDrawPictureShape.java b/poi-ooxml/src/test/java/org/apache/poi/sl/tests/draw/TestDrawPictureShape.java index 9f5568c4c4..0bd9414d8b 100644 --- a/poi-ooxml/src/test/java/org/apache/poi/sl/tests/draw/TestDrawPictureShape.java +++ b/poi-ooxml/src/test/java/org/apache/poi/sl/tests/draw/TestDrawPictureShape.java @@ -22,8 +22,9 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assumptions.assumeFalse; -import java.awt.Dimension; +import java.awt.*; import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; import java.io.IOException; import java.io.InputStream; @@ -117,4 +118,33 @@ class TestDrawPictureShape { return val; } } + + @Test + void testAlphaXSLFPictureShape() throws IOException { + SlideShow ss = openSampleDocument("picture-transparency.pptx"); + + // First slide contains a fully opaque bitmap + verifySlideFirstPixelColor(ss.getSlides().get(0), new Color(0, 0, 0, 255)); + // Second slide contains a 20% transparency bitmap (255*0.8=204) + verifySlideFirstPixelColor(ss.getSlides().get(1), new Color(0, 0, 0, 204)); + // Third slide contains a 60% transparency bitmap (255*0.4=102) + verifySlideFirstPixelColor(ss.getSlides().get(2), new Color(0, 0, 0, 102)); + // Fourth slide contains a fully transparent bitmap + verifySlideFirstPixelColor(ss.getSlides().get(3), new Color(0, 0, 0, 0)); + } + + private void verifySlideFirstPixelColor(Slide slide, Color color) { + PictureShape picShape = null; + for (Shape shape : slide.getShapes()) { + if (shape instanceof PictureShape) { + picShape = (PictureShape)shape; + break; + } + } + assertNotNull(picShape); + BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB); + new DrawPictureShape(picShape).draw(img.createGraphics()); + assertEquals(Transparency.TRANSLUCENT, img.getTransparency()); + assertEquals(color, new Color(img.getRGB(0, 0), true)); + } } diff --git a/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPictureShape.java b/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPictureShape.java index e10a54cb55..ac2f12bd2f 100644 --- a/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPictureShape.java +++ b/poi-scratchpad/src/main/java/org/apache/poi/hslf/usermodel/HSLFPictureShape.java @@ -228,4 +228,8 @@ public class HSLFPictureShape extends HSLFSimpleShape implements PictureShape, P extends TextParagraph > extends SimpleShape { + public static final int FULLY_TRANSPARENT_ALPHA_VALUE = 0; + public static final int FULLY_OPAQUE_ALPHA_VALUE = 100000; + /** * Returns the picture data for this picture. * @@ -47,4 +50,12 @@ public interface PictureShape< * @return the clipping rectangle, which is given in percent in relation to the image width/height */ Insets getClipping(); + + /** + * Returns alpha value in a range between 0 and 100000. + * Value 0 represents complete transparency and 100000 represents complete opacity. + * @return alpha value in the range 0..100000. + * @since 6.0.0 + */ + int getAlpha(); } diff --git a/test-data/slideshow/picture-transparency.pptx b/test-data/slideshow/picture-transparency.pptx new file mode 100644 index 0000000000000000000000000000000000000000..41121f7ddea28546a0faa8342ddc54c00b402c5a GIT binary patch literal 11793 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 JX{!6*{vS+Kgunm* literal 0 HcmV?d00001