606 lines
22 KiB
C#
Executable File
606 lines
22 KiB
C#
Executable File
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Gallery.Illust;
|
|
using Xamarin.Forms;
|
|
using System.Linq;
|
|
#if __IOS__
|
|
using AVFoundation;
|
|
using CoreGraphics;
|
|
using CoreMedia;
|
|
using CoreVideo;
|
|
using Foundation;
|
|
using UIKit;
|
|
#endif
|
|
|
|
namespace Gallery.Utils
|
|
{
|
|
public class Ugoira : IDisposable
|
|
{
|
|
private const int DELAY = 200;
|
|
private const int BUFFER_SIZE = 300000;
|
|
private const int BUFFER_TABLE = 30000;
|
|
private readonly object sync = new object();
|
|
|
|
private readonly IllustUgoiraBody ugoira;
|
|
private readonly IllustDetailItem detailItem;
|
|
private readonly ImageSource[] frames;
|
|
private Timer timer;
|
|
private int index = 0;
|
|
private ParallelTask downloadTask;
|
|
|
|
public bool IsPlaying { get; private set; }
|
|
public readonly int FrameCount;
|
|
public event EventHandler<UgoiraEventArgs> FrameChanged;
|
|
|
|
public Ugoira(IllustUgoiraData illust, IllustDetailItem item)
|
|
{
|
|
ugoira = illust.body;
|
|
detailItem = item;
|
|
frames = new ImageSource[ugoira.frames.Length];
|
|
FrameCount = frames.Length;
|
|
|
|
Task.Run(LoadFrames);
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
if (IsPlaying)
|
|
{
|
|
TogglePlay(false);
|
|
}
|
|
if (downloadTask != null)
|
|
{
|
|
downloadTask.Dispose();
|
|
downloadTask = null;
|
|
}
|
|
ClearTimer();
|
|
}
|
|
|
|
private void ClearTimer()
|
|
{
|
|
lock (sync)
|
|
{
|
|
if (timer != null)
|
|
{
|
|
timer.Dispose();
|
|
timer = null;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void TogglePlay(bool flag)
|
|
{
|
|
if (IsPlaying == flag)
|
|
{
|
|
return;
|
|
}
|
|
ClearTimer();
|
|
if (flag)
|
|
{
|
|
timer = new Timer(OnTimerCallback, null, 0, Timeout.Infinite);
|
|
IsPlaying = true;
|
|
}
|
|
else
|
|
{
|
|
IsPlaying = false;
|
|
}
|
|
}
|
|
|
|
public void ToggleFrame(int frame)
|
|
{
|
|
if (IsPlaying)
|
|
{
|
|
// TODO: doesn't support change current frame when playing
|
|
return;
|
|
}
|
|
if (frame < 0 || frame >= frames.Length)
|
|
{
|
|
return;
|
|
}
|
|
var image = frames[frame];
|
|
if (image != null)
|
|
{
|
|
index = frame;
|
|
FrameChanged?.Invoke(this, new UgoiraEventArgs
|
|
{
|
|
DetailItem = detailItem,
|
|
Image = image,
|
|
FrameIndex = frame
|
|
});
|
|
}
|
|
}
|
|
|
|
private void OnTimerCallback(object state)
|
|
{
|
|
lock (sync)
|
|
{
|
|
if (!IsPlaying)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
|
|
ImageSource frame;
|
|
var i = index;
|
|
var info = ugoira.frames[i];
|
|
while ((frame = frames[i]) == null)
|
|
{
|
|
lock (sync)
|
|
{
|
|
if (timer == null)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
// not downloaded yet, waiting...
|
|
Thread.Sleep(DELAY);
|
|
}
|
|
FrameChanged?.Invoke(this, new UgoiraEventArgs
|
|
{
|
|
DetailItem = detailItem,
|
|
Image = frame,
|
|
FrameIndex = i
|
|
});
|
|
i++;
|
|
if (i >= frames.Length)
|
|
{
|
|
i = 0;
|
|
}
|
|
index = i;
|
|
if (timer != null && IsPlaying)
|
|
{
|
|
timer.Change(info.delay, Timeout.Infinite);
|
|
}
|
|
}
|
|
|
|
private void LoadFrames()
|
|
{
|
|
var zip = Path.GetFileName(ugoira.originalSrc);
|
|
bool download = false;
|
|
var uframes = ugoira.frames;
|
|
for (var i = 0; i < uframes.Length; i++)
|
|
{
|
|
var frame = uframes[i];
|
|
var image = Stores.LoadUgoiraImage(zip, frame.file);
|
|
if (image != null)
|
|
{
|
|
frame.FilePath = image;
|
|
frames[i] = image;
|
|
}
|
|
else
|
|
{
|
|
frame.Incompleted = true;
|
|
download = true;
|
|
}
|
|
}
|
|
|
|
if (download)
|
|
{
|
|
// need download
|
|
var url = ugoira.originalSrc;
|
|
var id = detailItem.Id;
|
|
var (size, lastModified, client) = HttpUtility.GetUgoiraHeader(url, id);
|
|
#if LOG
|
|
App.DebugPrint($"starting download ugoira: {size} bytes, last modified: {lastModified}");
|
|
#endif
|
|
|
|
var data = new byte[size];
|
|
|
|
Segment[] segs;
|
|
var length = (int)Math.Ceiling((double)(size - BUFFER_TABLE) / BUFFER_SIZE) + 1;
|
|
segs = new Segment[length];
|
|
|
|
var tableOffset = size - BUFFER_TABLE;
|
|
segs[length - 1] = new Segment(length - 1, tableOffset, size - 1);
|
|
segs[length - 2] = new Segment(length - 2, (length - 2) * BUFFER_SIZE, tableOffset - 1);
|
|
for (var i = 0; i < length - 2; i++)
|
|
{
|
|
long from = i * BUFFER_SIZE;
|
|
segs[i] = new Segment(i, from, from + BUFFER_SIZE - 1);
|
|
}
|
|
|
|
// table
|
|
var segTable = segs[length - 1];
|
|
using (var ms = new MemoryStream(data, (int)segTable.From, BUFFER_TABLE))
|
|
{
|
|
HttpUtility.DownloadUgoiraImage(client, url, id, lastModified, segTable.From, segTable.To, ms);
|
|
if (timer == null)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
segTable.Done = true;
|
|
for (var i = tableOffset; i < size - 4; i++)
|
|
{
|
|
if (data[i + 0] == 0x50 && data[i + 1] == 0x4b &&
|
|
data[i + 2] == 0x01 && data[i + 3] == 0x02)
|
|
{
|
|
tableOffset = i;
|
|
break;
|
|
}
|
|
}
|
|
if (tableOffset > size - 31)
|
|
{
|
|
App.DebugError("find.table", $"failed to find table offset, id: {id}, url: {url}");
|
|
return;
|
|
}
|
|
for (var n = 0; n < uframes.Length; n++)
|
|
{
|
|
var frame = uframes[n];
|
|
var i = (int)tableOffset;
|
|
i += 10; // signature(4) & version(2) & version_need(2) & flags(2)
|
|
if (data[i] != 0 || data[i + 1] != 0)
|
|
{
|
|
App.DebugError("extract.image", $"doesn't support compressed data: {BitConverter.ToInt16(data, i):x4}");
|
|
return;
|
|
}
|
|
i += 10; // compression(2) & mod-time(2) & mod-date(2) & crc-32(4)
|
|
int entrySize = BitConverter.ToInt32(data, i);
|
|
i += 4; // size(4)
|
|
int rawSize = BitConverter.ToInt32(data, i);
|
|
if (entrySize != rawSize)
|
|
{
|
|
App.DebugError("find.table", $"data seems to be compressed: {entrySize:x8} ({rawSize:x8}) bytes");
|
|
return;
|
|
}
|
|
i += 4; // rawSize(4)
|
|
int filenameLength = BitConverter.ToInt16(data, i);
|
|
i += 2; // filename length(2)
|
|
int extraLength = BitConverter.ToInt16(data, i);
|
|
i += 2; // extra length(2)
|
|
int commentLength = BitConverter.ToInt16(data, i);
|
|
i += 10; // comment length(2) & disk start(2) & internal attr(2) & external attr(4)
|
|
int entryOffset = BitConverter.ToInt32(data, i);
|
|
i += 4; // offset(4)
|
|
var filename = Encoding.UTF8.GetString(data, i, filenameLength);
|
|
tableOffset = i + filenameLength + extraLength + commentLength; // filename & extra & comment
|
|
|
|
if (frame.file != filename)
|
|
{
|
|
App.DebugError("find.table", $"error when fill entry information, read name: {filename}, frame name: {frame.file}");
|
|
return;
|
|
}
|
|
frame.Offset = entryOffset;
|
|
frame.Length = entrySize;
|
|
}
|
|
|
|
// only download needed
|
|
var inSegs = new List<Segment>();
|
|
for (var i = 0; i < uframes.Length; i++)
|
|
{
|
|
var frame = uframes[i];
|
|
if (frame.Incompleted)
|
|
{
|
|
var (first, last) = QueryRange(segs, frame);
|
|
frame.First = first;
|
|
frame.Last = last;
|
|
for (var n = first; n <= last; n++)
|
|
{
|
|
var seg = segs[n];
|
|
if (!seg.Done && !inSegs.Contains(seg))
|
|
{
|
|
inSegs.Add(seg);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (downloadTask != null)
|
|
{
|
|
downloadTask.Dispose();
|
|
downloadTask = null;
|
|
}
|
|
downloadTask = ParallelTask.Start("ugoira.download", 0, inSegs.Count, 3, i =>
|
|
{
|
|
var seg = inSegs[i];
|
|
#if DEBUG
|
|
App.DebugPrint($"start to download segment #{seg.Index}, from {seg.From} to {seg.To} / {size}");
|
|
#endif
|
|
using (var ms = new MemoryStream(data, (int)seg.From, seg.Count))
|
|
{
|
|
HttpUtility.DownloadUgoiraImage(client, url, id, lastModified, seg.From, seg.To, ms);
|
|
}
|
|
seg.Done = true;
|
|
return timer != null;
|
|
});
|
|
|
|
for (var i = 0; i < uframes.Length; i++)
|
|
{
|
|
var frame = uframes[i];
|
|
if (frame.Incompleted)
|
|
{
|
|
var first = frame.First;
|
|
var last = frame.Last;
|
|
while (segs.AnyFor(first, last, s => !s.Done))
|
|
{
|
|
lock (sync)
|
|
{
|
|
if (timer == null)
|
|
{
|
|
return;
|
|
}
|
|
}
|
|
Thread.Sleep(DELAY);
|
|
}
|
|
|
|
var file = ExtractImage(zip, data, frame.Offset);
|
|
if (file != null)
|
|
{
|
|
frame.FilePath = file;
|
|
frames[i] = ImageSource.FromFile(file);
|
|
frame.Incompleted = false;
|
|
}
|
|
}
|
|
}
|
|
#if LOG
|
|
App.DebugPrint("load frames over");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
private (int first, int last) QueryRange(Segment[] segs, IllustUgoiraBody.Frame frame)
|
|
{
|
|
var start = frame.Offset;
|
|
var end = start + frame.Length;
|
|
var first = segs.LastIndexOf(s => start >= s.From);
|
|
var last = segs.IndexOf(s => end <= s.To);
|
|
return (first, last);
|
|
}
|
|
|
|
private string ExtractImage(string zip, byte[] data, int index)
|
|
{
|
|
var i = index;
|
|
if (i + 30 > data.Length - 1) // last
|
|
{
|
|
return null;
|
|
}
|
|
if (data[i] != 0x50 || data[i + 1] != 0x4b ||
|
|
data[i + 2] != 0x03 || data[i + 3] != 0x04)
|
|
{
|
|
App.DebugPrint($"extract complete, header: {BitConverter.ToInt32(data, i):x8}");
|
|
return null;
|
|
}
|
|
i += 8; // signature(4) & version(2) & flags(2)
|
|
if (data[i] != 0 || data[i + 1] != 0)
|
|
{
|
|
App.DebugError("extract.image", $"doesn't support compressed data: {BitConverter.ToInt16(data, i):x4}");
|
|
return null;
|
|
}
|
|
i += 10; // compression(2) & mod-time(2) & mod-date(2) & crc-32(4)
|
|
int size = BitConverter.ToInt32(data, i);
|
|
i += 4; // size(4)
|
|
int rawSize = BitConverter.ToInt32(data, i);
|
|
if (size != rawSize)
|
|
{
|
|
App.DebugError("extract.image", $"data seems to be compressed: {size:x8} ({rawSize:x8}) bytes");
|
|
return null;
|
|
}
|
|
i += 4; // rawSize(4)
|
|
int filenameLength = BitConverter.ToInt16(data, i);
|
|
i += 2; // filename length(2)
|
|
int extraLength = BitConverter.ToInt16(data, i);
|
|
i += 2; // extra length(2)
|
|
|
|
if (i + filenameLength + extraLength + size > data.Length - 1) // last
|
|
{
|
|
App.DebugPrint($"download is not completed, index: {index}, size: {size}, length: {data.Length}"); // , last: {last}
|
|
return null;
|
|
}
|
|
|
|
var filename = Encoding.UTF8.GetString(data, i, filenameLength);
|
|
i += filenameLength + extraLength; // filename & extra
|
|
|
|
// content
|
|
var content = new byte[size];
|
|
Array.Copy(data, i, content, 0, size);
|
|
//i += size;
|
|
|
|
var file = Stores.SaveUgoiraImage(zip, filename, content);
|
|
return file;
|
|
}
|
|
|
|
class Segment
|
|
{
|
|
public int Index;
|
|
public long From;
|
|
public long To;
|
|
public bool Done;
|
|
|
|
public int Count => (int)(To - From + 1);
|
|
|
|
public Segment(int index, long from, long to)
|
|
{
|
|
Index = index;
|
|
From = from;
|
|
To = to;
|
|
Done = false;
|
|
}
|
|
}
|
|
|
|
#if __IOS__
|
|
public async Task<string> ExportVideo()
|
|
{
|
|
if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var file = Stores.GetUgoiraPath(ugoira.originalSrc, ".mp4");
|
|
if (File.Exists(file))
|
|
{
|
|
File.Delete(file);
|
|
}
|
|
var fileURL = NSUrl.FromFilename(file);
|
|
var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();
|
|
|
|
var videoSettings = new NSMutableDictionary
|
|
{
|
|
{ AVVideo.CodecKey, AVVideo.CodecH264 },
|
|
{ AVVideo.WidthKey, NSNumber.FromNFloat(images[0].Size.Width) },
|
|
{ AVVideo.HeightKey, NSNumber.FromNFloat(images[0].Size.Height) }
|
|
};
|
|
var videoWriter = new AVAssetWriter(fileURL, AVFileType.Mpeg4, out var err);
|
|
if (err != null)
|
|
{
|
|
App.DebugError("export.video", $"failed to create an AVAssetWriter: {err}");
|
|
return null;
|
|
}
|
|
var writerInput = new AVAssetWriterInput(AVMediaType.Video, new AVVideoSettingsCompressed(videoSettings));
|
|
var sourcePixelBufferAttributes = new NSMutableDictionary
|
|
{
|
|
{ CVPixelBuffer.PixelFormatTypeKey, NSNumber.FromInt32((int)CVPixelFormatType.CV32ARGB) }
|
|
};
|
|
var pixelBufferAdaptor = new AVAssetWriterInputPixelBufferAdaptor(writerInput, sourcePixelBufferAttributes);
|
|
videoWriter.AddInput(writerInput);
|
|
if (videoWriter.StartWriting())
|
|
{
|
|
videoWriter.StartSessionAtSourceTime(CMTime.Zero);
|
|
var lastTime = CMTime.Zero;
|
|
#if DEBUG
|
|
bool log = false;
|
|
#endif
|
|
for (int i = 0; i < images.Length; i++)
|
|
{
|
|
while (!writerInput.ReadyForMoreMediaData)
|
|
{
|
|
lock (sync)
|
|
{
|
|
if (timer == null)
|
|
{
|
|
return null;
|
|
}
|
|
}
|
|
Thread.Sleep(50);
|
|
}
|
|
// get pixel buffer and fill it with the image
|
|
using (CVPixelBuffer pixelBufferImage = pixelBufferAdaptor.PixelBufferPool.CreatePixelBuffer())
|
|
{
|
|
pixelBufferImage.Lock(CVPixelBufferLock.None);
|
|
|
|
try
|
|
{
|
|
IntPtr pxdata = pixelBufferImage.BaseAddress;
|
|
|
|
if (pxdata != IntPtr.Zero)
|
|
{
|
|
using (var rgbColorSpace = CGColorSpace.CreateDeviceRGB())
|
|
{
|
|
var cgImage = images[i].CGImage;
|
|
var width = cgImage.Width;
|
|
var height = cgImage.Height;
|
|
var bitsPerComponent = cgImage.BitsPerComponent;
|
|
var bytesPerRow = cgImage.BytesPerRow;
|
|
// padding to 64
|
|
var bytes = bytesPerRow >> 6 << 6;
|
|
if (bytes < bytesPerRow)
|
|
{
|
|
bytes += 64;
|
|
}
|
|
#if DEBUG
|
|
if (!log)
|
|
{
|
|
log = true;
|
|
App.DebugPrint($"animation, width: {width}, height: {height}, type: {cgImage.UTType}\n" +
|
|
$"bitmapInfo: {cgImage.BitmapInfo}\n" +
|
|
$"bpc: {bitsPerComponent}\n" +
|
|
$"bpp: {cgImage.BitsPerPixel}\n" +
|
|
$"calculated: {bytesPerRow} => {bytes}");
|
|
}
|
|
#endif
|
|
using (CGBitmapContext bitmapContext = new CGBitmapContext(
|
|
pxdata, width, height,
|
|
bitsPerComponent,
|
|
bytes,
|
|
rgbColorSpace,
|
|
CGImageAlphaInfo.NoneSkipFirst))
|
|
{
|
|
if (bitmapContext != null)
|
|
{
|
|
bitmapContext.DrawImage(new CGRect(0, 0, width, height), cgImage);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
pixelBufferImage.Unlock(CVPixelBufferLock.None);
|
|
}
|
|
|
|
// and finally append buffer to adapter
|
|
if (pixelBufferAdaptor.AssetWriterInput.ReadyForMoreMediaData && pixelBufferImage != null)
|
|
{
|
|
pixelBufferAdaptor.AppendPixelBufferWithPresentationTime(pixelBufferImage, lastTime);
|
|
}
|
|
}
|
|
|
|
var frameTime = new CMTime(ugoira.frames[i].delay, 1000);
|
|
lastTime = CMTime.Add(lastTime, frameTime);
|
|
}
|
|
writerInput.MarkAsFinished();
|
|
await videoWriter.FinishWritingAsync();
|
|
return file;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
public Task<string> ExportGif()
|
|
{
|
|
if (ugoira == null || ugoira.frames.Any(f => f.Incompleted))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
var file = Stores.GetUgoiraPath(ugoira.originalSrc, ".gif");
|
|
if (File.Exists(file))
|
|
{
|
|
File.Delete(file);
|
|
}
|
|
var fileURL = NSUrl.FromFilename(file);
|
|
|
|
var dictFile = new NSMutableDictionary();
|
|
var gifDictionaryFile = new NSMutableDictionary
|
|
{
|
|
{ ImageIO.CGImageProperties.GIFLoopCount, NSNumber.FromFloat(0f) }
|
|
};
|
|
dictFile.Add(ImageIO.CGImageProperties.GIFDictionary, gifDictionaryFile);
|
|
var dictFrame = new NSMutableDictionary();
|
|
var gifDictionaryFrame = new NSMutableDictionary
|
|
{
|
|
{ ImageIO.CGImageProperties.GIFDelayTime, NSNumber.FromFloat(ugoira.frames[0].delay / 1000f) }
|
|
};
|
|
dictFrame.Add(ImageIO.CGImageProperties.GIFDictionary, gifDictionaryFrame);
|
|
|
|
var task = new TaskCompletionSource<string>();
|
|
Xamarin.Essentials.MainThread.BeginInvokeOnMainThread(() =>
|
|
{
|
|
var images = ugoira.frames.Select(f => UIImage.FromFile(f.FilePath)).ToArray();
|
|
|
|
var imageDestination = ImageIO.CGImageDestination.Create(fileURL, MobileCoreServices.UTType.GIF, images.Length);
|
|
imageDestination.SetProperties(dictFile);
|
|
for (int i = 0; i < images.Length; i++)
|
|
{
|
|
imageDestination.AddImage(images[i].CGImage, dictFrame);
|
|
}
|
|
imageDestination.Close();
|
|
|
|
task.SetResult(file);
|
|
});
|
|
return task.Task;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
public class UgoiraEventArgs : EventArgs
|
|
{
|
|
public IllustDetailItem DetailItem { get; set; }
|
|
public ImageSource Image { get; set; }
|
|
public int FrameIndex { get; set; }
|
|
}
|
|
}
|
|
|