2424import com .koadweb .javafpdf .util .Compressor ;
2525import java .awt .color .ColorSpace ;
2626import java .awt .image .BufferedImage ;
27+ import java .io .ByteArrayInputStream ;
28+ import java .io .ByteArrayOutputStream ;
2729import java .io .File ;
28- import java .io .FileInputStream ;
2930import java .io .FileOutputStream ;
3031import java .io .IOException ;
3132import java .io .InputStream ;
3233import java .io .OutputStream ;
3334import java .io .UnsupportedEncodingException ;
35+ import java .nio .file .Files ;
3436import java .util .ArrayList ;
3537import java .util .Calendar ;
3638import java .util .HashMap ;
@@ -590,12 +592,12 @@ protected void _out(final String s) {
590592 }
591593 }
592594
593- protected Map <String , Object > _parsejpg (final File file ) {
595+ protected Map <String , Object > _parsejpg (String fileName , byte [] data ) {
594596 BufferedImage img = null ;
595597 try {
596- img = ImageIO .read (file );
598+ img = ImageIO .read (new ByteArrayInputStream ( data ) );
597599
598- Map <String , Object > image = new HashMap <String , Object >();
600+ Map <String , Object > image = new HashMap <>();
599601 image .put ("w" , Integer .valueOf (img .getWidth ()));
600602 image .put ("h" , Integer .valueOf (img .getHeight ()));
601603 String colspace ;
@@ -613,28 +615,25 @@ protected Map<String, Object> _parsejpg(final File file) {
613615 image .put ("f" , "DCTDecode" );
614616 image .put ("i" , Integer .valueOf (this .images .size () + 1 ));
615617
616- InputStream f = new FileInputStream (file );
617- byte [] data = new byte [f .available ()];
618- f .read (data , 0 , f .available ());
619- f .close ();
620- image .put ("data" , data );
618+ ByteArrayOutputStream boas = new ByteArrayOutputStream ();
619+ ImageIO .write (img , "jpg" , boas );
620+ image .put ("data" , boas .toByteArray ());
621621 return image ;
622622 } catch (IOException e ) {
623623 throw new RuntimeException (e );
624624 }
625625 }
626626
627627 /** Extract info from a PNG file */
628- protected Map <String , Object > _parsepng (final File file ) throws IOException {
629- InputStream f = new FileInputStream (file );
630- try {
628+ protected Map <String , Object > _parsepng (String fileName , byte [] imageData ) throws IOException {
629+ try (ByteArrayInputStream f = new ByteArrayInputStream (imageData )) {
631630 // Check signature
632631 char [] sig = new char [] { 137 , 'P' , 'N' , 'G' , 13 , 10 , 26 , 10 };
633632 for (int i = 0 ; i < sig .length ; i ++) {
634633 int in = f .read ();
635634 char c = (char ) in ;
636635 if (c != sig [i ]) {
637- throw new IOException ("Not a PNG file: " + file );
636+ throw new IOException ("Not a PNG file: " + fileName );
638637 }
639638 }
640639 this ._fread (f , 4 );
@@ -644,14 +643,14 @@ protected Map<String, Object> _parsepng(final File file) throws IOException {
644643 int in = f .read ();
645644 char c = (char ) in ;
646645 if (c != chunk [i ]) {
647- throw new IOException ("Not a PNG file: " + file );
646+ throw new IOException ("Not a PNG file: " + fileName );
648647 }
649648 }
650649 int w = this ._freadint (f );
651650 int h = this ._freadint (f );
652651 int bpc = f .read ();
653652 if (bpc > 8 ) {
654- throw new IOException ("16-bit depth not supported: " + file );
653+ throw new IOException ("16-bit depth not supported: " + fileName );
655654 }
656655 int ct = f .read ();
657656 String colspace ;
@@ -660,18 +659,21 @@ protected Map<String, Object> _parsepng(final File file) throws IOException {
660659 } else if (ct == 2 ) {
661660 colspace = "DeviceRGB" ;
662661 } else if (ct == 3 ) {
663- colspace = "Indexed" ;
662+ colspace = "Indexed" ;
663+ } else if (ct == 6 ) {
664+ // RGBA needs handled separately
665+ return _parsepngWithAlpha (fileName , imageData );
664666 } else {
665- throw new IOException ("Alpha channel not supported: " + file );
667+ throw new IOException ("Alpha channel not supported for grayscale PNG images : " + fileName );
666668 }
667669 if (f .read () != 0 ) {
668- throw new IOException ("Unknown compression method: " + file );
670+ throw new IOException ("Unknown compression method: " + fileName );
669671 }
670672 if (f .read () != 0 ) {
671- throw new IOException ("Unknown filter method: " + file );
673+ throw new IOException ("Unknown filter method: " + fileName );
672674 }
673675 if (f .read () != 0 ) {
674- throw new IOException ("Interlacing not supported: " + file );
676+ throw new IOException ("Interlacing not supported: " + fileName );
675677 }
676678 this ._fread (f , 4 );
677679 StringBuilder sb = new StringBuilder ();
@@ -715,7 +717,7 @@ protected Map<String, Object> _parsepng(final File file) throws IOException {
715717 }
716718 } while (f .available () > 0 );
717719 if (colspace .equals ("Indexed" ) && (pal == null )) {
718- throw new IOException ("Missing palette in " + file );
720+ throw new IOException ("Missing palette in " + fileName );
719721 }
720722 Map <String , Object > image = new HashMap <String , Object >();
721723 image .put ("w" , Integer .valueOf (w ));
@@ -729,10 +731,47 @@ protected Map<String, Object> _parsepng(final File file) throws IOException {
729731 image .put ("data" , data );
730732 image .put ("i" , Integer .valueOf (this .images .size () + 1 ));
731733 return image ;
732- } finally {
733- f .close ();
734734 }
735735 }
736+
737+ /** Parse a PNG file with an alpha channel */
738+ protected Map <String , Object > _parsepngWithAlpha (String fileName , byte [] data ) throws IOException {
739+ BufferedImage img = ImageIO .read (new ByteArrayInputStream (data ));
740+ int width = img .getWidth ();
741+ int height = img .getHeight ();
742+
743+ // PNG files with alpha channel can only have 8 or 16 bit depth
744+ // we can't handle 16 bits, so that leaves a byte. we only need grayscale for the mask image
745+
746+ int [] imgPx = img .getRGB (0 , 0 , width , height , null , 0 , width );
747+ int [] maskPx = new int [width *height ];
748+
749+ // Split alpha channel off into a grayscale image
750+ for (int i = 0 ; i < imgPx .length ; i ++) {
751+ int a = (imgPx [i ] >> 24 ) & 0xFF ; // AARRGGBB -> XXXXXXAA -> 000000AA;
752+ maskPx [i ] = a | a << 8 | a << 16 ; // 000000AA | 0000AA00 | 00AA0000 -> 00AAAAAA
753+ imgPx [i ] = imgPx [i ] & 0x00FFFFFF ; // AARRGGBB -> 00RRGGBB
754+ }
755+
756+ // out contains the original image, stripped of the alpha channel
757+ BufferedImage out = new BufferedImage (width , height , BufferedImage .TYPE_INT_RGB );
758+ out .setRGB (0 , 0 , width , height , imgPx , 0 , width );
759+
760+ // mask contains the grayscale-converted alpha channel of the original image
761+ BufferedImage mask = new BufferedImage (width , height , BufferedImage .TYPE_BYTE_GRAY );
762+ mask .setRGB (0 , 0 , width , height , maskPx , 0 , width );
763+
764+ // attempt to re-parse the image, but without the alpha channel
765+ ByteArrayOutputStream baos = new ByteArrayOutputStream ();
766+ ImageIO .write (out , "png" , baos );
767+ Map <String , Object > info = _parsepng (fileName , baos .toByteArray ());
768+
769+ // attach the alpha mask to the image info for use later on
770+ baos .reset ();
771+ ImageIO .write (mask , "png" , baos );
772+ info .put ("alphaMask" , baos .toByteArray ());
773+ return info ;
774+ }
736775
737776 protected void _putcatalog () {
738777 this ._out ("/Type /Catalog" );
@@ -796,6 +835,11 @@ protected void _putimages() {
796835 + this .images .get (file ).get ("w" ));
797836 this ._out ("/Height "
798837 + this .images .get (file ).get ("h" ));
838+
839+ if (this .images .get (file ).containsKey ("alphaMask" )) {
840+ this ._out ("/SMask " + (Integer )this .images .get ("alphaMask-" + file ).get ("n" ) + " 0 R" );
841+ }
842+
799843 if (this .images .get (file ).get ("cs" ) == "Indexed" ) {
800844 this ._out ("/ColorSpace [/Indexed /DeviceRGB "
801845 + (((byte []) this .images .get (file ).get ("pal" )).length / 3 - 1 ) + " " + (this .n + 1 ) + " 0 R]" );
@@ -1812,9 +1856,15 @@ public float getY() {
18121856 * link identifier for the image
18131857 * @throws IOException
18141858 */
1815- @ SuppressWarnings ("fallthrough" )
18161859 public void Image (final String file , final Coordinate coords , final float w , final float h , final ImageType type ,
18171860 final int link ) throws IOException {
1861+ File f = new File (file );
1862+ Image (file , Files .readAllBytes (f .toPath ()), coords , w , h , type , link , false );
1863+ }
1864+
1865+ @ SuppressWarnings ("fallthrough" )
1866+ protected void Image (final String file , byte [] data , Coordinate coords , final float w , final float h , final ImageType type ,
1867+ final int link , boolean isMask ) throws IOException {
18181868 Map <String , Object > info = null ;
18191869 if (this .images .get (file ) == null ) {
18201870 // First use of image, get info
@@ -1829,17 +1879,19 @@ public void Image(final String file, final Coordinate coords, final float w, fin
18291879 } else {
18301880 type1 = type ;
18311881 }
1832- File f = new File ( file );
1882+
18331883 switch (type1 ) {
18341884 case GIF :
18351885 // gifs: convert to png first
1836- ImageIO .write (ImageIO .read (f ), "png" , f );
1886+ ByteArrayOutputStream baos = new ByteArrayOutputStream ();
1887+ ImageIO .write (ImageIO .read (new ByteArrayInputStream (data )), "png" , baos );
1888+ data = baos .toByteArray ();
18371889 // fallthrough!
18381890 case PNG :
1839- info = this ._parsepng (f );
1891+ info = this ._parsepng (file , data );
18401892 break ;
18411893 case JPEG :
1842- info = this ._parsejpg (f );
1894+ info = this ._parsejpg (file , data );
18431895 break ;
18441896 default :
18451897 throw new IOException ("Image type not supported." );
@@ -1849,6 +1901,17 @@ public void Image(final String file, final Coordinate coords, final float w, fin
18491901 } else {
18501902 info = this .images .get (file );
18511903 }
1904+
1905+ // if the image has an alpha mask, add it separately
1906+ if (info .containsKey ("alphaMask" )) {
1907+ this .Image ("alphaMask-" + file , (byte [])info .get ("alphaMask" ), new Coordinate (0 , 0 ), 0 , 0 , ImageType .PNG , 0 , true );
1908+ }
1909+
1910+ // masks are grayscale, regardless of what it claims
1911+ if (isMask ) {
1912+ info .put ("cs" , "DeviceGray" );
1913+ }
1914+
18521915 // Automatic width and height calculation if needed
18531916 float w1 = w ;
18541917 float h1 = h ;
@@ -1863,6 +1926,15 @@ public void Image(final String file, final Coordinate coords, final float w, fin
18631926 h1 = w * ((Integer ) info .get ("h" )).floatValue ()
18641927 / ((Integer ) info .get ("w" )).floatValue ();
18651928 }
1929+
1930+ // position the mask off the page so it can't be seen
1931+ if (isMask ) {
1932+ coords = new Coordinate (
1933+ (this .currentOrientation == Orientation .PORTRAIT ? this .fwPt : this .fhPt ) + 10 ,
1934+ coords .getY ()
1935+ );
1936+ }
1937+
18661938 this ._out (String .format (Locale .ENGLISH ,
18671939 "q %.2f 0 0 %.2f %.2f %.2f cm /I%d Do Q" ,
18681940 Float .valueOf (w1 * this .k ), Float .valueOf (h1 * this .k ), Float .valueOf (coords .getX () * this .k ),
0 commit comments