Skip to content

Commit

Permalink
[add] Added functionality to display video length information
Browse files Browse the repository at this point in the history
[add] Added functionality to display total frame count for GIF and WEBP files
  • Loading branch information
hbl917070 committed Dec 18, 2023
1 parent 49756c1 commit 1154ebf
Show file tree
Hide file tree
Showing 6 changed files with 199 additions and 76 deletions.
89 changes: 60 additions & 29 deletions Tiefsee/Lib/Exif.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Newtonsoft.Json;
using System.IO;
using System.Text;
using System.Xml.Linq;
using Windows.Storage;

namespace Tiefsee {
Expand Down Expand Up @@ -79,51 +80,28 @@ public static string GetExif(string path, int maxLength) {
ImgExif exif = new ImgExif();

exif.data.Add(new ImgExifItem { // 建立時間
group = "base",
group = "Base",
name = "Creation Time",
value = File.GetCreationTime(path).ToString("yyyy-MM-dd HH:mm:ss")
});
exif.data.Add(new ImgExifItem { // 最後修改時間
group = "base",
group = "Base",
name = "Last Write Time",
value = File.GetLastWriteTime(path).ToString("yyyy-MM-dd HH:mm:ss")
});
/*exif.data.Add(new ImgExifItem { // 上次存取時間
group = "base",
group = "Base",
name = "Last Access Time",
value = File.GetLastAccessTime(path).ToString("yyyy-MM-dd HH:mm:ss")
});*/
exif.data.Add(new ImgExifItem { // 檔案 size
group = "base",
group = "Base",
name = "Length",
value = new FileInfo(path).Length.ToString()
});
string w = "";
string h = "";

// 如果是影片,則另外讀取 Comment 資訊
string fileType = GetFileType(path);
if (fileType == "mp4" || fileType == "webm" || fileType == "avi") {

string comment = null;

Task.Run(async () => {
try {
var f = await StorageFile.GetFileFromPathAsync(path);
var v = await f.Properties.GetDocumentPropertiesAsync();
comment = v.Comment;
} catch { }
}).Wait(); // 等待非同步操作完成

if (string.IsNullOrEmpty(comment) == false) {
exif.data.Add(new ImgExifItem {
group = "Shell",
name = "Comment",
value = comment.Trim()
});
}
}

try {
IEnumerable<MetadataExtractor.Directory> directories = MetadataExtractor.ImageMetadataReader.ReadMetadata(path);

Expand Down Expand Up @@ -213,13 +191,62 @@ public static string GetExif(string path, int maxLength) {
}
}
}

// 新增圖片 size 的資訊
if (w != "" && h != "") {
exif.data.Add(new ImgExifItem {
group = "Image",
name = "Image Width/Height",
value = $"{w} x {h}"
});
}

// 如果是影片,則另外讀取 Comment 資訊
string fileType = GetFileType(path);
if (fileType == "mp4" || fileType == "webm" || fileType == "avi") {
string comment = null;
Task.Run(async () => {
try {
var f = await StorageFile.GetFileFromPathAsync(path);
var v = await f.Properties.GetDocumentPropertiesAsync();
comment = v.Comment;
} catch { }
}).Wait(); // 等待非同步操作完成

if (string.IsNullOrEmpty(comment) == false) {
exif.data.Add(new ImgExifItem {
group = "Shell",
name = "Comment",
value = comment.Trim()
});
}
}
// 如果是 webp 動圖,則另外讀取 總幀數 資訊
else if (fileType == "webp-animation") {
int frameCount = ImgLib.GetWebpFrameCount(path);
if (frameCount > 1) {
exif.data.Add(new ImgExifItem {
group = "Frames",
name = "Frame Count",
value = frameCount.ToString()
});
}
}
// 如果檔案類型是 GIF,則新增「總幀數」的資訊
else if (fileType == "gif") {
int frames = exif.data
.Where(x => x.group == "GIF Control")
.Where(x => x.name == "Delay")
.Count();
if (frames > 0) {
exif.data.Add(new ImgExifItem {
group = "Frames",
name = "Frame Count",
value = frames.ToString()
});
}
}

} catch { }

