Skip to content

Commit cd595ce

Browse files
author
Austin Hyde
committed
Add support for PNG images with an alpha channel
1 parent 9d2368f commit cd595ce

File tree

1 file changed

+100
-28
lines changed

1 file changed

+100
-28
lines changed

src/main/java/com/koadweb/javafpdf/FPDF.java

Lines changed: 100 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,15 @@
2424
import com.koadweb.javafpdf.util.Compressor;
2525
import java.awt.color.ColorSpace;
2626
import java.awt.image.BufferedImage;
27+
import java.io.ByteArrayInputStream;
28+
import java.io.ByteArrayOutputStream;
2729
import java.io.File;
28-
import java.io.FileInputStream;
2930
import java.io.FileOutputStream;
3031
import java.io.IOException;
3132
import java.io.InputStream;
3233
import java.io.OutputStream;
3334
import java.io.UnsupportedEncodingException;
35+
import java.nio.file.Files;
3436
import java.util.ArrayList;
3537
import java.util.Calendar;
3638
import 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

Comments
 (0)