Skip to content

Commit

Permalink
[add] Add multi-frame image parsing feature (gif, webp, ico, heic, ti…
Browse files Browse the repository at this point in the history
…f, dcm)
  • Loading branch information
hbl917070 committed Jan 8, 2024
1 parent de5c165 commit 4978555
Show file tree
Hide file tree
Showing 14 changed files with 1,359 additions and 893 deletions.
1,413 changes: 713 additions & 700 deletions Assets/ui icon.ai

Large diffs are not rendered by default.

334 changes: 198 additions & 136 deletions Tiefsee/Lib/Exif.cs

Large diffs are not rendered by default.

261 changes: 258 additions & 3 deletions Tiefsee/Lib/ImgLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
using ImageMagick.Formats;
using NetVips;
using NetVips.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using SQLite;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Drawing.Imaging;
using System.IO;
Expand Down Expand Up @@ -752,22 +755,266 @@ public static string PathToImg100(string path) {
}

/// <summary>
/// 取得 webp 的 總幀數
/// 取得 webp 的 總幀數、循環次數
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static int GetWebpFrameCount(string path) {
public static AnimationInfo GetWebpFrameCount(string path) {
int frameCount = -1;
int loopCount = -1;
try {
using var sr = new FileStream(path, FileMode.Open, FileAccess.Read);
using var image = NetVips.Image.NewFromStream(sr, access: NetVips.Enums.Access.Random);
frameCount = Int32.Parse(image.Get("n-pages").ToString());
loopCount = Int32.Parse(image.Get("loop").ToString());
} catch { }
return frameCount;

return new AnimationInfo { FrameCount = frameCount, LoopCount = loopCount };
}

#endregion

#region 解析多幀圖片

/// <summary>
/// 解析多幀圖片,並儲存到指定資料夾
/// </summary>
public static string ExtractFrames(string imgPath, string outputDir) {

string hase = FileToHash(imgPath);

if (outputDir == null || outputDir == "") { // 未指定資料夾
string name = Path.GetFileNameWithoutExtension(imgPath);

for (int i = 1; i <= 100; i++) {
if (i == 100) { throw new Exception(); }

// 如果資料夾已經存在,則在資料夾後面加上「 (2)」
if (i == 1) {
outputDir = Path.Combine(AppPath.tempDirWebFile, name);
} else {
outputDir = Path.Combine(AppPath.tempDirWebFile, name) + $" ({i})";
}

string infoPath = Path.Combine(outputDir, "info.json");
// 如果 info.json 已經存在,且是已經處理過的檔案,表示可以使用此資料夾
if (File.Exists(infoPath)) {
using var stream = new FileStream(infoPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using var reader = new StreamReader(stream);
var json = JObject.Parse(reader.ReadToEnd());

if ((json["hase"] ?? "").ToString() == hase) {

string[] arFile = Directory.GetFiles(outputDir, "*.*"); // 取得資料夾內所有檔案

// 產生集合,應該存在的圖片
var frameCount = int.Parse((json["frameCount"] ?? "0").ToString());
if (frameCount == 0) { break; }
var arPng = Enumerable.Range(1, frameCount)
.Select(i => Path.Combine(outputDir, i.ToString() + ".png"))
.ToArray();

bool allExists = arPng.All(x => arFile.Contains(x));
if (allExists) { // 如果所有圖片都存在,就直接回傳,不需要處理
return outputDir;
} else {
break; // 有缺漏圖片,需要處理
}
}
} else { // 如果找不到 info.json,表示可以使用此資料夾
break;
}
}
}

if (Directory.Exists(outputDir) == false) { // 資料夾不存在就新建
Directory.CreateDirectory(outputDir);
}

string ext = Exif.GetFileType(imgPath);
if (ext == "gif") {
ExtractFramesUseWinform(imgPath, outputDir);
} else {
ExtractFramesUseMagickImage(imgPath, outputDir, ext);
}
return outputDir;
}

/// <summary>
/// 解析多幀圖片,使用 Winform 內建的函式庫
/// </summary>
public static void ExtractFramesUseWinform(string imgPath, string outputDir) {

using var gif = System.Drawing.Image.FromFile(imgPath);
var fd = new System.Drawing.Imaging.FrameDimension(gif.FrameDimensionsList[0]);
int frameCount = gif.GetFrameCount(fd);

var blockingCollection = new BlockingCollection<int>(frameCount);
for (int i = 0; i < frameCount; i++) {
blockingCollection.Add(i);
}
blockingCollection.CompleteAdding();

// 以多執行序來同時處理
var tasks = Enumerable.Range(1, 7).Select(x => Task.Run(() => {
//var gif2 = System.Drawing.Image.FromFile(inputGifPath);
System.Drawing.Image gif2;
lock (gif) {
gif2 = (System.Drawing.Image)gif.Clone();
}
int n;
while (blockingCollection.TryTake(out n)) {
string output = Path.Combine(outputDir, (n + 1) + ".png");
if (File.Exists(output)) { continue; }
gif2.SelectActiveFrame(fd, n);
gif2.Save(output, System.Drawing.Imaging.ImageFormat.Png);
}
gif2.Dispose();
})
).ToArray();

Task.WaitAll(tasks); // 等待所有的任務完成

// --------

var exif = Exif.GetExif(imgPath, 500);
Dictionary<int, AnimationItemInfo> itemInfo = new Dictionary<int, AnimationItemInfo>();

// 取得每一幀的延遲(毫秒)
var exifDelay = exif.data.Where(x => x.name == "Delay").ToList();
for (int i = 0; i < exifDelay.Count; i++) {
var delay = int.Parse(exifDelay[i].value) * 10; // GIF 的延遲是 1/100秒
itemInfo.Add(i + 1, new() { Delay = delay });
}

// 取得循環次數
int? loopCount = null;
var exifLoopCount = exif.data.FirstOrDefault(x => x.name == "Loop Count");
if (exifLoopCount != null) {
loopCount = int.Parse(exifLoopCount.value);
}

ExtractFramesSaveInfo(outputDir, imgPath, frameCount, loopCount, itemInfo); // 儲存 info.json
}

/// <summary>
/// 解析多幀圖片,使用 MagickImage
/// </summary>
public static void ExtractFramesUseMagickImage(string imgPath, string outputDir, string type) {

var collection = new MagickImageCollection(imgPath);
int count = collection.Count;
int? loopCount = null;
if (type != "ico") {
collection.Coalesce(); // 使用 Coalesce 方法使每一幀成為完整的圖片
}
var blockingCollection = new BlockingCollection<int>(count);
for (int i = 0; i < count; i++) {
blockingCollection.Add(i);
}
blockingCollection.CompleteAdding();

Dictionary<int, AnimationItemInfo> itemInfo = new Dictionary<int, AnimationItemInfo>();

// 以多執行序來同時處理
var tasks = Enumerable.Range(1, 7).Select(x => Task.Run(() => {
while (blockingCollection.TryTake(out var n)) {
using var image = collection[n];

if (type == "webps") {
lock (itemInfo) {
if (n == 0) {
loopCount = image.AnimationIterations;
}
int delay = image.AnimationDelay * 10; // webp 的延遲是 1/100秒
itemInfo.Add(n + 1, new() { Delay = delay });
}
}

string outputPath = Path.Combine(outputDir, (n + 1) + ".png");
if (File.Exists(outputPath)) { continue; }
image.Write(outputPath);
}
})
).ToArray();

Task.WaitAll(tasks); // 等待所有的任務完成
collection.Dispose();

// ----------

itemInfo = itemInfo.OrderBy(x => x.Key).ToDictionary(x => x.Key, x => x.Value);

ExtractFramesSaveInfo(outputDir, imgPath, count, loopCount, itemInfo); // 儲存 info.json
}

/// <summary>
/// 解析多幀圖片,儲存 info.json
/// </summary>
public static void ExtractFramesSaveInfo(string outputDir, string path, int frameCount, int? loopCount, Dictionary<int, AnimationItemInfo> frames) {

string hase = FileToHash(path);
object data;
if (loopCount != null && frames != null && frames.Count > 0) {
data = new {
hase = hase,
path = path,
loopCount = loopCount,
frameCount = frameCount,
frames = frames
};
} else if (frames != null && frames.Count > 0) {
data = new {
hase = hase,
path = path,
// loopCount = loopCount,
frameCount = frameCount,
frames = frames
};
} else {
data = new {
hase = hase,
path = path,
// loopCount = loopCount,
frameCount = frameCount,
// frames = frames
};
}

string json = JsonConvert.SerializeObject(data, Formatting.Indented);
string jsonPath = Path.Combine(outputDir, "info.json");

var utf8WithoutBom = new System.Text.UTF8Encoding(false);
using FileStream fs = new FileStream(jsonPath, FileMode.Create);
using StreamWriter sw = new StreamWriter(fs, utf8WithoutBom);

sw.Write(json);
}

/// <summary>
/// 取得多幀圖片的總幀數
/// </summary>
public static int GetFrameCount(string path) {

// 如果已經讀取過,就從暫存裡面取得
string hash = FileToHash(path);
if (_tempGetFrameCount.ContainsKey(hash)) {
return _tempGetFrameCount[hash];
}

try {
using (var images = new MagickImageCollection()) {
images.Ping(path);
int count = images.Count;
_tempGetFrameCount.Add(hash, count);
return count;
}
} catch { }
return -1;
}
private static Dictionary<string, int> _tempGetFrameCount = new();

#endregion
}

public class ImgInitInfo {
Expand All @@ -778,4 +1025,12 @@ public class ImgInitInfo {
//public string msg = "";
}

public class AnimationInfo {
public int FrameCount { get; set; }
public int LoopCount { get; set; }
}

public class AnimationItemInfo {
public int Delay { get; set; }
}
}
26 changes: 21 additions & 5 deletions Tiefsee/Server/WebServerController.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ public WebServerController(WebServer _webServer) {
webServer.RouteAdd("/api/getRelatedFileList", getRelatedFileList);
webServer.RouteAdd("/api/isBinary", IsBinary);
webServer.RouteAdd("/api/getClipboardContent", GetClipboardContent);
webServer.RouteAdd("/api/extractFrames", ExtractFrames);


webServer.RouteAdd("/api/sort", sort);
webServer.RouteAdd("/api/sort2", sort2);

Expand Down Expand Up @@ -62,7 +64,7 @@ public WebServerController(WebServer _webServer) {
#region web API

/// <summary>
/// 取得檔案的Exif資訊
/// 取得檔案的 Exif 資訊
/// </summary>
void getExif(RequestData d) {

Expand All @@ -79,9 +81,9 @@ void getExif(RequestData d) {

//bool is304 = HeadersAdd304(d, path); //回傳檔案時加入快取的Headers
//if (is304) { return; }

string json = Exif.GetExif(path, maxLength);

var exif = Exif.GetExif(path, maxLength);
string json = JsonConvert.SerializeObject(exif);
WriteString(d, json);
}

Expand Down Expand Up @@ -475,7 +477,6 @@ private void getRelatedFileList(RequestData d) {
WriteString(d, retJson); // 回傳
}


/// <summary>
/// 檢查檔案是否為二進制檔
/// </summary>
Expand Down Expand Up @@ -504,6 +505,21 @@ private void GetClipboardContent(RequestData d) {
WriteString(d, retJson);
}

/// <summary>
/// 解析多幀圖片,並儲存到指定資料夾
/// </summary>
private void ExtractFrames(RequestData d) {

string postData = d.postData;
var json = JObject.Parse(postData);
string imgPath = json["imgPath"].ToObject<string>();
string outputDir = json["outputDir"].ToObject<string>();

var ret = ImgLib.ExtractFrames(imgPath, outputDir);
string srtStrJson = JsonConvert.SerializeObject(ret);
WriteString(d, srtStrJson); // 回傳
}

/// <summary>
///
/// </summary>
Expand Down
2 changes: 2 additions & 0 deletions TiefseeAppPackager/Package.appxmanifest
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,10 @@
<uap:FileType>.psb</uap:FileType>
<uap:FileType>.pcx</uap:FileType>
<uap:FileType>.heic</uap:FileType>
<uap:FileType>.heif</uap:FileType>
<uap:FileType>.avif</uap:FileType>
<uap:FileType>.fits</uap:FileType>
<uap:FileType>.dcm</uap:FileType>
<uap:FileType>.hdr</uap:FileType>
<uap:FileType>.miff</uap:FileType>
<uap:FileType>.mng</uap:FileType>
Expand Down
8 changes: 8 additions & 0 deletions Www/img/default/tool-export.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 4978555

Please sign in to comment.