加密、哈希和数据打包:CryptEncode

MQL5 中负责数据加密、哈希和压缩的函数是 CryptEncode。它通过指定的方法将所传递源数组 data 的数据转换到目标数组 result 中。

int CryptEncode(ENUM_CRYPT_METHOD method, const uchar &data[], const uchar &key[], uchar &result[])

加密方法还需要传递一个带有私有(秘密)密钥的字节数组 key:其长度取决于具体方法,并在上一节的 ENUM_CRYPT_METHOD 方法表格中指定。如果 key 数组的大小更大,则仍将仅使用所需数量的第一个字节作为密钥。

哈希或压缩不需要密钥,但对于 CRYPT_ARCH_ZIP 有一个注意事项。事实是,终端内置的 "deflate" 算法的实现在结果数据中添加了几个字节以控制完整性:最初的 2 个字节包含 "deflate" 算法的设置,末尾的 4 个字节包含 Adler32 校验和。由于此特性,生成的打包容器与 ZIP 存档为存档的每个单独元素生成的容器不同(ZIP 标准在其头部存储含义相似的 CRC32)。因此,为了能够基于 CryptEncode 函数打包的数据创建和读取兼容的 ZIP 存档,MQL5 允许你使用 key 数组中的特殊值来禁用其自身的完整性检查和额外字节的生成。

uchar key[] = {1000};
CryptEncode(CRYPT_ARCH_ZIPdatakeyresult);

可以使用任何长度至少为 4 字节的密钥。获得的 result 数组可以根据标准 ZIP 格式添加标题(这个问题超出了本书的范围),以创建其他程序可以访问的存档。

函数返回放置在目标数组中的字节数,如果出错则返回 0。错误代码将照常存储在 _LastError 中。

我们使用 CryptEncode.mq5 脚本来检查函数性能。它允许用户输入文本 (Text) 或指定一个文件 (File) 进行处理。要使用文件,你需要清除 Text 字段。

你可以选择一个特定的 Method,或者一次性遍历所有方法,以直观地查看和比较不同的结果。对于这样的审查循环,请在 Method 参数中保留默认值 _CRYPT_ALL。

顺便说一句,为了引入这样的功能,我们再次需要扩展标准枚举(这次是 ENUM_CRYPT_METHOD),但由于 MQL5 中的枚举不能像类那样被继承,这里实际上声明了一个新的枚举 ENUM_CRYPT_METHOD_EXT。这样做的一个额外好处是,我们为元素添加了更友好的名称(在注释中,带有将在设置对话框中显示的提示)。

enum ENUM_CRYPT_METHOD_EXT
{
   _CRYPT_ALL = 0xFF,                      // Try All in a Loop
   _CRYPT_DES = CRYPT_DES,                 // DES    (key required, 7 bytes)
   _CRYPT_AES128 = CRYPT_AES128,           // AES128 (key required, 16 bytes)
   _CRYPT_AES256 = CRYPT_AES256,           // AES256 (key required, 32 bytes)
   _CRYPT_HASH_MD5 = CRYPT_HASH_MD5,       // MD5
   _CRYPT_HASH_SHA1 = CRYPT_HASH_SHA1,     // SHA1
   _CRYPT_HASH_SHA256 = CRYPT_HASH_SHA256// SHA256
   _CRYPT_ARCH_ZIP = CRYPT_ARCH_ZIP,       // ZIP
   _CRYPT_BASE64 = CRYPT_BASE64,           // BASE64
};
   
input string Text = "Let's encrypt this message"// Text (empty to process File)
input string File = "MQL5Book/clock10.htm"// File (used only if Text is empty)
input ENUM_CRYPT_METHOD_EXT Method = _CRYPT_ALL;

默认情况下,Text 参数填充了要加密的消息。你可以将其替换为自己的内容。如果我们清除 Text,程序将处理文件。至少有一个参数(TextFile)应包含信息。

由于加密需要密钥,其他两个选项允许你直接以文本形式输入它(尽管密钥不必是文本,并且可以包含任何二进制数据,但输入中不支持这些数据),或根据加密方法生成所需的长度。

