private BufferedImage convertGreyscaleToArgb(byte[] data, BufferedImage bi) {
		// we use an optimised greyscale colour conversion, as with scanned
		// greyscale/mono documents consisting of nothing but page-size
		// images, using the ICC converter is perhaps 15 times slower than this
		// method. Using an example scanned, mainly monochrome document, on this
		// developer's machine pages took an average of 3s to render using the
		// ICC converter filter, and around 115ms using this method. We use
		// pre-calculated tables generated using the ICC converter to map
		// between
		// each possible greyscale value and its desired value in sRGB.
		// We also try to avoid going through SampleModels, WritableRasters or
		// BufferedImages as that takes about 3 times as long.
		final int[] convertedPixels = new int[getWidth() * getHeight()];
		final WritableRaster r = bi.getRaster();
		int i = 0;
		final int[] greyToArgbMap = getGreyToArgbMap(bpc);
		if (bpc == 1) {
			int calculatedLineBytes = (getWidth() + 7) / 8;
			int rowStartByteIndex;
			// avoid hitting the WritableRaster for the common 1 bpc case
			if (greyToArgbMap[0] == 0 && greyToArgbMap[1] == 0xFFFFFFFF) {
				// optimisation for common case of a direct map to full white
				// and black, using bit twiddling instead of consulting the
				// greyToArgb map
				for (int y = 0; y < getHeight(); ++y) {
					// each row is byte-aligned
					rowStartByteIndex = y * calculatedLineBytes;
					for (int x = 0; x < getWidth(); ++x) {
						final byte b = data[rowStartByteIndex + x / 8];
						final int white = b >> (7 - (x & 7)) & 1;
						// if white == 0, white - 1 will be 0xFFFFFFFF,
						// which when xored with 0xFFFFFF will produce 0
						// if white == 1, white - 1 will be 0,
						// which when xored with 0xFFFFFF will produce 0xFFFFFF
						// (ignoring the top two bytes, which are always set
						// high anyway)
						convertedPixels[i] = 0xFF000000 | ((white - 1) ^ 0xFFFFFF);
						++i;
					}
				}
			} else {
				// 1 bpc case where we can't bit-twiddle and need to consult
				// the map
				for (int y = 0; y < getHeight(); ++y) {
					rowStartByteIndex = y * calculatedLineBytes;
					for (int x = 0; x < getWidth(); ++x) {
						final byte b = data[rowStartByteIndex + x / 8];
						final int val = b >> (7 - (x & 7)) & 1;
						convertedPixels[i] = greyToArgbMap[val];
						++i;
					}
				}
			}
		} else {
			for (int y = 0; y < getHeight(); ++y) {
				for (int x = 0; x < getWidth(); ++x) {
					final int greyscale = r.getSample(x, y, 0);
					convertedPixels[i] = greyToArgbMap[greyscale];
					++i;
				}
			}
		}

		final ColorModel ccm = ColorModel.getRGBdefault();
		return new BufferedImage(ccm,
				Raster.createPackedRaster(new DataBufferInt(convertedPixels, convertedPixels.length), getWidth(),
						getHeight(), getWidth(), ((PackedColorModel) ccm).getMasks(), null),
				false, null);
	}