我期待着一种在 gif 文件的每一帧中存储
text
的方法。不打印图像中的文本,而是作为属性添加。微软制作的一个旧程序可以为每个框架设置文本。
如您所见,每帧都有一个“注释”字段。
现在,我的问题是:
这个字段是 GIF 规范认可的吗?几乎没有任何文件可以这么说。 (其实是有的)
如果是:
位于哪里?使用这些方法之一?
protected void WriteGraphicCtrlExt()
{
fs.WriteByte(0x21); // extension introducer
fs.WriteByte(0xf9); // GCE label
fs.WriteByte(4); // data block size
int transp, disp;
if (transparent == Color.Empty)
{
transp = 0;
disp = 0; // dispose = no action
}
else
{
transp = 1;
disp = 2; // force clear if using transparent color
}
//If first frame, no transparency and no dispose.
if (firstFrame)
{
disp = 0;
transp = 0;
}
else
{
if (dispose >= 0)
{
disp = dispose & 7; // user override
}
disp <<= 2;
}
// packed fields
fs.WriteByte( Convert.ToByte( 0 | // 1:3 reserved
disp | // 4:6 disposal
0 | // 7 user input - 0 = none
transp )); // 8 transparency flag
WriteShort(delay); // delay x 1/100 sec
fs.WriteByte( Convert.ToByte( transIndex)); // transparent color index
fs.WriteByte(0); // block terminator
}
protected void WriteImageDesc()
{
fs.WriteByte(0x2c); // image separator
WriteShort(0); // image position x,y = 0,0
WriteShort(0);
WriteShort(width); // image size
WriteShort(height);
// packed fields
if (firstFrame)
{
// no LCT - GCT is used for first (or only) frame
fs.WriteByte(0);
}
else
{
// specify normal LCT
fs.WriteByte( Convert.ToByte( 0x80 | // 1 local color table 1=yes
0 | // 2 interlace - 0=no
0 | // 3 sorted - 0=no
0 | // 4-5 reserved
palSize ) ); // 6-8 size of color table
}
}
我找到了一种方法来做到这一点,就像 Hans Passand 所写的那样:
protected void WriteComment(string comment)
{
fs.WriteByte(0x21);
fs.WriteByte(0xfe);
byte[] lenght = StringToByteArray(comment.Length.ToString("X"));
foreach (byte b in lenght)
{
fs.WriteByte(b);
}
WriteString(comment);
}
最好只使用该工具(Microsoft 的 GIFAnimator,可通过 MSDN 订阅获得)并使用十六进制查看器查看它生成的内容。将GIF 规范放在手边,以便您可以将您看到的内容与规范相关联。
我创建了一个非常简单的 GIF 文件,其中包含两个 8x8 帧,为第一个帧键入“frame 1”,为第二个帧键入“number 2”。产生了这个十六进制转储:
我用红色突出显示了相关块。它们符合规范中的第 24 节“注释扩展”:
评论扩展包含文本信息, 不是 GIF 数据流中实际图形的一部分。适合 包括有关图形、学分、描述或任何内容的评论 其他类型的非控制和非图形数据。评论扩展 可能被解码器忽略,或者可能被保存以供以后处理; 在任何情况下,评论扩展都不应扰乱或干扰 随着数据流的处理。
该块是可选的;任意数量的它们都可能出现在数据流中。
注意它们后面是如何跟随一个 21 F9 块,一个“图形控制扩展”块,规范中的第 23 节。它描述了图像文件中的每一帧,“延迟时间”值至关重要。接下来是 2C,一个“图像描述符”块,规范中的第 20 节。它包含每一帧的图像数据。
回答您的具体问题:
这个字段是 GIF 规范认可的吗?
是的,明确如第 24 节所述。应用程序完全取决于应用程序是否合适地使用它们。它们只是注释性的,对于其他 GIF 实用程序或图像使用者来说没有任何特殊意义。
位于哪里?使用这些方法之一?
不,该代码不写注释,它只发出 21 个 F9 和 2C 块。只需更改代码以在其前面插入 21 FE 块即可。像这样的东西:
protected void WriteGraphicCtrlExt(string comment)
{
if (!string.IsNullOrEmpty(comment)) {
fs.WriteByte(0x21);
fs.WriteByte(0xfe);
var bytes = Encoding.ASCII.GetBytes(comment);
fs.WriteByte((byte)bytes.Length);
fs.Write(bytes, 0, bytes.Length);
fs.WriteByte(0);
}
// Rest of code
//...
}
嗯,有 GifLib 项目可以帮助你解决这个问题。具体来说,这些文件:
http://giflib.codeplex.com/SourceControl/latest#GifEncoder.cs,第 #87 行 Encode(),它在代码中指定以什么顺序将内容写入输出 gif 流
对于评论扩展,有一些提示:
http://giflib.codeplex.com/SourceControl/latest#CommentEx.cs,第 57 行 GetBuffer():
internal byte[] GetBuffer()
{
List<byte> list = new List<byte>();
list.Add(GifExtensions.ExtensionIntroducer); // 0x21
list.Add(GifExtensions.CommentLabel); // 0xFE
foreach (string coment in CommentDatas)
{
char[] commentCharArray = coment.ToCharArray();
list.Add((byte)commentCharArray.Length);
foreach (char c in commentCharArray)
{
list.Add((byte)c);
}
}
list.Add(GifExtensions.Terminator); // 0
return list.ToArray();
}
处理 GIF 文件有时有点棘手: https://giflib.sourceforge.net/whatsinagif/bits_and_bytes.html
令人遗憾的是,使用 GifBitmapEncoder 或 GifBitmapDecoder 无法开箱即用地操作元数据上的 GIF 注释,请参阅示例:https://github.com/heinrichelsigan/area23.at/blob/main/www/Area23 .At.Www.S/Util/GifMetadataAdapter.cs
/// <summary>
/// GenerateGifWithComment - hackish raw method to add a GIF-Comment
/// </summary>
/// <param name="codecImage">existing Bitmap to modify</param>
/// <param name="gifComment">comment, that you want to insert in CompuServe GIF</param>
/// <param name="saveFilePath">filepath to save modified GIF file with comment on filesysten with full directory path</param>
/// <returns></returns>
public Bitmap GenerateGifWithComment(Bitmap codecImage, string gifComment = null, string saveFilePath = null)
{
string outFullPath = System.AppDomain.CurrentDomain.BaseDirectory + System.IO.Path.PathSeparator;
string outGenerated = outFullPath + DateTime.UtcNow.ToString("{yyyy-MM-dd_HH.mm.ss.fff}.gif");
saveFilePath = saveFilePath ?? outGenerated;
gifComment = gifComment ?? "";
gifComment = gifComment.Trim("\r\b\a\v".ToCharArray()) ;
MemoryStream gifMemStream = new MemoryStream(); // create gifMemStream - a new MemoryStream()
codecImage.Save(gifMemStream, ImageFormat.Gif); // save codecImage to gifMemStream in ImageFormat.Gif
byte[] imgGifBytes = gifMemStream.ToArray(); // get all byte[] imgGifBytes from gifMemStream
List<byte> toByteList = new List<byte>();
bool flagOnce = false;
for (int bydx = 0; bydx < imgGifBytes.Length; bydx++)
{
// this kind of MIT Magick Cookie will detect position, where to insert before GIF-Comment
// insert GIF-Comment before <= 0x21 0xF9 0x04 0x01 0x00
// insert GIF-Comment before <= 0x21 0xFE 0x04 0x01 0x00
if ((imgGifBytes[bydx] == (byte)0x21) && // 0x21 (byte)((char)'!')
(imgGifBytes[bydx + 1] == (byte)0xf9 || // 0xF9 | 0xFE
imgGifBytes[bydx + 1] == (byte)0xfe) &&
(imgGifBytes[bydx + 2] == (byte)0x04) && // 0x04 EOT
(imgGifBytes[bydx + 3] == (byte)0x01) && // 0x01 SOH
(imgGifBytes[bydx + 4] == (byte)0x00)) // 0x00 NUL
{
if (!flagOnce)
{
foreach (byte b in GifCommentBytes(gifComment))
{
toByteList.Add(b);
}
flagOnce = true;
}
}
if ((bydx == imgGifBytes.Length - 1) && // only short check at end of gifBytes,
((imgGifBytes[bydx - 1] == (byte)0x0) && // that penultimate byte is NUL
imgGifBytes[bydx] == (byte)0x3b)) // and last byte of GIF is 0x3b
{ ; } /* <= only breakpoint trigger */
toByteList.Add(imgGifBytes[bydx]); // Add next byte from gifBytes to ToByteList
}
// save original codecImage as Gif to filename $"{dateTime.UtcNow:yyyy-MM-dd_HH.mm.ss.fff}_original.gif"
outGenerated = outFullPath + DateTime.UtcNow.ToString("{yyyy-MM-dd_HH.mm.ss.fff}_original.gif");
codecImage.Save(outGenerated, ImageFormat.Gif);
// open a FileStream and write (byte[])toByteList.ToArray() fully out => flush
using (Stream fs = File.Open(saveFilePath, FileMode.Create, FileAccess.ReadWrite))
{
fs.Write(toByteList.ToArray(), 0, toByteList.Count); // writes toByteList.Count byte[] from toByteList
fs.Flush();
}
Bitmap gifMap = new Bitmap(saveFilePath);
return gifMap;
}
/// <summary>
/// GifCommentBytes - gets a byte[] for further comment to add to GIF
/// </summary>
/// <param name="c">GIF-Comment as <see cref="string"/></param>
/// <returns><see cref="byte[]">array of byte</see></returns>
protected virtual byte[] GifCommentBytes(string c)
{
if (string.IsNullOrEmpty(c))
return new byte[0];
List<byte> byteList = new List<byte>(); // create a new generic List<byte>()
byteList.Add((byte)0x21); // Write first 0x21 '!' detection sequence
byteList.Add((byte)0xfe); // Write 0xfe ((byte)254) as snd byte
byte[] bytes = Encoding.ASCII.GetBytes(c); // get byte[] from string ASCII
byte b0 = Convert.ToByte(c.Length & 0xff); // write first content length of now following comment
if (c.Length > (int)0xff) // TODO: might be still buggy, if comment length >= 256 ;(
{
byte b1 = Convert.ToByte((c.Length >> 8) & 0xff);
byteList.Add(b1); // add most significant byte from content length to List<byte>
}
byteList.Add(b0); // finally add the least significant byte from content length to List<byte>
foreach (byte b in bytes) // loop through all comment bytes
{
byteList.Add(b); // add byte per byte from byte[] comment to List<byte>
}
byteList.Add((byte)0x0); // add 0x00 as termination symbol to finish comment header in GIF
return byteList.ToArray();
}