enum DUMMY_KEY_LENGTH
{
   DUMMY_KEY_0 = 0,   // 0 bytes (no key)
   DUMMY_KEY_7 = 7,   // 7 bytes (sufficient for DES)
   DUMMY_KEY_16 = 16// 16 bytes (sufficient for AES128)
   DUMMY_KEY_32 = 32// 32 bytes (sufficient for AES256)
   DUMMY_KEY_CUSTOM,  // use CustomKey
};
   
input DUMMY_KEY_LENGTH GenerateKey = DUMMY_KEY_CUSTOM// GenerateKey (length, or from CustomKey)
input string CustomKey = "My top secret key is very strong";

最后,有一个选项 DisableCRCinZIP 用于启用 ZIP 兼容模式,这仅影响 CRYPT_ARCH_ZIP 方法。

input bool DisableCRCinZIP = false;

为了简化检查方法是否需要加密密钥或是否计算哈希(不可逆的单向转换),定义了 2 个宏。

#defineKEY_REQUIRED(C) ((C) ==CRYPT_DES|| (C) ==CRYPT_AES128|| (C) ==CRYPT_AES256)

#defineIS_HASH(C) ((C) ==CRYPT_HASH_MD5|| (C) ==CRYPT_HASH_SHA1|| (C) ==CRYPT_HASH_SHA256)

OnStart 的开头部分包含了所需变量和数组的描述。

