/// <inheritdoc/> public void Encode(Stream output, ImageDetails image, CancellationToken cancellationToken) { using (var stream = new BeBinaryWriter(output, Encoding.Default, true)) { var h = image.Height; int size = 0; this.WriteHeader(stream, image); for (int y = 0; y < h; y++) { if (cancellationToken.IsCancellationRequested) { stream.Flush(); break; } var row = image.GetScanline(y); size += this.WritePixelScanLine(stream, image, row); } this.WriteTrailer(stream, size); } }
internal void WriteHeader(BeBinaryWriter imageOutput, ImageDetails imageDetails) { // If the image is indexed, it'll always be a single channel, otherwise PICT is always 3 Channels + Alpha var pictComponents = (imageDetails.IsIndexed ? 1 : 4); // If a source image is 24 bit, return 32 bit // Otherwise 1, 4, 8, 16 and 32 bit images supported. var pictBps = imageDetails.BitsPerPixel == 24 ? 32 : imageDetails.BitsPerPixel; // I've seen all sorts of weird things PackType packType = PackType.PackBits; if (imageDetails.BitsPerPixel == 1) { packedBytes = (imageDetails.Width / 8 + ((imageDetails.Width % 8) != 0 ? 1 : 0)); rowBytes = packedBytes; sourceRowBytes = rowBytes; packType = PackType.PackBits; } if (imageDetails.BitsPerPixel == 2) { packedBytes = (imageDetails.Width / 4 + ((imageDetails.Width % 4) != 0 ? 1 : 0)); rowBytes = packedBytes; sourceRowBytes = rowBytes; packType = PackType.PackBits; } if (imageDetails.BitsPerPixel == 4) { packedBytes = (imageDetails.Width / 2 + ((imageDetails.Width % 2) != 0 ? 1 : 0)); rowBytes = packedBytes; sourceRowBytes = rowBytes; packType = PackType.PackBits; } else if (imageDetails.BitsPerPixel == 8) { packedBytes = (int)(((imageDetails.Width * 8) / imageDetails.BitsPerPixel) + (((imageDetails.Width * 8) % imageDetails.Width) != 0 ? 1 : 0)); rowBytes = imageDetails.Width * 1; sourceRowBytes = rowBytes; packType = PackType.PackBits; } else if (imageDetails.BitsPerPixel == 16) { packedBytes = imageDetails.Width * 2; rowBytes = imageDetails.Width * 2; sourceRowBytes = imageDetails.Width * 2; packType = PackType.PackBits; } else if (imageDetails.BitsPerPixel == 24) { packedBytes = imageDetails.Width * 4; rowBytes = imageDetails.Width * 4; sourceRowBytes = imageDetails.Width * 3; packType = PackType.PackBitsRgb; } else if (imageDetails.BitsPerPixel == 32) { packedBytes = imageDetails.Width * pictComponents; rowBytes = imageDetails.Width * pictComponents; sourceRowBytes = imageDetails.Width * 4; packType = PackType.PackBitsRgb; } if (rowBytes < 8) { packType = PackType.NotPacked; } // TODO: Make 512 byte header optional // Write empty 512-byte header byte[] buf = new byte[Pict.PICT_NULL_HEADER_SIZE]; imageOutput.Write(buf); // Write out the size, leave as 0, this is ok imageOutput.WriteShort(0); // Write image frame (same as image bounds) imageOutput.WriteShort(imageDetails.Top); imageOutput.WriteShort(imageDetails.Left); imageOutput.WriteShort(imageDetails.Bottom); imageOutput.WriteShort(imageDetails.Right); // Write version, version 2 imageOutput.WriteShort(Pict.OP_VERSION); imageOutput.WriteShort(Pict.OP_VERSION_2); // Version 2 HEADER_OP, extended version. imageOutput.WriteShort(Pict.OP_HEADER_OP); imageOutput.WriteInt(Pict.HEADER_V2_EXT); // incl 2 bytes reseverd //imageOutput.WriteShort(FFEE); // FFEF or FFEE //imageOutput.WriteShort(0x0); // Reservered // Original Horizontal Resolution in Pixels / Inch // Image resolution, 72 dpi (long) imageOutput.WriteShort(72); imageOutput.WriteShort(0); // Original Verticale Resolution in Pixels / Inch imageOutput.WriteShort(72); imageOutput.WriteShort(0); // Optimal source rectangle (same as image bounds) // Frame at original resolution imageOutput.WriteShort(imageDetails.Top); imageOutput.WriteShort(imageDetails.Left); imageOutput.WriteShort(imageDetails.Bottom); imageOutput.WriteShort(imageDetails.Right); // Reserved (4 bytes) imageOutput.WriteInt(0); // ------------------ END OF HEADER -------------------- // // This is where things get weird...depending on bpp // Set the clip rectangle imageOutput.WriteShort(Pict.OP_CLIP_RGN); imageOutput.WriteShort(10); imageOutput.WriteShort(imageDetails.Top); // top imageOutput.WriteShort(imageDetails.Left); // left imageOutput.WriteShort(imageDetails.Bottom); imageOutput.WriteShort(imageDetails.Right); if (imageDetails.IsIndexed) { if (imageDetails.BitsPerPixel == 1) { imageOutput.WriteShort(Pict.OP_BITS_RECT); imageOutput.WriteShort(rowBytes); // Write bounds rectangle (same as image bounds) imageOutput.WriteShort(imageDetails.Top); // top imageOutput.WriteShort(imageDetails.Left); // left imageOutput.WriteShort(imageDetails.Bottom); // TODO: Handle overflow? // bottom imageOutput.WriteShort(imageDetails.Right); // right // Source and dest rect (both are same as image bounds) imageOutput.WriteShort(imageDetails.Top); imageOutput.WriteShort(imageDetails.Left); imageOutput.WriteShort(imageDetails.Bottom); imageOutput.WriteShort(imageDetails.Right); // Dest Rect imageOutput.WriteShort(imageDetails.Top); imageOutput.WriteShort(imageDetails.Left); imageOutput.WriteShort(imageDetails.Bottom); imageOutput.WriteShort(imageDetails.Right); // Transfer mode imageOutput.WriteShort(0); imageOutput.Flush(); return; } else { imageOutput.WriteShort(Pict.OP_PACK_BITS_RECT); // Packbits // The offset in bytes from one row of the image to the next. The value must be even, less than $4000, and for best performance it should be a multiple of 4. // The high 2 bits of rowBytes are used as flags. If bit 15 = 1, the data structure pointed to is a PixMap record; otherwise it is a BitMap record. imageOutput.WriteUShort((ushort)(rowBytes | 0x8000)); } } else // RGB Image { // Pixmap operation imageOutput.WriteShort(Pict.OP_DIRECT_BITS_RECT); // 0x9A - sometimes called pict9a // PixMap pointer (always 0x000000FF); imageOutput.WriteInt(0xff); // I see conflicting things about writing out row bytes at this point... imageOutput.WriteUShort((ushort)(rowBytes | 0x8000)); } // Write bounds rectangle (same as image bounds) imageOutput.WriteShort(imageDetails.Top); // top imageOutput.WriteShort(imageDetails.Left); // left imageOutput.WriteShort(imageDetails.Bottom); // TODO: Handle overflow? // bottom imageOutput.WriteShort(imageDetails.Right); // right // PixMap record version // The version number of Color QuickDraw that created this PixMap record. // The value of pmVersion is normally 0. If pmVersion is 4, Color QuickDraw treats the PixMap record's baseAddr field as 32-bit clean. // (All other flags are private.) Most applications never need to set this field. imageOutput.WriteShort(0); // Packing format (always 4: PackBits) // * 0 is default indexed packing. // * 1 is no packing (rowBytes < 8) // * 2 // * 3 // * 4 is default direct packing - run length encoded scan lines by component, red first. imageOutput.WriteShort(packType); // Size of packed data (leave as 0) // The size of the packed image in bytes. When the packType field contains the value 0, this field is always set to 0 imageOutput.WriteInt(0); // Pixmap resolution, 72 dpi //imageOutput.WriteShort(Pict.MAC_DEFAULT_DPI+0.5); imageOutput.WriteShort(72); imageOutput.WriteShort(0); //imageOutput.WriteShort(Pict.MAC_DEFAULT_DPI+0.5); imageOutput.WriteShort(72); imageOutput.WriteShort(0); // Pixel type // The storage format for a pixel image. Indexed pixels are indicated by a value of 0. // Direct pixels are specified by a value of RGBDirect, or 16. // In the PixMap record of the GDevice record (described in the chapter "Graphics Devices") // for a direct device, this field is set to the constant RGBDirect when the screen depth is set. if (imageDetails.IsIndexed) { imageOutput.WriteShort(0); } else { imageOutput.WriteShort(16); } // Pixel size // Pixel depth; that is, the number of bits used to represent a pixel. // Indexed pixels can have sizes of 1, 2, 4, and 8 bits; direct pixel sizes are 16 and 32 bits. imageOutput.WriteShort(pictBps); // Pixel component count (planes) - ie 1 for indexed, 3 or 4 for RGB imageOutput.WriteShort(pictComponents); // Pixel component size /* * The size in bits of each component for a pixel. Color QuickDraw expects that the sizes of all components * are the same, and that the value of the cmpCount field multiplied by the value of the cmpSize field * is less than or equal to the value in the pixelSize field. * For an indexed pixel value, which has only one component, the value of the cmpSize field is the same * as the value of the pixelSize field--that is, 1, 2, 4, or 8. * For direct pixels there are two additional possibilities: * A 16-bit pixel, which has three components, has a cmpSize value * of 5. This leaves an unused high-order bit, which Color QuickDraw sets to 0. * A 32-bit pixel, which has three components (red, green, and blue), has a cmpSize value of 8. * This leaves an unused high-order byte, which Color QuickDraw sets to 0. */ if (imageDetails.IsIndexed) { imageOutput.WriteShort(pictBps); } else if (imageDetails.BitsPerPixel == 16) { imageOutput.WriteShort(5); } else { imageOutput.WriteShort(8); } // PlaneBytes, ignored for now imageOutput.WriteInt(0); // TODO: Allow IndexColorModel? // ColorTable record (for RGB direct pixels, just write 0) // Pixmap Colour Table (seems to always be 0?) imageOutput.WriteInt(0); //imageOutput.WriteInt(0x101F); // Reserved (4 bytes) imageOutput.WriteInt(0); // Write out ColorTable if (imageDetails.IsIndexed) { imageOutput.WriteInt(0); // color seed - Resource ID // Colour Flags Flags. A value of $0000 identifies this as a color table for a pixel map. A value of $8000 identifies this as a color table for an indexed device. imageOutput.WriteShort(0); // Entry count / Size. One less than the number of color specification entries in the rest of this resource. imageOutput.WriteShort((ushort)(imageDetails.Palette.Length - 1)); for (ushort i = 0; i < imageDetails.Palette.Length; i++) { imageOutput.WriteShort(i); // pixel value // Each colour is a 16bit value...scale to short imageOutput.WriteUShort(ScaleQuantumToShort(imageDetails.Palette[i].R)); imageOutput.WriteUShort(ScaleQuantumToShort(imageDetails.Palette[i].G)); imageOutput.WriteUShort(ScaleQuantumToShort(imageDetails.Palette[i].B)); } } // Source and dest rect (both are same as image bounds) imageOutput.WriteShort(imageDetails.Top); imageOutput.WriteShort(imageDetails.Left); imageOutput.WriteShort(imageDetails.Bottom); imageOutput.WriteShort(imageDetails.Right); // Dest Rect imageOutput.WriteShort(imageDetails.Top); imageOutput.WriteShort(imageDetails.Left); imageOutput.WriteShort(imageDetails.Bottom); imageOutput.WriteShort(imageDetails.Right); // Transfer mode if (imageDetails.IsIndexed) { imageOutput.WriteShort(0); } else { imageOutput.WriteShort(/*QuickDraw.SRC_COPY*/ 0x40); } imageOutput.Flush(); // Now write image data }
/// <inheritdoc/> public void Encode(Stream output, ImageDetails image) => this.Encode(output, image, CancellationToken.None);
/// <summary> /// Writes and compresses (if needed) each scan line /// </summary> /// <param name="imageOutput"></param> /// <param name="imageDetails"></param> /// <param name="scanLine"></param> /// <returns></returns> internal int WritePixelScanLine(BeBinaryWriter imageOutput, ImageDetails imageDetails, byte[] scanLine) { int xOffset = 0; var pixels = scanLine; int w = imageDetails.IsIndexed ? packedBytes : imageDetails.Width; uint bytesPerPixel = imageDetails.IsIndexed ? 1 : imageDetails.BitsPerPixel / 8; uint pixelsPerByte = 8 / imageDetails.BitsPerPixel; byte[] scanlineBytes = new byte[rowBytes]; bool invert = imageDetails.BitsPerPixel == 1; // Mask for Pixel Packing based on bits per pixel int packMask = (1 << (int)imageDetails.BitsPerPixel) - 1; // Masks for RGB555 (16bit) encoding int redMask = 0x7C00; int greenMask = 0x3E0; int blueMask = 0x1F; int byteCount = 0; // Treat the scanline. for (int x = 0; x < w; x++) { var colorIndex = x * bytesPerPixel; if (imageDetails.IsIndexed) { // 8 Bit images if (pixelsPerByte == 1) { scanlineBytes[x] = pixels[x]; } else { // Pack 1, 2, 4 bit image palette entries into a single byte int shift = 8 - (int)imageDetails.BitsPerPixel; byte packedPixel = 0; xOffset = x * (int)pixelsPerByte; // Calculate the reamining pixels from the end of the buffer int remaining = 0; byte pixel; if (xOffset >= pixels.Length - pixelsPerByte) { remaining = (int)pixelsPerByte - (pixels.Length % (int)pixelsPerByte); } ; for (int j = 0; j < pixelsPerByte - remaining; j++) { pixel = pixels[xOffset + j]; // 1bpp images are inverted, that is zero is white. if (invert) { pixel = (byte)~pixel; // reverse the pixel value } packedPixel = (byte)(packedPixel | ((byte)(pixel & packMask) << shift)); shift -= (int)imageDetails.BitsPerPixel; } scanlineBytes[x] = packedPixel; } } else if (imageDetails.BitsPerPixel == 16) { // not working yet, not sure if it's the header or what... var pixelShort = (short)((pixels[colorIndex + 0] << 8) + pixels[colorIndex + 1]); byte r = (byte)(((pixelShort & redMask) >> 10) << 3); byte g = (byte)(((pixelShort & greenMask) >> 5) << 3); byte b = (byte)((pixelShort & blueMask) << 3); var color = new PaletteEntry(255, r, g, b); // this is the colour at this location, but we can probably just copy it back out? scanlineBytes[colorIndex] = pixels[colorIndex + 1]; scanlineBytes[colorIndex + 1] = pixels[colorIndex]; } else // True color { PaletteEntry color; if (imageDetails.BitsPerPixel == 32) { color = new PaletteEntry(pixels[colorIndex + 3], pixels[colorIndex + 2], pixels[colorIndex + 1], pixels[colorIndex + 0]); } else //(imageDetails.BitsPerPixel == 24) { color = new PaletteEntry(255, pixels[colorIndex + 2], pixels[colorIndex + 1], pixels[colorIndex + 0]); } // Write out as rows of pixels, 1 per channel scanlineBytes[xOffset + x] = color.A; scanlineBytes[xOffset + w + x] = color.R; scanlineBytes[xOffset + 2 * w + x] = color.G; scanlineBytes[xOffset + 3 * w + x] = color.B; } } // Pack using PackBitsEncoder // https://web.archive.org/web/20080705155158/http://developer.apple.com/technotes/tn/tn1023.html // Small images aren't compressed if (rowBytes < 8) { byteCount += scanlineBytes.Length; imageOutput.BaseStream.Write(scanlineBytes, 0, scanlineBytes.Length); } // TODO Check the compression setting else // Pack the bytes { var packedBytes = PackBitsCompressor.PackBits(scanlineBytes); //var packedBytes = new PackBitsEncoder().compress(scanlineBytes); if (rowBytes > 250) // apple says 250, ImageMagick has 200...hmm { imageOutput.WriteShort((short)packedBytes.Length); byteCount += 2; } else { imageOutput.Write((byte)packedBytes.Length); byteCount++; } byteCount += packedBytes.Length; imageOutput.BaseStream.Write(packedBytes, 0, packedBytes.Length); } imageOutput.Flush(); return(byteCount); }