exif.code = "1";
Expand All @@ -239,7 +266,7 @@ public static string GetFileType(string path) {
try {
using FileStream fs = new(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
using BinaryReader br = new(fs);
int readLength = 20;
int readLength = 100;

for (int i = 0; i < readLength; i++) {
if (fs.Position >= fs.Length) break; // 如果已經讀取到文件的結尾,則跳出循環
Expand All @@ -261,7 +288,11 @@ public static string GetFileType(string path) {
} else if (hex.StartsWith("89 50 4E 47 0D 0A 1A 0A")) {
return "png";
} else if (hex.Contains("57 45 42 50 56 50 38")) { // WEBPVP8
return "webp";
if (hex.Contains("41 4E 49 4D")) { // ANIM
return "webp-animation";
} else {
return "webp";
}
} else if (hex.StartsWith("25 50 44 46")) { // %PDF
return "pdf";
} else if (hex.Contains("66 74 79 70")) { // 66(f) 74(t) 79(y) 70(p) 。其他影片格式也可能誤判成mp4
Expand Down
15 changes: 15 additions & 0 deletions Tiefsee/Lib/ImgLib.cs
Original file line number Diff line number Diff line change
Expand Up @@ -751,6 +751,21 @@ public static string PathToImg100(string path) {
return filePath;
}

/// <summary>
/// 取得 webp 的 總幀數
/// </summary>
/// <param name="path"></param>
/// <returns></returns>
public static int GetWebpFrameCount(string path) {
int frameCount = -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());
} catch { }
return frameCount;
}

#endregion

}
Expand Down
19 changes: 19 additions & 0 deletions Www/lang/langData.js
Original file line number Diff line number Diff line change
Expand Up @@ -2095,6 +2095,25 @@ var langData = {
"en": "Software",
"ja": "ソフトウェア",
},

// 影片 ----------

"Duration": {
"zh-TW": "影片長度",
"en": "Video Length",
"ja": "ビデオの長さ"
},

// Gif ----------

"Frame Count": {
"zh-TW": "總幀數",
"en": "Frame Count",
"ja": "フレーム数"
},

// 每個檔案固定都會有的欄位 ----------

"Creation Time": {
"zh-TW": "建立日期",
"en": "Creation Time",
Expand Down
2 changes: 2 additions & 0 deletions Www/ts/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class Config {
"Orientation", // 旋轉資訊
"Software", // 軟體
// "Color Space", // 色彩空間
"Duration", // 影片長度
"Frame Count", // 總幀數
"Creation Time", // 建立日期
"Last Write Time", // 修改日期
// "Last Access Time", // 存取時間
Expand Down
106 changes: 87 additions & 19 deletions Www/ts/MainWindow/MainExif.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,33 +226,73 @@ class MainExif {
return;
}
}
let groupType = M.fileShow.getGroupType();

// 取得經緯度
let GPS_lat = getItem(json.data, "GPS Latitude"); // 緯度
let GPS_lng = getItem(json.data, "GPS Longitude"); // 經度
if (GPS_lat === `0° 0' 0"` && GPS_lng === `0° 0' 0"`) { // 避免空白資料
GPS_lat = undefined;
GPS_lng = undefined;
let gpsLat = getItem(json.data, "GPS Latitude")?.value; // 緯度
let gpsLng = getItem(json.data, "GPS Longitude")?.value; // 經度
if (gpsLat === `0° 0' 0"` && gpsLng === `0° 0' 0"`) { // 避免空白資料
gpsLat = undefined;
gpsLng = undefined;
}
let hasGPS = GPS_lat !== undefined && GPS_lng !== undefined;
let hasGPS = gpsLat !== undefined && gpsLng !== undefined;
if (hasGPS) { // 如果經緯度不是空,就新增「Map」欄位
json.data.push({ group: "GPS", name: "Map", value: `${GPS_lat},${GPS_lng}` });
json.data.push({ group: "GPS", name: "Map", value: `${gpsLat},${gpsLng}` });
}

let deferredFunc: (() => void)[] = []; // 最後才執行的函數
let comfyuiPrompt: string | undefined;
let comfyuiWorkflow: string | undefined;

// 待影片載入完畢,更新「影片長度」的資訊
async function updateVideoDuration(domVideo: HTMLElement) {
await Lib.sleep(10);
for (let i = 0; i < 100; i++) {

let duration = M.fileShow.tiefseeview.getVideoDuration();

if (isNaN(duration) === false) {

let value = formatVideoLength(duration);

// 產生新的 dom
let name = M.i18n.t(`exif.name.Duration`);
let domVideoNew = getItemDom(name, value);
domTabContentInfo.appendChild(domVideoNew);

// 把新的 dom 插到原有的 dom 後面,然後刪除原有的 dom
domVideo.insertAdjacentElement("afterend", domVideoNew);
domVideo.parentNode?.removeChild(domVideo);

return;
}
await Lib.sleep(100);
}
}

let ar = json.data;
let whitelist = M.config.exif.whitelist;
for (let i = 0; i < whitelist.length; i++) {

let name = whitelist[i];
let value = getItem(ar, name);

if (value === undefined) {
// 如果是影片
if (groupType === GroupType.video && name === "Duration") {
// 先產生一個沒有資料的項目
let domVideo = getItemDom(M.i18n.t(`exif.name.${name}`), " ");
domTabContentInfo.appendChild(domVideo);
// 待影片載入完畢,更新「影片長度」的資訊
updateVideoDuration(domVideo);
continue;

}
else if (name === "Map") {

let exifItem = getItem(ar, name);
if (exifItem === undefined) { continue; }
let value = exifItem.value;
let group = exifItem.group;
if (value === "") { continue; }

if (name === "Map") {

value = encodeURIComponent(value); // 移除可能破壞html的跳脫符號

Expand Down Expand Up @@ -372,18 +412,19 @@ class MainExif {
let nameI18n = `exif.name.${name}`;
let valueI18n = "";

// 處理value的值
// 處理 value 的值
if (name === "Metering Mode") {
value = M.i18n.t(`exif.value.${name}.${value}`);
valueI18n = `exif.value.${name}.${value}`;
}
if (name === "Flash") {
else if (name === "Flash") {
value = M.i18n.t(`exif.value.${name}.${value}`);
valueI18n = `exif.value.${name}.${value}`
}
if (name === "Length") {
else if (name === "Length") {
value = Lib.getFileLength(Number(value));
}

name = M.i18n.t(`exif.name.${name}`);
domTabContentInfo.appendChild(getItemDom(name, value, nameI18n, valueI18n));
}
Expand Down Expand Up @@ -426,8 +467,8 @@ class MainExif {
}
domTabContentInfo.appendChild(collapseDom.domBox);
}
}

}

/**
* 讀取 相關檔案(於初始化後呼叫)
Expand Down Expand Up @@ -502,6 +543,30 @@ class MainExif {
return btnNew;
}

/**
* 格式化影片長度。毫秒 → 00:00
*/
function formatVideoLength(input: any): string {
if (input === undefined || input === null || input === "") { return ""; }

let milliseconds = Number(input);
if (isNaN(milliseconds)) { return input; }

//let totalSeconds = Math.floor(milliseconds / 1000);
let totalSeconds = Math.floor(milliseconds);
let hours = Math.floor(totalSeconds / 3600);
let minutes = Math.floor((totalSeconds % 3600) / 60);
let seconds = totalSeconds % 60;

// 如果小時數小於1,則只返回分鐘和秒數
if (hours < 1) {
return (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
} else {
// 否則,返回格式化的小時、分鐘和秒數
return (hours < 10 ? "0" : "") + hours + ":" + (minutes < 10 ? "0" : "") + minutes + ":" + (seconds < 10 ? "0" : "") + seconds;
}
}

/**
* 取得 相關檔案的 項目
* @param itemPath
Expand Down Expand Up @@ -807,7 +872,7 @@ class MainExif {
/**
* 從 exif 裡面取得全部的 value
*/
function getItems(ar: any, key: string) {
function getItems(ar: { group: string, name: string, value: string }[], key: string) {
let arOutput = [];
for (let i = 0; i < ar.length; i++) {
let item = ar[i];
Expand All @@ -819,13 +884,16 @@ class MainExif {
}

/**
* 從exif裡面取得第一筆的value
* 從 exif 裡面取得第一筆的 value
*/
function getItem(ar: any, key: string) {
function getItem(ar: { group: string, name: string, value: string }[], key: string) {
for (let i = 0; i < ar.length; i++) {
let item = ar[i];
if (item.name == key) {
return item.value;
return {
"group": item.group,
"value": item.value
};
}
}
return undefined;
Expand Down
Loading

0 comments on commit 1154ebf

Please sign in to comment.