void OnStart()
{
   ENUM_CRYPT_METHOD method = 0;
   int methods[];           // here we will collect all the elements of ENUM_CRYPT_METHOD for looping over them
   uchar key[] = {};        // empty by default: suitable for hashing, zip, base64
   uchar zip[], opt[] = {1000}; // "options" for zip
   uchar data[], result[];  // initial data and result

根据 GenerateKey 设置,我们从 CustomKey 字段获取密钥,或者仅仅用单调递增的整数值填充 key 数组。实际上,密钥应该是一个秘密的、非平凡的、任意选择的值块。

   if(GenerateKey == DUMMY_KEY_CUSTOM)
   {
      if(StringLen(CustomKey))
      {
         PRTF(CustomKey);
         StringToCharArray(CustomKeykey0, -1, CP_UTF8);
         ArrayResize(keyArraySize(key) - 1);
      }
   }
   else if(GenerateKey != DUMMY_KEY_0)
   {
      ArrayResize(keyGenerateKey);
      for(int i = 0i < GenerateKey; ++ikey[i] = (uchar)i;
   }

此处及下文,请注意在 StringToCharArray 之后使用 ArrayResize。务必将数组大小减少 1 个元素,因为如果 StringToCharArray 函数将字符串转换为字节数组时包含了末尾的 0,这可能会破坏程序的预期执行。特别是在这种情况下,我们的秘密密钥中会有一个额外的零字节,如果在接收端没有使用带有类似人为处理的程序,那么它将无法解密消息。这种额外的零也可能影响与数据交换协议的兼容性(如果 MQL 程序与“外部世界”进行某种集成)。

接下来,我们以十六进制格式记录下最终密钥的原始表示:这是由 ByteArrayPrint 函数完成的,该函数曾在 以简化模式读写文件一节中使用过。

   if(ArraySize(key))
   {   
      Print("Key (bytes):");
      ByteArrayPrint(key);
   }
   else
   {
      Print("Key is not provided");
   }

根据 TextFile 是否可用,我们用文本字符或文件内容填充 data 数组。

   if(StringLen(Text))
   {
      PRTF(Text);
      PRTF(StringToCharArray(Textdata0, -1, CP_UTF8));
      ArrayResize(dataArraySize(data) - 1);
   }
   else if(StringLen(File))
   {
      PRTF(File);
      if(PRTF(FileLoad(Filedata)) <= 0)
      {
         return// error
      }
   }

最后,我们遍历所有方法或使用特定方法执行一次转换。

   const int n = (Method == _CRYPT_ALL) ?
      EnumToArray(methodmethods0UCHAR_MAX) : 1;
   ResetLastError();
   for(int i = 0i < n; ++i)
   {
      method = (ENUM_CRYPT_METHOD)((Method == _CRYPT_ALL) ? methods[i] : Method);
      Print("- "i" "EnumToString(method), ", key required: ",
         KEY_REQUIRED(method));
      
      if(method == CRYPT_ARCH_ZIP)
      {
         if(DisableCRCinZIP)
         {
            ArrayCopy(zipopt); // array with additional option dynamic for ArraySwap
         }
         ArraySwap(keyzip); // change key to empty or option
      }
      
      if(PRTF(CryptEncode(methoddatakeyresult)))
      {
         if(StringLen(Text))
         {
            // code page Latin (Western) to unify the display for all users
            Print(CharArrayToString(result0WHOLE_ARRAY1252));
            ByteArrayPrint(result);
            if(method != CRYPT_BASE64)
            {
               const uchar dummy[] = {};
               uchar readable[];
               if(PRTF(CryptEncode(CRYPT_BASE64resultdummyreadable)))
               {
                  PrintFormat("Try to decode this with CryptDecode.mq5 (%s):",
                     EnumToString(method));
                  // to receive encoded data back for decoding
                  // via string input, apply Base64 over binary result
                  Print("base64:'" + CharArrayToString(readable0WHOLE_ARRAY1252) + "'");
               }
            }
         }
         else
         {
            string parts[];
            const string filename = File + "." +
               parts[StringSplit(EnumToString(method), '_', parts) - 1];
            if(PRTF(FileSave(filenameresult)))
            {
               Print("File saved: "filename);
               if(IS_HASH(method))
               {
                  ByteArrayPrint(result1000"");
               }
            }
         }
      }
   }
}

当我们转换文本时,我们会记录结果,但由于除了 CRYPT_BASE64 方法外,结果几乎总是二进制数据,它们的显示将完全是乱码(说实话,二进制数据不应该被记录,但我们这样做是为了清晰起见)。不可打印的符号和代码大于 128 的符号在不同语言的计算机上显示不同。因此,为了统一所有读者的示例显示,在 CharArrayToString 中形成行时,我们使用显式代码页(1252,西欧语言)。诚然,出版书籍时使用的字体很可能会影响某些字符的显示方式(字体中的字形集可能有限)。

务必要注意,我们仅在显示方法中控制代码页的选择,而 result 数组中的字节不会因此改变(当然,以这种方式获得的字符串不应再发送到任何地方;它仅用于可视化,数据交换应使用 result 本身的字节)。

然而,我们仍然希望为用户提供一些保存加密结果以便以后解码的机会。最简单的方法是使用 CRYPT_BASE64 方法重新转换二进制数据。

关于文件编码,我们只需将结果保存在一个新文件中,其名称是在原始文件名上添加方法名称中最后一个单词的扩展名。例如,将 CRYPT_HASH_MD5 应用于文件 Example.txt,我们将得到输出文件 Example.txt.MD5,其中包含源文件的 MD5 哈希。请注意,对于 CRYPT_ARCH_ZIP 方法,我们将得到一个带有 ZIP 扩展名的文件,但它不是标准的 ZIP 存档(由于缺少包含元信息的头部和目录表)。

我们使用默认设置运行该脚本:它们对应于在循环中检查文本 "Let's encrypt this message" 的所有方法。

CustomKey=My top secret key is very strong / ok

Key (bytes):

[00] 4D | 79 | 20 | 74 | 6F | 70 | 20 | 73 | 65 | 63 | 72 | 65 | 74 | 20 | 6B | 65 |

[16] 79 | 20 | 69 | 73 | 20 | 76 | 65 | 72 | 79 | 20 | 73 | 74 | 72 | 6F | 6E | 67 |

Text=Let's encrypt this message / ok

StringToCharArray(Text,data,0,-1,CP_UTF8)=26 / ok

- 0 CRYPT_BASE64, key required: false

CryptEncode(method,data,key,result)=36 / ok

TGV0J3MgZW5jcnlwdCB0aGlzIG1lc3NhZ2U=

[00] 54 | 47 | 56 | 30 | 4A | 33 | 4D | 67 | 5A | 57 | 35 | 6A | 63 | 6E | 6C | 77 |

[16] 64 | 43 | 42 | 30 | 61 | 47 | 6C | 7A | 49 | 47 | 31 | 6C | 63 | 33 | 4E | 68 |

[32] 5A | 32 | 55 | 3D |

- 1 CRYPT_AES128, key required: true

CryptEncode(method,data,key,result)=32 / ok

¯T* Ë[3hß Ã/-C }¬ŠÑØN¨®Ê† ‡Ñ

[00] 01 | 0B | AF | 54 | 2A | 12 | CB | 5B | 33 | 68 | DF | 0E | C3 | 2F | 2D | 43 |

[16] 19 | 7D | AC | 8A | D1 | 8F | D8 | 4E | A8 | AE | CA | 81 | 86 | 06 | 87 | D1 |

CryptEncode(CRYPT_BASE64,result,dummy,readable)=44 / ok

Try to decode this with CryptDecode.mq5 (CRYPT_AES128):

base64:'AQuvVCoSy1szaN8Owy8tQxl9rIrRj9hOqK7KgYYGh9E='

- 2 CRYPT_AES256, key required: true

CryptEncode(method,data,key,result)=32 / ok

ø‘UL»ÉsëDC‰ô ¬.K)ŒýÁ LḠ+< !Dï

