|
3 | 3 | Project: Simba (https://github.com/MerlijnWajer/Simba) |
4 | 4 | License: GNU General Public License (https://www.gnu.org/licenses/gpl-3.0) |
5 | 5 | -------------------------------------------------------------------------- |
| 6 | + Save/Load images from/to a base64 string. |
6 | 7 |
|
7 | | - Save/Load bitmaps from/to a base64 string. |
8 | | -
|
9 | | - Strings are generated as PNG with a small header. |
| 8 | + A string is "IMG:" + base64(header + name + LZMA-compressed BGRA pixels). |
| 9 | + Version 1 stored a PNG which is still supported for reading |
10 | 10 | } |
11 | 11 | unit simba.image_stringconv; |
12 | 12 |
|
13 | | -{$i simba.inc} |
| 13 | +{$I simba.inc} |
14 | 14 |
|
15 | 15 | interface |
16 | 16 |
|
17 | 17 | uses |
18 | 18 | Classes, SysUtils, |
19 | | - simba.base, simba.image; |
| 19 | + simba.base, |
| 20 | + simba.image; |
| 21 | + |
| 22 | +procedure SimbaImage_FromString(Image: TSimbaImage; Str: String); |
| 23 | +function SimbaImage_ToString(Image: TSimbaImage): String; |
| 24 | + |
| 25 | +implementation |
| 26 | + |
| 27 | +uses |
| 28 | + FPReadPNG, |
| 29 | + simba.encoding, |
| 30 | + simba.compress, |
| 31 | + simba.image_lazbridge, |
| 32 | + simba.vartype_string; |
20 | 33 |
|
21 | 34 | const |
22 | 35 | HeaderPrefix = 'IMG:'; |
23 | 36 |
|
| 37 | + VERSION_PNG = 1; // legacy: header with fixed name String[128] then PNG data |
| 38 | + VERSION_LZMA = 2; // header + LZMA(interleaved BGRA) |
| 39 | + VERSION_LZMA_SPLIT = 3; // header + LZMA(channel-split BGRA) |
| 40 | + |
24 | 41 | type |
25 | | - TImageStringHeader = packed record |
26 | | - Version: Integer; |
27 | | - Width, Height: Integer; |
| 42 | + THeaderLegacy = packed record |
| 43 | + Version: Int32; |
| 44 | + Width, Height: Int32; |
28 | 45 | Name: String[128]; |
29 | 46 | end; |
30 | 47 |
|
31 | | -procedure SimbaImage_FromString(Image: TSimbaImage; Str: String); |
32 | | -function SimbaImage_ToString(Image: TSimbaImage): String; |
| 48 | + THeader = packed record |
| 49 | + Version: Int32; |
| 50 | + Width, Height: Int32; |
| 51 | + NameLen: Int32; // length of the name that follows the header |
| 52 | + end; |
33 | 53 |
|
34 | | -implementation |
| 54 | +function ReadName(Stream: TStream; Len: SizeInt): String; |
| 55 | +begin |
| 56 | + Result := ''; |
| 57 | + if (Len > 0) and (Stream.Position + Len <= Stream.Size) then |
| 58 | + begin |
| 59 | + SetLength(Result, Len); |
| 60 | + Stream.ReadBuffer(Result[1], Len); |
| 61 | + end; |
| 62 | +end; |
35 | 63 |
|
36 | | -uses |
37 | | - FPReadPNG, FPWritePNG, |
38 | | - simba.encoding, simba.image_lazbridge, simba.vartype_string; |
| 64 | +function SplitBGRA(Src: PByte; PixelCount: SizeInt): TByteArray; |
| 65 | +var |
| 66 | + I: SizeInt; |
| 67 | + ptrB, ptrG, ptrR, ptrA: PByte; |
| 68 | +begin |
| 69 | + SetLength(Result, PixelCount * 4); |
| 70 | + |
| 71 | + ptrB := Pointer(Result); |
| 72 | + ptrG := ptrB + PixelCount; |
| 73 | + ptrR := ptrG + PixelCount; |
| 74 | + ptrA := ptrR + PixelCount; |
| 75 | + |
| 76 | + for I := 1 to PixelCount do |
| 77 | + begin |
| 78 | + ptrB^ := Src[0]; |
| 79 | + ptrG^ := Src[1]; |
| 80 | + ptrR^ := Src[2]; |
| 81 | + ptrA^ := Src[3]; |
| 82 | + |
| 83 | + Inc(ptrB); |
| 84 | + Inc(ptrG); |
| 85 | + Inc(ptrR); |
| 86 | + Inc(ptrA); |
| 87 | + Inc(Src, 4); |
| 88 | + end; |
| 89 | +end; |
| 90 | + |
| 91 | +procedure UnsplitBGRA(Src, Dst: PByte; PixelCount: SizeInt); |
| 92 | +var |
| 93 | + I: SizeInt; |
| 94 | + ptrB, ptrG, ptrR, ptrA: PByte; |
| 95 | +begin |
| 96 | + ptrB := Src; |
| 97 | + ptrG := ptrB + PixelCount; |
| 98 | + ptrR := ptrG + PixelCount; |
| 99 | + ptrA := ptrR + PixelCount; |
| 100 | + |
| 101 | + for I := 1 to PixelCount do |
| 102 | + begin |
| 103 | + Dst[0] := ptrB^; |
| 104 | + Dst[1] := ptrG^; |
| 105 | + Dst[2] := ptrR^; |
| 106 | + Dst[3] := ptrA^; |
| 107 | + |
| 108 | + Inc(ptrB); |
| 109 | + Inc(ptrG); |
| 110 | + Inc(ptrR); |
| 111 | + Inc(ptrA); |
| 112 | + Inc(Dst, 4); |
| 113 | + end; |
| 114 | +end; |
39 | 115 |
|
40 | 116 | procedure SimbaImage_FromString(Image: TSimbaImage; Str: String); |
41 | 117 | var |
42 | | - Stream: TStringStream; |
43 | | - Header: TImageStringHeader; |
| 118 | + Stream: TBytesStream; |
| 119 | + DecompressedData: TByteArray; |
| 120 | + Header: THeader; |
| 121 | + HeaderLegacy: THeaderLegacy; |
44 | 122 | begin |
45 | 123 | if not Str.StartsWith(HeaderPrefix, True) then |
46 | | - SimbaException('TImage.FromString: Invalid string, should begin with "IMG:"'); |
| 124 | + SimbaException('TImage.FromString: Invalid string. Must start with "IMG:"'); |
47 | 125 | Str.DeleteRange(1, Length(HeaderPrefix)); |
48 | 126 |
|
49 | | - Stream := nil; |
| 127 | + Stream := TBytesStream.Create(); |
50 | 128 | try |
51 | | - Stream := TStringStream.Create(BaseDecode(EBaseEncoding.b64, Str)); |
52 | | - Stream.Read(Header, SizeOf(TImageStringHeader)); |
53 | | - |
54 | | - Image.Name := Header.Name; |
55 | | - |
56 | | - SimbaImage_FromFPImageReader(Image, TFPReaderPNG, Stream); |
| 129 | + BaseDecode(EBaseEncoding.b64, Str, Stream); |
| 130 | + if (Stream.Size < SizeOf(THeader)) then |
| 131 | + SimbaException('TImage.FromString: Invalid string. Too small'); |
| 132 | + |
| 133 | + Stream.Position := 0; |
| 134 | + Stream.Read(Header, SizeOf(THeader)); |
| 135 | + |
| 136 | + case Header.Version of |
| 137 | + VERSION_LZMA, |
| 138 | + VERSION_LZMA_SPLIT: |
| 139 | + begin |
| 140 | + Image.Name := ReadName(Stream, Header.NameLen); |
| 141 | + Image.SetSize(Header.Width, Header.Height); |
| 142 | + |
| 143 | + DecompressedData := DecompressStream(ESimbaCompressAlgo.LZMA, Stream); |
| 144 | + if (Header.Version = VERSION_LZMA_SPLIT) then |
| 145 | + UnsplitBGRA(@DecompressedData[0], PByte(Image.Data), Header.Width * Header.Height) |
| 146 | + else |
| 147 | + Move(DecompressedData[0], Image.Data^, (Header.Width * Header.Height) * SizeOf(TColorBGRA)); |
| 148 | + end; |
| 149 | + |
| 150 | + VERSION_PNG: |
| 151 | + begin |
| 152 | + Stream.Position := 0; |
| 153 | + Stream.Read(HeaderLegacy, SizeOf(THeaderLegacy)); |
| 154 | + Image.Name := HeaderLegacy.Name; |
| 155 | + SimbaImage_FromFPImageReader(Image, TFPReaderPNG, Stream); |
| 156 | + end; |
| 157 | + else |
| 158 | + SimbaException('TImage.FromString: Unsupported version (%d)', [Header.Version]); |
| 159 | + end; |
57 | 160 | finally |
58 | 161 | Stream.Free(); |
59 | 162 | end; |
60 | 163 | end; |
61 | 164 |
|
62 | 165 | function SimbaImage_ToString(Image: TSimbaImage): String; |
63 | 166 | var |
64 | | - HeaderStream, ImageStream: TStringStream; |
65 | | - Header: TImageStringHeader; |
| 167 | + Interleaved, Split, CompressedData: TByteArray; |
| 168 | + Buffer: TBytesStream; |
| 169 | + Header: THeader; |
| 170 | + PixelCount, DataSize: SizeInt; |
66 | 171 | begin |
67 | | - HeaderStream := nil; |
68 | | - ImageStream := nil; |
69 | | - |
70 | | - try |
71 | | - Header.Version := 1; |
72 | | - Header.Width := Image.Width; |
73 | | - Header.Height := Image.Height; |
74 | | - Header.Name := Image.Name; |
75 | | - |
76 | | - HeaderStream := TStringStream.Create(); |
77 | | - HeaderStream.Write(Header, SizeOf(TImageStringHeader)); |
78 | | - ImageStream := TStringStream.Create(); |
| 172 | + PixelCount := Image.Width * Image.Height; |
| 173 | + DataSize := PixelCount * SizeOf(TColorBGRA); |
| 174 | + |
| 175 | + // pick the smaller output, and store in version field |
| 176 | + Interleaved := CompressData(ESimbaCompressAlgo.LZMA, PByte(Image.Data), DataSize); |
| 177 | + Split := CompressBytes(ESimbaCompressAlgo.LZMA, SplitBGRA(PByte(Image.Data), PixelCount)); |
| 178 | + |
| 179 | + if (Length(Split) < Length(Interleaved)) then |
| 180 | + begin |
| 181 | + Header.Version := VERSION_LZMA_SPLIT; |
| 182 | + CompressedData := Split; |
| 183 | + end else |
| 184 | + begin |
| 185 | + Header.Version := VERSION_LZMA; |
| 186 | + CompressedData := Interleaved; |
| 187 | + end; |
79 | 188 |
|
80 | | - SimbaImage_ToFPImageWriter(Image, TFPWriterPNG, ImageStream); |
| 189 | + Header.Width := Image.Width; |
| 190 | + Header.Height := Image.Height; |
| 191 | + Header.NameLen := Length(Image.Name); |
81 | 192 |
|
82 | | - Result := HeaderPrefix + BaseEncode(EBaseEncoding.b64, HeaderStream.DataString + ImageStream.DataString); |
| 193 | + Buffer := TBytesStream.Create(); |
| 194 | + try |
| 195 | + Buffer.Write(Header, SizeOf(Header)); |
| 196 | + if (Length(Image.Name) > 0) then |
| 197 | + Buffer.Write(Image.Name[1], Length(Image.Name)); |
| 198 | + if (Length(CompressedData) > 0) then |
| 199 | + Buffer.Write(CompressedData[0], Length(CompressedData)); |
| 200 | + |
| 201 | + Buffer.Position := 0; |
| 202 | + Result := HeaderPrefix + BaseEncode(EBaseEncoding.b64, Buffer); |
83 | 203 | finally |
84 | | - HeaderStream.Free(); |
85 | | - ImageStream.Free(); |
| 204 | + Buffer.Free(); |
86 | 205 | end; |
87 | 206 | end; |
88 | 207 |
|
89 | 208 | end. |
90 | | - |
|
0 commit comments