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 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(); 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 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 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(); 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; } } }