[00] F8 | 91 | 55 | 4C | BB | C9 | 73 | EB | 44 | 43 | 89 | F4 | 06 | 13 | AC | 2E |

[16] 4B | 29 | 8C | FD | C1 | 11 | 4C | E1 | B8 | 05 | 2B | 3C | 14 | 21 | 44 | EF |

CryptEncode(CRYPT_BASE64,result,dummy,readable)=44 / ok

Try to decode this with CryptDecode.mq5 (CRYPT_AES256):

base64:'+JFVTLvJc+tEQ4n0BhOsLkspjP3BEUzhuAUrPBQhRO8='

- 3 CRYPT_DES, key required: true

CryptEncode(method,data,key,result)=32 / ok

µ b &“#ÇÅ+ýº'¥ B8f¡rØ-Pè<6âì‚Ë£

[00] B5 | 06 | 9D | 62 | 11 | 26 | 93 | 23 | C7 | C5 | 2B | FD | BA | 27 | A5 | 10 |

[16] 42 | 38 | 66 | A1 | 72 | D8 | 2D | 50 | E8 | 3C | 36 | E2 | EC | 82 | CB | A3 |

CryptEncode(CRYPT_BASE64,result,dummy,readable)=44 / ok

Try to decode this with CryptDecode.mq5 (CRYPT_DES):

base64:'tQadYhEmkyPHxSv9uielEEI4ZqFy2C1Q6Dw24uyCy6M='

- 4 CRYPT_HASH_SHA1, key required: false

CryptEncode(method,data,key,result)=20 / ok

§ßö*©ºø

�|)bËbzÇÍ Û�

[00] A7 | DF | F6 | 2A | A9 | BA | F8 | 0A | 80 | 7C | 29 | 62 | CB | 62 | 7A | C7 |

[16] CD | 0E | DB | 80 |

CryptEncode(CRYPT_BASE64,result,dummy,readable)=28 / ok

Try to decode this with CryptDecode.mq5 (CRYPT_HASH_SHA1):

base64:'p9/2Kqm6+AqAfCliy2J6x80O24A='

- 5 CRYPT_HASH_SHA256, key required: false

CryptEncode(method,data,key,result)=32 / ok

ÚZ2š�»”¾7 �… ñ–ÄÁ´˜¦“ome2r@¾ô®³”

[00] DA | 5A | 32 | 9A | 80 | BB | 94 | BE | 37 | 0C | 80 | 85 | 07 | F1 | 96 | C4 |

[16] C1 | B4 | 98 | A6 | 93 | 6F | 6D | 65 | 32 | 72 | 40 | BE | F4 | AE | B3 | 94 |

CryptEncode(CRYPT_BASE64,result,dummy,readable)=44 / ok

Try to decode this with CryptDecode.mq5 (CRYPT_HASH_SHA256):

base64:'2loymoC7lL43DICFB/GWxMG0mKaTb21lMnJAvvSus5Q='

- 6 CRYPT_HASH_MD5, key required: false

CryptEncode(method,data,key,result)=16 / ok

zIGT…  Fû;―3þèå

[00] 7A | 49 | 47 | 54 | 85 | 1B | 7F | 11 | 46 | FB | 3B | 97 | 33 | FE | E8 | E5 |

CryptEncode(CRYPT_BASE64,result,dummy,readable)=24 / ok

Try to decode this with CryptDecode.mq5 (CRYPT_HASH_MD5):

base64:'eklHVIUbfxFG+zuXM/7o5Q=='

- 7 CRYPT_ARCH_ZIP, key required: false

CryptEncode(method,data,key,result)=34 / ok

x^óI-Q/VHÍK.ª,(Q(ÉÈ,VÈM-.NLO

[00] 78 | 5E | F3 | 49 | 2D | 51 | 2F | 56 | 48 | CD | 4B | 2E | AA | 2C | 28 | 51 |

[16] 28 | C9 | C8 | 2C | 56 | C8 | 4D | 2D | 2E | 4E | 4C | 4F | 05 | 00 | 80 | 07 |

[32] 09 | C2 |

CryptEncode(CRYPT_BASE64,result,dummy,readable)=48 / ok

Try to decode this with CryptDecode.mq5 (CRYPT_ARCH_ZIP):

base64:'eF7zSS1RL1ZIzUsuqiwoUSjJyCxWyE0tLk5MTwUAgAcJwg=='

在这种情况下,该密钥的长度足以满足所有三种加密方法的需求,而其他不需要密钥的方法则直接忽略它。因此,所有函数调用均已成功完成。

在下一节中,我们将学习如何解码加密内容,并可以检查 CryptDecode 函数是否返回原始消息。请注意这部分日志。

启用的 DisableCRCinZIP 选项将使 CRYPT_ARCH_ZIP 方法的结果减少几个开销字节。

- 7 CRYPT_ARCH_ZIP, key required: false

CryptEncode(method,data,key,result)=28 / ok

óI-Q/VHÍK.ª,(Q(ÉÈ,VÈM-.NLO

[00] F3 | 49 | 2D | 51 | 2F | 56 | 48 | CD | 4B | 2E | AA | 2C | 28 | 51 | 28 | C9 |

[16] C8 | 2C | 56 | C8 | 4D | 2D | 2E | 4E | 4C | 4F | 05 | 00 |

CryptEncode(CRYPT_BASE64,result,dummy,readable)=40 / ok

Try to decode this with CryptDecode.mq5 (CRYPT_ARCH_ZIP):

base64:'80ktUS9WSM1LLqosKFEoycgsVshNLS5OTE8FAA=='

现在,我们将编码实验转移到文件上。为此,再次运行脚本并清除 Text f字段中的文本。结果,程序将多次处理 MQL5Book/clock10.htm 文件,并将创建几个具有不同扩展名的派生文件。

File=MQL5Book/clock10.htm / ok
FileLoad(File,data)=988 / ok
- 0 CRYPT_BASE64, key required: false
CryptEncode(method,data,key,result)=1320 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.htm.BASE64
- 1 CRYPT_AES128, key required: true
CryptEncode(method,data,key,result)=992 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.htm.AES128
- 2 CRYPT_AES256, key required: true
CryptEncode(method,data,key,result)=992 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.htm.AES256
- 3 CRYPT_DES, key required: true
CryptEncode(method,data,key,result)=992 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.htm.DES
- 4 CRYPT_HASH_SHA1, key required: false
CryptEncode(method,data,key,result)=20 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.htm.SHA1
[00] 486ADFDD071CD23AB28E820B164D813A310B213F
- 5 CRYPT_HASH_SHA256, key required: false
CryptEncode(method,data,key,result)=32 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.htm.SHA256
[00] 8990BBAC9C23B1F987952564EBCEF2078232D8C9D6F2CCC2A50784E8CDE044D0
- 6 CRYPT_HASH_MD5, key required: false
CryptEncode(method,data,key,result)=16 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.htm.MD5
[00] 0CC4FBC899554BE0C0DBF5C18748C773
- 7 CRYPT_ARCH_ZIP, key required: false
CryptEncode(method,data,key,result)=687 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.htm.ZIP

你可以从文件管理器中查看所有文件的内部,并确保与原始内容已无共同之处。许多文件管理器都有计算哈希和的命令或插件,以便可以将它们与打印到日志中的 MD5、SHA1 和 SHA256 十六进制值进行比较。

如果我们尝试编码文本或文件而未提供正确长度的密钥,我们将收到 INVALID_ARRAY(4006) 错误。例如,对于默认的文本消息,我们在 method 参数中选择 AES256(需要 32 字节的密钥)。使用 GenerateKey 参数,我们预订一个长度为 16 字节的密钥(或者你可以部分或完全删除 CustomKey 字段中的文本,保留 GenerateKey 为默认值)。

Key (bytes):
[00] 00 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 08 | 09 | 0A | 0B | 0C | 0D | 0E | 0F | 
Text=Let's encrypt this message / ok
StringToCharArray(Text,data,0,-1,CP_UTF8)=26 / ok
- 0 CRYPT_AES256, key required: true
CryptEncode(method,data,key,result)=0 / INVALID_ARRAY(4006)

你还可以使用 CRYPT_ARCH_ZIP 方法或使用常规压缩软件压缩同一个文件(就像我们对 clock10.htm 所做的那样)。如果你稍后使用二进制查看器实用程序(通常内置于文件管理器中)查看,那么两个结果都将显示一个共同的打包块,差异仅在于围绕它的元数据。

使用 CRYPT_ARCH_ZIP 方法压缩的文件(左)与包含它的标准 ZIP 存档(右)的比较

使用 CRYPT_ARCH_ZIP 方法压缩的文件(左)与包含它的标准 ZIP 存档(右)的比较

它显示存档的中间和主要部分是一个字节序列(以深色突出显示),与 CryptEncode 函数产生的字节序列相同。

最后,我们将展示图形文件 clock10.pngBase64 文本表示是如何生成的。为此,清除字段 Text 并在 File 参数中写入 MQL5Book/clock10.png。在下拉列表 Method 中选择 Base64

File=MQL5Book/clock10.png / ok
FileLoad(File,data)=457 / ok
- 0 CRYPT_BASE64, key required: false
CryptEncode(method,data,key,result)=612 / ok
FileSave(filename,result)=true / ok
File saved: MQL5Book/clock10.png.BASE64

结果创建了 clock10.png.BASE64 文件。在其中,我们将看到插入到网页代码中 img 标签里的那一行文本。

顺便说一句,"deflate" 压缩方法是 PNG 图形格式的基础,因此我们可以使用 CryptEncode 将资源位图保存为 PNG 文件。随书附带了头文件 PNG.mqh,其中包含描述图像所需内部结构的最小支持:建议独立试验其源代码。使用 PNG.mqh,我们编写了一个简单的脚本 CryptPNG.mq5,它将终端附带的 "euro.bmp" 文件中的资源转换为 "my.png" 文件。未实现 PNG 文件的加载。

#resource "\\Images\\euro.bmp"
   
#include <MQL5Book/PNG.mqh>
   
void OnStart()
{
   uchar null[];      // empty key for CRYPT_ARCH_ZIP
   uchar result[];    // receiving array
   uint data[];       // original pixels
   uchar bytes[];     // original bytes
   int widthheight;
   PRTF(ResourceReadImage("::Images\\euro.bmp"datawidthheight));
   
   ArrayResize(bytesArraySize(data) * 3 + width); // *3 for PNG_CTYPE_TRUECOLOR (RGB)
   ArrayInitialize(bytes0);
   int j = 0;
   for(int i = 0i < ArraySize(data); ++i)
   {
      if(i % width == 0bytes[j++] = 0// each line is prepended with a filter mode byte
      const uint c = data[i];
      // bytes[j++] = (uchar)((c >> 24) & 0xFF); // alpha, for PNG_CTYPE_TRUECOLORALPHA (ARGB)
      bytes[j++] = (uchar)((c >> 16) & 0xFF);
      bytes[j++] = (uchar)((c >> 8) & 0xFF);
      bytes[j++] = (uchar)(c & 0xFF);
   }
   
   PRTF(CryptEncode(CRYPT_ARCH_ZIPbytesnullresult));
   
   int h = PRTF(FileOpen("my.png"FILE_BIN | FILE_WRITE));
   
   PNG::Image image(widthheightresult); // default PNG_CTYPE_TRUECOLOR (RGB)
   image.write(h);
   
   FileClose(h);
}