├── .gitignore ├── .vs ├── LinuxNote │ ├── DesignTimeBuild │ │ └── .dtbcache.v2 │ └── v16 │ │ └── .suo ├── ProjectSettings.json ├── VSWorkspaceState.json └── slnx.sqlite ├── Encoder ├── Ditherer.cs ├── Encoder.cs ├── FlipnoteEncoder.cs ├── FrameSplitter.cs └── TColor.cs ├── Encoders └── Mp4Encoder.cs ├── LinuxNote.csproj ├── LinuxNote.csproj.user ├── LinuxNote.sln ├── Program.cs ├── Properties └── PublishProfiles │ ├── FolderProfile.pubxml │ └── FolderProfile.pubxml.user ├── README.md ├── Utilities ├── ArrayExtensions.cs ├── EncodeConfig.cs └── MathUtils.cs └── runtimeconfig.template.json /.gitignore: -------------------------------------------------------------------------------- 1 | /.vscode 2 | /bin 3 | /config.json 4 | /obj 5 | DummyFlipnote 6 | ffmpeg 7 | -------------------------------------------------------------------------------- /.vs/LinuxNote/DesignTimeBuild/.dtbcache.v2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RinLovesYou/LinuxNote/95f18ef1d5bd3b4a8879120f14d5cbf46b080a96/.vs/LinuxNote/DesignTimeBuild/.dtbcache.v2 -------------------------------------------------------------------------------- /.vs/LinuxNote/v16/.suo: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RinLovesYou/LinuxNote/95f18ef1d5bd3b4a8879120f14d5cbf46b080a96/.vs/LinuxNote/v16/.suo -------------------------------------------------------------------------------- /.vs/ProjectSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "CurrentProjectSetting": null 3 | } -------------------------------------------------------------------------------- /.vs/VSWorkspaceState.json: -------------------------------------------------------------------------------- 1 | { 2 | "ExpandedNodes": [ 3 | "" 4 | ], 5 | "SelectedNode": "\\LinuxNote.csproj", 6 | "PreviewInSolutionExplorer": false 7 | } -------------------------------------------------------------------------------- /.vs/slnx.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RinLovesYou/LinuxNote/95f18ef1d5bd3b4a8879120f14d5cbf46b080a96/.vs/slnx.sqlite -------------------------------------------------------------------------------- /Encoder/Ditherer.cs: -------------------------------------------------------------------------------- 1 | using LinuxNote.Utilities; 2 | using SixLabors.ImageSharp; 3 | using SixLabors.ImageSharp.PixelFormats; 4 | using SixLabors.ImageSharp.Processing; 5 | using SixLabors.ImageSharp.Processing.Processors.Dithering; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.IO; 9 | using System.Linq; 10 | 11 | namespace LinuxNote.Encoder 12 | { 13 | public class Ditherer 14 | { 15 | private EncodeConfig Config { get; set; } 16 | 17 | public Ditherer() 18 | { 19 | Config = Program.FlipnoteConfig; 20 | } 21 | 22 | public bool FullColor() 23 | { 24 | var Folder = Config.InputFolder; 25 | var files = Directory.EnumerateFiles(Folder, "*.png"); 26 | var filenames = files.ToArray(); 27 | MathUtils.NumericalSort(filenames); 28 | 29 | var contrast = Config.Contrast; 30 | 31 | IDither DitheringType = null; 32 | switch (Config.DitheringMode) 33 | { 34 | case 1: 35 | DitheringType = KnownDitherings.Bayer8x8; 36 | break; 37 | case 2: 38 | DitheringType = KnownDitherings.Bayer4x4; 39 | break; 40 | case 3: 41 | DitheringType = KnownDitherings.Bayer2x2; 42 | break; 43 | case 4: 44 | DitheringType = KnownDitherings.FloydSteinberg; 45 | break; 46 | case 5: 47 | DitheringType = KnownDitherings.Atkinson; 48 | break; 49 | case 6: 50 | DitheringType = KnownDitherings.Burks; 51 | break; 52 | case 7: 53 | DitheringType = KnownDitherings.JarvisJudiceNinke; 54 | break; 55 | case 8: 56 | DitheringType = KnownDitherings.StevensonArce; 57 | break; 58 | case 9: 59 | DitheringType = KnownDitherings.Sierra2; 60 | break; 61 | case 10: 62 | DitheringType = KnownDitherings.Sierra3; 63 | break; 64 | case 11: 65 | DitheringType = KnownDitherings.SierraLite; 66 | break; 67 | case 12: 68 | DitheringType = KnownDitherings.Stucki; 69 | break; 70 | case 13: 71 | DitheringType = KnownDitherings.Ordered3x3; 72 | break; 73 | default: 74 | DitheringType = null; 75 | break; 76 | } 77 | 78 | List colors = new List(); 79 | colors.Add(Color.Red); 80 | colors.Add(Color.Black); 81 | colors.Add(Color.White); 82 | colors.Add(Color.Blue); 83 | Directory.CreateDirectory("tmp"); 84 | if (DitheringType != null) 85 | { 86 | for (int i = 0; i < filenames.Length; i++) 87 | { 88 | Image image; 89 | try 90 | { 91 | image = Image.Load(filenames[i + 1]); 92 | } 93 | catch (Exception e) 94 | { 95 | continue; 96 | } 97 | 98 | Image bw = image.Clone(); 99 | bw.Mutate(x => 100 | { 101 | if (contrast != 0) 102 | { 103 | x.Contrast(contrast); 104 | } 105 | x.BinaryDither(DitheringType); 106 | }); 107 | bw.SaveAsPng($"tmp/frame_{i+1}.png"); 108 | bw.Dispose(); 109 | 110 | image.Mutate(x => 111 | { 112 | if (contrast != 0) 113 | { 114 | x.Contrast(contrast); 115 | } 116 | //x.BinaryDither(DitheringType); 117 | var Palette = new ReadOnlyMemory(colors.ToArray()); 118 | 119 | x.Dither(DitheringType, Palette); 120 | }); 121 | 122 | image.SaveAsPng($"frames/frame_{i+1}.png"); 123 | image.Dispose(); 124 | } 125 | } 126 | 127 | return true; 128 | } 129 | 130 | public void ThreeColor(int WhichColor) 131 | { 132 | var Folder = Config.InputFolder; 133 | var files = Directory.EnumerateFiles(Folder, "*.png"); 134 | var filenames = files.ToArray(); 135 | MathUtils.NumericalSort(filenames); 136 | 137 | var contrast = Config.Contrast; 138 | 139 | IDither DitheringType = null; 140 | switch (Config.DitheringMode) 141 | { 142 | case 1: 143 | DitheringType = KnownDitherings.Bayer8x8; 144 | break; 145 | case 2: 146 | DitheringType = KnownDitherings.Bayer4x4; 147 | break; 148 | case 3: 149 | DitheringType = KnownDitherings.Bayer2x2; 150 | break; 151 | case 4: 152 | DitheringType = KnownDitherings.FloydSteinberg; 153 | break; 154 | case 5: 155 | DitheringType = KnownDitherings.Atkinson; 156 | break; 157 | case 6: 158 | DitheringType = KnownDitherings.Burks; 159 | break; 160 | case 7: 161 | DitheringType = KnownDitherings.JarvisJudiceNinke; 162 | break; 163 | case 8: 164 | DitheringType = KnownDitherings.StevensonArce; 165 | break; 166 | case 9: 167 | DitheringType = KnownDitherings.Sierra2; 168 | break; 169 | case 10: 170 | DitheringType = KnownDitherings.Sierra3; 171 | break; 172 | case 11: 173 | DitheringType = KnownDitherings.SierraLite; 174 | break; 175 | case 12: 176 | DitheringType = KnownDitherings.Stucki; 177 | break; 178 | case 13: 179 | DitheringType = KnownDitherings.Ordered3x3; 180 | break; 181 | default: 182 | //this one is my favorite :) 183 | DitheringType = KnownDitherings.Bayer8x8; 184 | break; 185 | } 186 | 187 | List colors = new List(); 188 | switch (WhichColor) 189 | { 190 | case 2: colors.Add(Color.Red); break; 191 | case 3: colors.Add(Color.Blue); break; 192 | default: colors.Add(Color.Red); break; 193 | 194 | } 195 | colors.Add(Color.Black); 196 | colors.Add(Color.White); 197 | 198 | for (int i = 0; i < filenames.Length; i++) 199 | { 200 | Image image; 201 | try 202 | { 203 | image = Image.Load(filenames[i + 1]); 204 | } 205 | catch (Exception e) 206 | { 207 | continue; 208 | } 209 | image.Mutate(x => 210 | { 211 | if (contrast != 0) 212 | { 213 | x.Contrast(contrast); 214 | } 215 | //x.BinaryDither(DitheringType); 216 | var Palette = new ReadOnlyMemory(colors.ToArray()); 217 | 218 | 219 | x.Dither(DitheringType, Palette); 220 | }); 221 | image.SaveAsPng($"frames/frame_{i+1}.png"); 222 | image.Dispose(); 223 | } 224 | } 225 | 226 | public bool TwoColor() 227 | { 228 | var Folder = Config.InputFolder; 229 | var files = Directory.EnumerateFiles(Folder, "*.png"); 230 | var filenames = files.ToArray(); 231 | MathUtils.NumericalSort(filenames); 232 | 233 | var contrast = Config.Contrast; 234 | 235 | IDither DitheringType = null; 236 | switch (Config.DitheringMode) 237 | { 238 | case 0: 239 | break; 240 | case 1: 241 | DitheringType = KnownDitherings.Bayer8x8; 242 | break; 243 | case 2: 244 | DitheringType = KnownDitherings.Bayer4x4; 245 | break; 246 | case 3: 247 | DitheringType = KnownDitherings.Bayer2x2; 248 | break; 249 | case 4: 250 | DitheringType = KnownDitherings.FloydSteinberg; 251 | break; 252 | case 5: 253 | DitheringType = KnownDitherings.Atkinson; 254 | break; 255 | case 6: 256 | DitheringType = KnownDitherings.Burks; 257 | break; 258 | case 7: 259 | DitheringType = KnownDitherings.JarvisJudiceNinke; 260 | break; 261 | case 8: 262 | DitheringType = KnownDitherings.StevensonArce; 263 | break; 264 | case 9: 265 | DitheringType = KnownDitherings.Sierra2; 266 | break; 267 | case 10: 268 | DitheringType = KnownDitherings.Sierra3; 269 | break; 270 | case 11: 271 | DitheringType = KnownDitherings.SierraLite; 272 | break; 273 | case 12: 274 | DitheringType = KnownDitherings.Stucki; 275 | break; 276 | case 13: 277 | DitheringType = KnownDitherings.Ordered3x3; 278 | break; 279 | default: 280 | //this one is my favorite :) 281 | DitheringType = KnownDitherings.Bayer8x8; 282 | break; 283 | } 284 | 285 | List colors = new List(); 286 | 287 | for (int i = 0; i < filenames.Length; i++) 288 | { 289 | Image image; 290 | try 291 | { 292 | image = Image.Load(filenames[i + 1]); 293 | } 294 | catch (Exception e) 295 | { 296 | continue; 297 | } 298 | image.Mutate(x => 299 | { 300 | if (contrast != 0) 301 | { 302 | x.Contrast(contrast); 303 | } 304 | if(DitheringType == null) 305 | { 306 | x.AdaptiveThreshold(); 307 | } else 308 | { 309 | x.BinaryDither(DitheringType); 310 | } 311 | 312 | }); 313 | image.SaveAsPng($"{Folder}/frame_{i+1}.png"); 314 | image.Dispose(); 315 | } 316 | return true; 317 | } 318 | 319 | } 320 | } 321 | -------------------------------------------------------------------------------- /Encoder/Encoder.cs: -------------------------------------------------------------------------------- 1 | using FFMpegCore; 2 | using PPMLib; 3 | using LinuxNote.Utilities; 4 | using LinuxNote; 5 | using LinuxNote.Encoder; 6 | using SixLabors.ImageSharp; 7 | using SixLabors.ImageSharp.PixelFormats; 8 | using SixLabors.ImageSharp.Processing; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.IO; 12 | using System.Linq; 13 | 14 | namespace LinuxNote.Encoder 15 | { 16 | public class Encoder 17 | { 18 | private EncodeConfig Config = Program.FlipnoteConfig; 19 | 20 | public Encoder() 21 | { 22 | Config = Program.FlipnoteConfig; 23 | } 24 | 25 | public bool PrepareAudio() 26 | { 27 | var Folder = Config.InputFolder; 28 | var filename = Config.InputFilename; 29 | try 30 | { 31 | Console.ForegroundColor = ConsoleColor.Gray; 32 | Console.Write("Writing Audio..."); 33 | 34 | FFMpegArguments 35 | .FromFileInput($"{Folder}/{filename}", true) 36 | .OutputToFile($"{Folder}/{filename}.wav", true, o => o 37 | .WithCustomArgument("-ac 1 -ar 8192")) 38 | .ProcessSynchronously(); 39 | 40 | Console.CursorLeft = 0; 41 | Console.ForegroundColor = ConsoleColor.Green; 42 | Console.WriteLine("Audio Written! "); 43 | return true; 44 | } 45 | catch (Exception e) 46 | { 47 | Console.CursorLeft = 16; 48 | Console.ForegroundColor = ConsoleColor.Red; 49 | Console.WriteLine("Could not write audio!"); 50 | return false; 51 | } 52 | } 53 | 54 | public int PrepareFrames() 55 | { 56 | switch (Config.ColorMode) 57 | { 58 | case 1: 59 | { 60 | FrameSplitter splitter = new FrameSplitter(); 61 | splitter.SplitFrames(Config.InputFolder, Config.InputFilename); 62 | 63 | Ditherer ditherer = new Ditherer(); 64 | ditherer.TwoColor(); 65 | break; 66 | } 67 | case 2: 68 | { 69 | FrameSplitter splitter = new FrameSplitter(); 70 | splitter.SplitFrames(Config.InputFolder, Config.InputFilename); 71 | 72 | Ditherer ditherer = new Ditherer(); 73 | ditherer.ThreeColor(2); 74 | break; 75 | } 76 | case 3: 77 | { 78 | FrameSplitter splitter = new FrameSplitter(); 79 | splitter.SplitFrames(Config.InputFolder, Config.InputFilename); 80 | 81 | Ditherer ditherer = new Ditherer(); 82 | ditherer.ThreeColor(3); 83 | break; 84 | } 85 | case 4: 86 | { 87 | FrameSplitter splitter = new FrameSplitter(); 88 | splitter.SplitFrames(Config.InputFolder, Config.InputFilename); 89 | 90 | Ditherer ditherer = new Ditherer(); 91 | ditherer.FullColor(); 92 | break; 93 | } 94 | case 5: 95 | { 96 | FrameSplitter splitter = new FrameSplitter(); 97 | splitter.SplitFrames(Config.InputFolder, Config.InputFilename); 98 | 99 | Ditherer ditherer = new Ditherer(); 100 | ditherer.FullColor(); 101 | break; 102 | } 103 | default: 104 | { 105 | FrameSplitter splitter = new FrameSplitter(); 106 | splitter.SplitFrames(Config.InputFolder, Config.InputFilename); 107 | 108 | Ditherer ditherer = new Ditherer(); 109 | ditherer.TwoColor(); 110 | break; 111 | } 112 | } 113 | 114 | return Directory.EnumerateFiles(Config.InputFolder, "*.png").ToArray().Length; 115 | } 116 | 117 | public IEnumerable AutoColorFrames(string Folder) 118 | { 119 | var images = Directory.GetFiles($"{Folder}", "*.png"); 120 | MathUtils.NumericalSort(images); 121 | var PreviousColor = PenColor.Blue; 122 | PPMFrame PreviousFrame = null; 123 | if (Config.DitheringMode != 0) 124 | { 125 | for (int i = 0; i < images.Length; i++) 126 | { 127 | PPMFrame frame = new PPMFrame(); 128 | 129 | int black = 0; 130 | int white = 0; 131 | int red = 0; 132 | int blue = 0; 133 | Rgba32 ColorBlue = Rgba32.ParseHex("#0000ff"); 134 | Rgba32 ColorRed = Rgba32.ParseHex("#FF0000"); 135 | Rgba32 ColorBlack = Rgba32.ParseHex("#000000"); 136 | Rgba32 ColorWhite = Rgba32.ParseHex("FFFFFF"); 137 | 138 | var ColorImage = Image.Load($"{Folder}/frame_{i+1}.png"); 139 | Image GrayImage = null; 140 | try 141 | { 142 | GrayImage = Image.Load($"tmp/frame_{i+1}.png"); 143 | } 144 | catch (Exception e) 145 | { 146 | GrayImage = Image.Load($"tmp/frame_{i - 1}.png"); 147 | } 148 | 149 | GrayImage.Mutate(x => x.BinaryThreshold(0.5f)); 150 | 151 | //Set all the funny pixels 152 | for (int x = 0; x < 256; x++) 153 | { 154 | for (int y = 0; y < 192; y++) 155 | { 156 | frame.Layer1.SetLineEncoding(y, LineEncoding.CodedLine); 157 | frame.Layer2.SetLineEncoding(y, LineEncoding.CodedLine); 158 | var ColorPixel = ColorImage[x, y]; 159 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 160 | 161 | var GrayPixel = GrayImage[x, y]; 162 | var GrayLuminance = (0.299 * GrayPixel.R + 0.587 * GrayPixel.G + 0.114 * GrayPixel.B) / 255; 163 | 164 | var Red = ColorPixel == ColorRed; 165 | var Blue = ColorPixel == ColorBlue; 166 | var White = ColorPixel == ColorWhite; 167 | var Black = ColorPixel == ColorBlack; 168 | 169 | if (Red) 170 | { 171 | frame.Layer1[x, y] = false; 172 | frame.Layer2[x, y] = true; 173 | red++; 174 | } 175 | if (Blue) 176 | { 177 | frame.Layer1[x, y] = false; 178 | frame.Layer2[x, y] = true; 179 | blue++; 180 | } 181 | if (White) 182 | { 183 | frame.Layer1[x, y] = false; 184 | frame.Layer2[x, y] = false; 185 | white++; 186 | } 187 | if (Black) 188 | { 189 | frame.Layer1[x, y] = true; 190 | frame.Layer2[x, y] = false; 191 | black++; 192 | } 193 | } 194 | } 195 | 196 | byte header = 0; 197 | if (i == 0) 198 | { 199 | header |= 1 << 7; // first frame, no diffing 200 | } 201 | 202 | for (int x = 0; x < 256; x++) 203 | { 204 | for (int y = 0; y < 192; y++) 205 | { 206 | frame.Layer1.SetLineEncoding(y, LineEncoding.CodedLine); 207 | frame.Layer2.SetLineEncoding(y, LineEncoding.CodedLine); 208 | var ColorPixel = ColorImage[x, y]; 209 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 210 | 211 | var GrayPixel = GrayImage[x, y]; 212 | var GrayLuminance = (0.299 * GrayPixel.R + 0.587 * GrayPixel.G + 0.114 * GrayPixel.B) / 255; 213 | 214 | var Red = ColorPixel == ColorRed; 215 | var Blue = ColorPixel == ColorBlue; 216 | var White = ColorPixel == ColorWhite; 217 | var Black = ColorPixel == ColorBlack; 218 | 219 | if (red >= blue && PreviousColor == PenColor.Blue) 220 | { 221 | if (Blue) 222 | { 223 | if (GrayLuminance > 0.5f) 224 | { 225 | frame.Layer1[x, y] = true; 226 | frame.Layer2[x, y] = false; 227 | black++; 228 | } 229 | else 230 | { 231 | frame.Layer1[x, y] = false; 232 | frame.Layer2[x, y] = false; 233 | } 234 | } 235 | } 236 | else if (blue > red && PreviousColor == PenColor.Red) 237 | { 238 | if (Red) 239 | { 240 | if (GrayLuminance > 0.5f) 241 | { 242 | frame.Layer1[x, y] = true; 243 | frame.Layer2[x, y] = false; 244 | black++; 245 | } 246 | else 247 | { 248 | frame.Layer1[x, y] = false; 249 | frame.Layer2[x, y] = false; 250 | } 251 | } 252 | } 253 | } 254 | } 255 | 256 | if (red >= blue) 257 | { 258 | header |= (byte)((int)PenColor.Red << 3); 259 | PreviousColor = PenColor.Blue; 260 | } 261 | else 262 | { 263 | header |= (byte)((int)PenColor.Blue << 3); 264 | PreviousColor = PenColor.Red; 265 | } 266 | 267 | header |= (byte)(((int)PenColor.Inverted) << 1); 268 | header |= (byte)(white > black ? 1 : 0); 269 | frame._firstByteHeader = header; 270 | 271 | // reverse paper color if black > white, saves the tiniest amount of space. 272 | if (black > white) 273 | { 274 | for (int x = 0; x < 256; x++) 275 | { 276 | for (int y = 0; y < 192; y++) 277 | { 278 | var ColorPixel = ColorImage[x, y]; 279 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 280 | 281 | var GrayPixel = GrayImage[x, y]; 282 | var GrayLuminance = (0.299 * GrayPixel.R + 0.587 * GrayPixel.G + 0.114 * GrayPixel.B) / 255; 283 | 284 | var Red = ColorPixel == ColorRed; 285 | var Blue = ColorPixel == ColorBlue; 286 | var White = ColorPixel == ColorWhite; 287 | var Black = ColorPixel == ColorBlack; 288 | 289 | if (Red) 290 | { 291 | if (PreviousColor == PenColor.Red) 292 | { 293 | if (GrayLuminance > 0.5f) 294 | { 295 | frame.Layer1[x, y] = true; 296 | frame.Layer2[x, y] = false; 297 | black++; 298 | } 299 | else 300 | { 301 | frame.Layer1[x, y] = false; 302 | frame.Layer2[x, y] = false; 303 | } 304 | } 305 | else 306 | { 307 | frame.Layer1[x, y] = false; 308 | frame.Layer2[x, y] = true; 309 | } 310 | } 311 | if (Blue) 312 | { 313 | if (PreviousColor == PenColor.Blue) 314 | { 315 | if (GrayLuminance > 0.5f) 316 | { 317 | frame.Layer1[x, y] = true; 318 | frame.Layer2[x, y] = false; 319 | black++; 320 | } 321 | else 322 | { 323 | frame.Layer1[x, y] = false; 324 | frame.Layer2[x, y] = false; 325 | } 326 | } 327 | else 328 | { 329 | frame.Layer1[x, y] = false; 330 | frame.Layer2[x, y] = true; 331 | } 332 | } 333 | if (White) 334 | { 335 | frame.Layer1[x, y] = true; 336 | frame.Layer2[x, y] = false; 337 | } 338 | if (Black) 339 | { 340 | frame.Layer1[x, y] = false; 341 | frame.Layer2[x, y] = false; 342 | } 343 | } 344 | } 345 | } 346 | 347 | frame.PaperColor = (PaperColor)(frame._firstByteHeader % 2); 348 | frame.Layer1.PenColor = (PenColor)((frame._firstByteHeader >> 1) & 3); 349 | frame.Layer2.PenColor = (PenColor)((frame._firstByteHeader >> 3) & 3); 350 | if (i == 0) 351 | { 352 | PreviousFrame = frame; 353 | yield return frame; 354 | } 355 | else 356 | { 357 | var temp = frame.CreateDiff0(PreviousFrame); 358 | PreviousFrame = frame; 359 | yield return temp; 360 | } 361 | } 362 | } 363 | else 364 | { 365 | for (int i = 0; i < images.Length; i++) 366 | { 367 | PPMFrame frame = new PPMFrame(); 368 | 369 | int black = 0; 370 | int white = 0; 371 | int red = 0; 372 | int blue = 0; 373 | Rgba32 ColorBlue = Rgba32.ParseHex("#0000ff"); 374 | Rgba32 ColorRed = Rgba32.ParseHex("#FF0000"); 375 | Rgba32 ColorBlack = Rgba32.ParseHex("#000000"); 376 | Rgba32 ColorWhite = Rgba32.ParseHex("FFFFFF"); 377 | 378 | var ColorImage = Image.Load($"{Folder}/frame_{i+1}.png"); 379 | Image GrayImage = null; 380 | try 381 | { 382 | GrayImage = Image.Load($"tmp/frame_{i+1}.png"); 383 | } 384 | catch (Exception e) 385 | { 386 | GrayImage = Image.Load($"tmp/frame_{i - 1}.png"); 387 | } 388 | 389 | //Set all the funny pixels 390 | for (int x = 0; x < 256; x++) 391 | { 392 | for (int y = 0; y < 192; y++) 393 | { 394 | frame.Layer1.SetLineEncoding(y, LineEncoding.CodedLine); 395 | frame.Layer2.SetLineEncoding(y, LineEncoding.CodedLine); 396 | var ColorPixel = ColorImage[x, y]; 397 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 398 | 399 | var GrayPixel = GrayImage[x, y]; 400 | var GrayLuminance = (0.299 * GrayPixel.R + 0.587 * GrayPixel.G + 0.114 * GrayPixel.B) / 255; 401 | 402 | var Red = GrayPixel == ColorRed; 403 | var Blue = GrayPixel == ColorBlue; 404 | var White = GrayPixel == ColorWhite; 405 | var Black = GrayPixel == ColorBlack; 406 | var GWhite = ColorPixel == ColorWhite; 407 | var GBlack = ColorPixel == ColorBlack; 408 | 409 | if (Red) 410 | { 411 | frame.Layer1[x, y] = false; 412 | frame.Layer2[x, y] = true; 413 | red++; 414 | } 415 | if (Blue) 416 | { 417 | frame.Layer1[x, y] = false; 418 | frame.Layer2[x, y] = true; 419 | blue++; 420 | } 421 | if (White && GWhite) 422 | { 423 | frame.Layer1[x, y] = false; 424 | 425 | white++; 426 | } 427 | if (GBlack) 428 | { 429 | frame.Layer1[x, y] = true; 430 | 431 | black++; 432 | } 433 | } 434 | } 435 | 436 | byte header = 0; 437 | if (i == 0) 438 | { 439 | header |= 1 << 7; // no frame diffing 440 | } 441 | 442 | for (int x = 0; x < 256; x++) 443 | { 444 | for (int y = 0; y < 192; y++) 445 | { 446 | frame.Layer1.SetLineEncoding(y, LineEncoding.CodedLine); 447 | frame.Layer2.SetLineEncoding(y, LineEncoding.CodedLine); 448 | var ColorPixel = ColorImage[x, y]; 449 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 450 | 451 | var GrayPixel = GrayImage[x, y]; 452 | var GrayLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 453 | 454 | var Red = GrayPixel == ColorRed; 455 | var Blue = GrayPixel == ColorBlue; 456 | var White = GrayPixel == ColorWhite; 457 | var Black = GrayPixel == ColorBlack; 458 | var GWhite = ColorPixel == ColorWhite; 459 | var GBlack = ColorPixel == ColorBlack; 460 | 461 | if (red >= blue && PreviousColor == PenColor.Blue) 462 | { 463 | if (Blue) 464 | { 465 | if (GrayLuminance > 0.5f) 466 | { 467 | frame.Layer1[x, y] = true; 468 | frame.Layer2[x, y] = false; 469 | black++; 470 | } 471 | else 472 | { 473 | frame.Layer1[x, y] = false; 474 | frame.Layer2[x, y] = false; 475 | } 476 | } 477 | } 478 | else if (blue > red && PreviousColor == PenColor.Red) 479 | { 480 | if (Red) 481 | { 482 | if (GrayLuminance > 0.5f) 483 | { 484 | frame.Layer1[x, y] = true; 485 | frame.Layer2[x, y] = false; 486 | black++; 487 | } 488 | else 489 | { 490 | frame.Layer1[x, y] = false; 491 | frame.Layer2[x, y] = false; 492 | } 493 | } 494 | } 495 | } 496 | } 497 | 498 | if (red >= blue) 499 | { 500 | header |= (byte)((int)PenColor.Red << 3); 501 | PreviousColor = PenColor.Blue; 502 | } 503 | else 504 | { 505 | header |= (byte)((int)PenColor.Blue << 3); 506 | PreviousColor = PenColor.Red; 507 | } 508 | 509 | header |= (byte)(((int)PenColor.Inverted) << 1); 510 | header |= (byte)(1); 511 | frame._firstByteHeader = header; 512 | 513 | frame.PaperColor = (PaperColor)(frame._firstByteHeader % 2); 514 | frame.Layer1.PenColor = (PenColor)((frame._firstByteHeader >> 1) & 3); 515 | frame.Layer2.PenColor = (PenColor)((frame._firstByteHeader >> 3) & 3); 516 | if (i == 0) 517 | { 518 | PreviousFrame = frame; 519 | yield return frame; 520 | } 521 | else 522 | { 523 | var temp = frame.CreateDiff0(PreviousFrame); 524 | PreviousFrame = frame; 525 | yield return temp; 526 | } 527 | } 528 | } 529 | } 530 | 531 | public IEnumerable FullColorFrames(string Folder) 532 | { 533 | var images = Directory.GetFiles($"{Folder}", "*.png"); 534 | MathUtils.NumericalSort(images); 535 | var PreviousColor = PenColor.Red; 536 | PPMFrame PreviousFrame = null; 537 | for (int i = 0; i < images.Length; i++) 538 | { 539 | PPMFrame frame = new PPMFrame(); 540 | 541 | int black = 0; 542 | int white = 0; 543 | Rgba32 ColorBlue = Rgba32.ParseHex("#0000ff"); 544 | Rgba32 ColorRed = Rgba32.ParseHex("#FF0000"); 545 | Rgba32 ColorBlack = Rgba32.ParseHex("#000000"); 546 | Rgba32 ColorWhite = Rgba32.ParseHex("FFFFFF"); 547 | 548 | var ColorImage = Image.Load($"{Folder}/frame_{i+1}.png"); 549 | Image GrayImage = null; 550 | try 551 | { 552 | GrayImage = Image.Load($"tmp/frame_{i+1}.png"); 553 | } 554 | catch (Exception e) 555 | { 556 | GrayImage = Image.Load($"tmp/frame_{i}.png"); 557 | } 558 | 559 | GrayImage.Mutate(x => x.BinaryThreshold(0.5f)); 560 | 561 | //Set all the funny pixels 562 | for (int x = 0; x < 256; x++) 563 | { 564 | for (int y = 0; y < 192; y++) 565 | { 566 | frame.Layer1.SetLineEncoding(y, LineEncoding.CodedLine); 567 | frame.Layer2.SetLineEncoding(y, LineEncoding.CodedLine); 568 | var ColorPixel = ColorImage[x, y]; 569 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 570 | 571 | var GrayPixel = GrayImage[x, y]; 572 | var GrayLuminance = (0.299 * GrayPixel.R + 0.587 * GrayPixel.G + 0.114 * GrayPixel.B) / 255; 573 | 574 | var Red = ColorPixel == ColorRed; 575 | var Blue = ColorPixel == ColorBlue; 576 | var White = ColorPixel == ColorWhite; 577 | var Black = ColorPixel == ColorBlack; 578 | 579 | if (Red) 580 | { 581 | if (PreviousColor == PenColor.Blue) 582 | { 583 | if (GrayLuminance < 0.5f) 584 | { 585 | frame.Layer1[x, y] = true; 586 | frame.Layer2[x, y] = false; 587 | black++; 588 | } 589 | else white++; 590 | } 591 | else 592 | { 593 | frame.Layer1[x, y] = false; 594 | frame.Layer2[x, y] = true; 595 | } 596 | } 597 | if (Blue) 598 | { 599 | if (PreviousColor == PenColor.Red) 600 | { 601 | if (GrayLuminance < 0.5f) 602 | { 603 | frame.Layer1[x, y] = true; 604 | frame.Layer2[x, y] = false; 605 | black++; 606 | } 607 | else white++; 608 | } 609 | else 610 | { 611 | frame.Layer1[x, y] = false; 612 | frame.Layer2[x, y] = true; 613 | } 614 | } 615 | if (White) 616 | { 617 | frame.Layer1[x, y] = false; 618 | frame.Layer2[x, y] = false; 619 | white++; 620 | } 621 | if (Black) 622 | { 623 | frame.Layer1[x, y] = true; 624 | frame.Layer2[x, y] = false; 625 | black++; 626 | } 627 | } 628 | } 629 | 630 | byte header = 0; 631 | 632 | if (i == 0) 633 | { 634 | header |= 1 << 7; // no frame diffing 635 | } 636 | 637 | if (PreviousColor == PenColor.Red) 638 | { 639 | header |= (byte)((int)PenColor.Red << 3); 640 | PreviousColor = PenColor.Blue; 641 | } 642 | else 643 | { 644 | header |= (byte)((int)PenColor.Blue << 3); 645 | PreviousColor = PenColor.Red; 646 | } 647 | 648 | header |= (byte)(((int)PenColor.Inverted) << 1); 649 | header |= (byte)(white > black ? 1 : 0); 650 | frame._firstByteHeader = header; 651 | 652 | if (black > white) 653 | { 654 | for (int x = 0; x < 256; x++) 655 | { 656 | for (int y = 0; y < 192; y++) 657 | { 658 | var ColorPixel = ColorImage[x, y]; 659 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 660 | 661 | var GrayPixel = GrayImage[x, y]; 662 | var GrayLuminance = (0.299 * GrayPixel.R + 0.587 * GrayPixel.G + 0.114 * GrayPixel.B) / 255; 663 | 664 | var Red = ColorPixel == ColorRed; 665 | var Blue = ColorPixel == ColorBlue; 666 | var White = ColorPixel == ColorWhite; 667 | var Black = ColorPixel == ColorBlack; 668 | 669 | if (Red) 670 | { 671 | if (PreviousColor == PenColor.Red) 672 | { 673 | if (GrayLuminance > 0.5f) 674 | { 675 | frame.Layer1[x, y] = true; 676 | frame.Layer2[x, y] = false; 677 | black++; 678 | } 679 | else 680 | { 681 | frame.Layer1[x, y] = false; 682 | frame.Layer2[x, y] = false; 683 | } 684 | } 685 | else 686 | { 687 | frame.Layer1[x, y] = false; 688 | frame.Layer2[x, y] = true; 689 | } 690 | } 691 | if (Blue) 692 | { 693 | if (PreviousColor == PenColor.Blue) 694 | { 695 | if (GrayLuminance > 0.5f) 696 | { 697 | frame.Layer1[x, y] = true; 698 | frame.Layer2[x, y] = false; 699 | black++; 700 | } 701 | else 702 | { 703 | frame.Layer1[x, y] = false; 704 | frame.Layer2[x, y] = false; 705 | } 706 | } 707 | else 708 | { 709 | frame.Layer1[x, y] = false; 710 | frame.Layer2[x, y] = true; 711 | } 712 | } 713 | if (White) 714 | { 715 | frame.Layer1[x, y] = true; 716 | frame.Layer2[x, y] = false; 717 | } 718 | if (Black) 719 | { 720 | frame.Layer1[x, y] = false; 721 | frame.Layer2[x, y] = false; 722 | } 723 | } 724 | } 725 | } 726 | 727 | frame.PaperColor = (PaperColor)(frame._firstByteHeader % 2); 728 | frame.Layer1.PenColor = (PenColor)((frame._firstByteHeader >> 1) & 3); 729 | frame.Layer2.PenColor = (PenColor)((frame._firstByteHeader >> 3) & 3); 730 | if (i == 0) 731 | { 732 | PreviousFrame = frame; 733 | yield return frame; 734 | } 735 | else 736 | { 737 | var temp = frame.CreateDiff0(PreviousFrame); 738 | PreviousFrame = frame; 739 | yield return temp; 740 | } 741 | } 742 | } 743 | 744 | public IEnumerable ThreeColorFrames(string Folder, PenColor color) 745 | { 746 | var images = Directory.GetFiles($"{Folder}", "*.png"); 747 | MathUtils.NumericalSort(images); 748 | PPMFrame PreviousFrame = null; 749 | for (int i = 0; i < images.Length; i++) 750 | { 751 | PPMFrame frame = new PPMFrame(); 752 | 753 | Rgba32 ColorBlue = Rgba32.ParseHex("#0000ff"); 754 | Rgba32 ColorRed = Rgba32.ParseHex("#FF0000"); 755 | Rgba32 ColorBlack = Rgba32.ParseHex("#000000"); 756 | Rgba32 ColorWhite = Rgba32.ParseHex("FFFFFF"); 757 | Image ColorImage; 758 | try 759 | { 760 | ColorImage = Image.Load($"{Folder}/frame_{i + 1}.png"); 761 | } 762 | catch (Exception e) 763 | { 764 | ColorImage = Image.Load($"{Folder}/frame_{i}.png"); 765 | } 766 | int black = 0; 767 | int white = 0; 768 | for (int x = 0; x < 256; x++) 769 | { 770 | for (int y = 0; y < 192; y++) 771 | { 772 | frame.Layer1.SetLineEncoding(y, LineEncoding.CodedLine); 773 | frame.Layer2.SetLineEncoding(y, LineEncoding.CodedLine); 774 | 775 | var ColorPixel = ColorImage[x, y]; 776 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 777 | 778 | var Red = ColorPixel == ColorRed; 779 | var Blue = ColorPixel == ColorBlue; 780 | var White = ColorPixel == ColorWhite; 781 | var Black = ColorPixel == ColorBlack; 782 | 783 | if (Red) 784 | { 785 | frame.Layer2[x, y] = true; 786 | frame.Layer1[x, y] = false; 787 | } 788 | if (Blue) 789 | { 790 | frame.Layer2[x, y] = true; 791 | frame.Layer1[x, y] = false; 792 | } 793 | if (White) 794 | { 795 | frame.Layer2[x, y] = false; 796 | frame.Layer1[x, y] = false; 797 | white++; 798 | } 799 | if (Black) 800 | { 801 | frame.Layer2[x, y] = false; 802 | frame.Layer1[x, y] = true; 803 | black++; 804 | } 805 | } 806 | } 807 | 808 | if (black > white) 809 | { 810 | for (int x = 0; x < 256; x++) 811 | { 812 | for (int y = 0; y < 192; y++) 813 | { 814 | var ColorPixel = ColorImage[x, y]; 815 | var ColorLuminance = (0.299 * ColorPixel.R + 0.587 * ColorPixel.G + 0.114 * ColorPixel.B) / 255; 816 | 817 | var Red = ColorPixel == ColorRed; 818 | var Blue = ColorPixel == ColorBlue; 819 | var White = ColorPixel == ColorWhite; 820 | var Black = ColorPixel == ColorBlack; 821 | 822 | if (Red) 823 | { 824 | frame.Layer2[x, y] = true; 825 | frame.Layer1[x, y] = false; 826 | } 827 | if (Blue) 828 | { 829 | frame.Layer2[x, y] = true; 830 | frame.Layer1[x, y] = false; 831 | } 832 | if (White) 833 | { 834 | frame.Layer2[x, y] = false; 835 | frame.Layer1[x, y] = true; 836 | } 837 | if (Black) 838 | { 839 | frame.Layer2[x, y] = false; 840 | frame.Layer1[x, y] = false; 841 | } 842 | } 843 | } 844 | } 845 | byte header = 0; 846 | if (i == 0) 847 | { 848 | header |= 1 << 7; // no frame diffing 849 | } 850 | header |= (byte)(((int)color) << 3); 851 | header |= (byte)(((int)PenColor.Inverted) << 1); 852 | header |= (byte)(white > black ? 1 : 0); 853 | frame._firstByteHeader = header; 854 | 855 | frame.PaperColor = (PaperColor)(frame._firstByteHeader % 2); 856 | frame.Layer1.PenColor = (PenColor)((frame._firstByteHeader >> 1) & 3); 857 | frame.Layer2.PenColor = (PenColor)((frame._firstByteHeader >> 3) & 3); 858 | 859 | if (i == 0) 860 | { 861 | PreviousFrame = frame; 862 | yield return frame; 863 | } 864 | else 865 | { 866 | var temp = frame.CreateDiff0(PreviousFrame); 867 | PreviousFrame = frame; 868 | yield return temp; 869 | } 870 | } 871 | } 872 | 873 | public IEnumerable BlackWhiteFrames(string Folder) 874 | { 875 | var images = Directory.GetFiles($"{Folder}", "*.png"); 876 | MathUtils.NumericalSort(images); 877 | Rgba32 ColorBlack = Rgba32.ParseHex("#000000"); 878 | Rgba32 ColorWhite = Rgba32.ParseHex("FFFFFF"); 879 | PPMFrame PreviousFrame = null; 880 | for (int i = 0; i < images.Length; i++) 881 | { 882 | PPMFrame frame = new PPMFrame(); 883 | Image ColorImage; 884 | try 885 | { 886 | ColorImage = Image.Load($"{Folder}/frame_{i + 1}.png"); 887 | } 888 | catch (Exception e) 889 | { 890 | ColorImage = Image.Load($"{Folder}/frame_{i}.png"); 891 | } 892 | 893 | int white = 0; 894 | int black = 0; 895 | for (int y = 0; y < 192; y++) 896 | { 897 | frame.Layer1.SetLineEncoding(y, LineEncoding.InvertedCodedLine); 898 | frame.Layer2.SetLineEncoding(y, LineEncoding.SkipLine); 899 | for (int x = 0; x < 256; x++) 900 | { 901 | var ColorPixel = ColorImage[x, y]; 902 | var Black = ColorPixel == ColorBlack; 903 | 904 | if (Black) 905 | { 906 | frame.Layer1[x, y] = true; 907 | 908 | black++; 909 | } 910 | else 911 | { 912 | frame.Layer1[x, y] = false; 913 | 914 | white++; 915 | } 916 | } 917 | } 918 | 919 | if (black > white) 920 | { 921 | for (int y = 0; y < 192; y++) 922 | { 923 | for (int x = 0; x < 256; x++) 924 | { 925 | var ColorPixel = ColorImage[x, y]; 926 | var Black = ColorPixel == ColorBlack; 927 | 928 | if (Black) 929 | { 930 | frame.Layer1[x, y] = false; 931 | } 932 | else 933 | { 934 | frame.Layer1[x, y] = true; 935 | } 936 | } 937 | } 938 | } 939 | 940 | byte header = 0; 941 | if (i == 0) 942 | { 943 | header |= 1 << 7; // no frame diffing 944 | } 945 | 946 | header |= (byte)(((int)PenColor.Red) << 3); 947 | header |= (byte)(((int)PenColor.Inverted) << 1); 948 | header |= (byte)((white > black ? 1 : 0) << 0); 949 | frame._firstByteHeader = header; 950 | 951 | if (i == 0) 952 | { 953 | PreviousFrame = frame; 954 | yield return frame; 955 | } 956 | else 957 | { 958 | var temp = frame.CreateDiff0(PreviousFrame); 959 | PreviousFrame = frame; 960 | yield return temp; 961 | } 962 | } 963 | } 964 | } 965 | } -------------------------------------------------------------------------------- /Encoder/FlipnoteEncoder.cs: -------------------------------------------------------------------------------- 1 | using PPMLib; 2 | using PPMLib.Extensions; 3 | using LinuxNote; 4 | using LinuxNote.Utilities; 5 | using LinuxNote.Encoder; 6 | using System; 7 | using System.IO; 8 | using System.Collections.Generic; 9 | using System.Linq; 10 | using NAudio.Wave; 11 | 12 | namespace LinuxNote.Encoder 13 | { 14 | class FlipnoteEncoder 15 | { 16 | private EncodeConfig Config { get; set; } 17 | 18 | public FlipnoteEncoder() 19 | { 20 | Config = Program.FlipnoteConfig; 21 | } 22 | 23 | public PPMFile Encode() 24 | { 25 | PPMFile Dummy = new PPMFile(); 26 | PPMFile encoded = new PPMFile(); 27 | LinuxNote.Encoder.Encoder encoder = new LinuxNote.Encoder.Encoder(); 28 | 29 | Console.ForegroundColor = ConsoleColor.Gray; 30 | Console.Write("Getting Dummy..."); 31 | try 32 | { 33 | var DummyPath = Directory.GetFiles("DummyFlipnote", "*.ppm"); 34 | Dummy.LoadFrom(DummyPath[0]); 35 | Console.CursorLeft = 0; 36 | Console.ForegroundColor = ConsoleColor.Green; 37 | Console.WriteLine("Dummy got! "); 38 | } 39 | catch (Exception e) 40 | { 41 | Console.ForegroundColor = ConsoleColor.Red; 42 | Console.WriteLine("Could not get Dummy!"); 43 | return null; 44 | } 45 | 46 | try 47 | { 48 | var audio = encoder.PrepareAudio(); 49 | encoder.PrepareFrames(); 50 | } 51 | catch (Exception e) 52 | { 53 | 54 | } 55 | 56 | 57 | List EncodedFrames = new List(); 58 | 59 | Directory.CreateDirectory("out"); 60 | try 61 | { 62 | Console.ForegroundColor = ConsoleColor.Gray; 63 | Console.Write("Dithering Frames..."); 64 | switch (Config.ColorMode) 65 | { 66 | 67 | 68 | case 1: EncodedFrames = encoder.BlackWhiteFrames(Config.InputFolder).ToList(); break; 69 | case 2: EncodedFrames = encoder.ThreeColorFrames(Config.InputFolder, PenColor.Red).ToList(); break; 70 | case 3: EncodedFrames = encoder.ThreeColorFrames(Config.InputFolder, PenColor.Blue).ToList(); break; 71 | case 4: EncodedFrames = encoder.FullColorFrames(Config.InputFolder).ToList(); break; 72 | case 5: EncodedFrames = encoder.AutoColorFrames(Config.InputFolder).ToList(); break; 73 | default: EncodedFrames = encoder.BlackWhiteFrames(Config.InputFolder).ToList(); break; 74 | } 75 | Console.CursorLeft = 0; 76 | Console.ForegroundColor = ConsoleColor.Green; 77 | Console.WriteLine("Frames Dithered! "); 78 | } 79 | catch (Exception e) 80 | { 81 | Console.CursorLeft = 0; 82 | Console.ForegroundColor = ConsoleColor.Red; 83 | Console.WriteLine("Could not dither Frames!"); 84 | } 85 | 86 | 87 | if (File.Exists($"{Config.InputFolder}/{Config.InputFilename}.wav")) 88 | { 89 | Console.ForegroundColor = ConsoleColor.Gray; 90 | Console.Write("Preparing Audio..."); 91 | using (WaveFileReader reader = new WaveFileReader($"{Config.InputFolder}/{Config.InputFilename}.wav")) 92 | { 93 | 94 | byte[] buffer = new byte[reader.Length]; 95 | int read = reader.Read(buffer, 0, buffer.Length); 96 | short[] sampleBuffer = new short[read / 2]; 97 | Buffer.BlockCopy(buffer, 0, sampleBuffer, 0, read); 98 | 99 | List bgm = new List(); 100 | AdpcmEncoder aencoder = new AdpcmEncoder(); 101 | for (int i = 0; i < sampleBuffer.Length; i += 2) 102 | { 103 | try 104 | { 105 | bgm.Add((byte)(aencoder.Encode(sampleBuffer[i]) | aencoder.Encode(sampleBuffer[i + 1]) << 4)); 106 | } 107 | catch (Exception e) 108 | { 109 | 110 | } 111 | 112 | } 113 | 114 | encoded = PPMFile.Create(Dummy.CurrentAuthor, EncodedFrames, bgm.ToArray()); 115 | } 116 | 117 | 118 | Console.CursorLeft = 0; 119 | Console.ForegroundColor = ConsoleColor.Green; 120 | Console.WriteLine("Audio Prepared! "); 121 | } 122 | else 123 | { 124 | Console.CursorLeft = 0; 125 | Console.ForegroundColor = ConsoleColor.Green; 126 | Console.WriteLine("No Audio Detected "); 127 | encoded = PPMFile.Create(Dummy.CurrentAuthor, EncodedFrames, new List().ToArray()); 128 | } 129 | 130 | Random rnd = new Random(); 131 | 132 | //encoded.Thumbnail.Buffer = CreateThumbnailW64(encoded.Frames[0]); 133 | 134 | //encoded.Save($"out/{encoded.CurrentFilename}.ppm"); 135 | 136 | return encoded; 137 | 138 | } 139 | 140 | private void w64SetPixel(byte[] raw, int x, int y, TColor color) 141 | { 142 | var val = (int)color; 143 | int offset = (8 * (y / 8) + (x / 8)) * 32 + 4 * (y % 8) + (x % 8) / 2; 144 | int nibble = x & 1; 145 | raw[offset] &= (byte)~(0x0F << (4 * nibble)); 146 | raw[offset] |= (byte)(val << (4 * nibble)); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /Encoder/FrameSplitter.cs: -------------------------------------------------------------------------------- 1 | using FFMpegCore; 2 | using LinuxNote.Utilities; 3 | using LinuxNote; 4 | using System; 5 | 6 | namespace LinuxNote.Encoder 7 | { 8 | public class FrameSplitter 9 | { 10 | private EncodeConfig Config { get; set; } 11 | 12 | public FrameSplitter() 13 | { 14 | Config = Program.FlipnoteConfig; 15 | } 16 | 17 | public bool SplitFrames(string Folder, string Filename) 18 | { 19 | try 20 | { 21 | Console.ForegroundColor = ConsoleColor.Gray; 22 | Console.Write("Splitting Frames..."); 23 | FFMpegArguments 24 | .FromFileInput($"{Folder}/{Filename}", true, o => 25 | { 26 | 27 | }) 28 | .OutputToFile($"{Folder}/frame_%d.png", true, o => 29 | { 30 | if (Config.Accurate) 31 | o.WithFramerate(30); 32 | o.Resize(new System.Drawing.Size(256, 192)); 33 | 34 | 35 | }) 36 | .ProcessSynchronously(); 37 | 38 | Console.CursorLeft = 0; 39 | Console.ForegroundColor = ConsoleColor.Green; 40 | Console.WriteLine("Frames Split! "); 41 | return true; 42 | } 43 | catch (Exception e) 44 | { 45 | Console.CursorLeft = 0; 46 | Console.ForegroundColor = ConsoleColor.Red; 47 | Console.WriteLine("Could not Split Frames! "); 48 | return false; 49 | } 50 | 51 | 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Encoder/TColor.cs: -------------------------------------------------------------------------------- 1 | namespace LinuxNote.Encoder 2 | { 3 | public enum TColor 4 | { 5 | White, 6 | Black, 7 | WhiteAgain, 8 | Gray, 9 | Rose, 10 | DarkerRose, 11 | Pink, 12 | Lime, 13 | Blue, 14 | DarkerBlue, 15 | LightBlue, 16 | LimeAgain, 17 | Magenta, 18 | LimeAGAIN, 19 | MORELIME, 20 | WHYSOMANYLIME 21 | 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /Encoders/Mp4Encoder.cs: -------------------------------------------------------------------------------- 1 |  2 | using FFMpegCore; 3 | using FFMpegCore.Enums; 4 | using PPMLib.Extensions; 5 | using PPMLib; 6 | using System; 7 | using System.IO; 8 | using System.Linq; 9 | using SixLabors.ImageSharp; 10 | 11 | namespace LinuxNote.Encoders 12 | { 13 | public class Mp4Encoder 14 | { 15 | private PPMFile Flipnote { get; set; } 16 | 17 | /// 18 | /// Simple Mp4 Encoder. Requires FFMpeg to be installed in path. 19 | /// 20 | /// 21 | public Mp4Encoder(PPMFile flipnote) 22 | { 23 | this.Flipnote = flipnote; 24 | } 25 | 26 | /// 27 | /// Encode the Mp4. 28 | /// 29 | /// Mp4 byte array 30 | public byte[] EncodeMp4() 31 | { 32 | return Encode(); 33 | } 34 | 35 | /// 36 | /// Encode the Mp4 and save it to the specified path 37 | /// 38 | /// Creates path if it doesn't exist. Doesn't save if path is "temp" 39 | /// Mp4 byte array 40 | public byte[] EncodeMp4(string path) 41 | { 42 | return Encode(path); 43 | } 44 | 45 | /// 46 | /// Encode the Mp4 with the specified scale multiplier 47 | /// 48 | /// Scale Multiplier 49 | /// Mp4 byte array 50 | public byte[] EncodeMp4(int scale) 51 | { 52 | return Encode("out", scale); 53 | } 54 | 55 | /// 56 | /// Encode the Mp4 with the specified scale and save it to the given path. 57 | /// 58 | /// Creates path if it doesn't exist. Doesn't save if path is "temp" 59 | /// Scale Multiplier 60 | /// Mp4 byte array 61 | public byte[] EncodeMp4(string path, int scale) 62 | { 63 | return Encode(path, scale); 64 | } 65 | 66 | private byte[] Encode(string path = "temp", int scale = 1) 67 | { 68 | try 69 | { 70 | if (!Directory.Exists(path)) 71 | { 72 | Directory.CreateDirectory(path); 73 | } 74 | 75 | if (!Directory.Exists($"{path}/temp")) 76 | { 77 | Directory.CreateDirectory($"{path}/temp"); 78 | } 79 | else 80 | { 81 | Cleanup(path); 82 | } 83 | 84 | 85 | for (int i = 0; i < Flipnote.FrameCount; i++) 86 | { 87 | try 88 | { 89 | PPMRenderer.GetFrameBitmap(Flipnote.Frames[i]).SaveAsPng($"{path}/temp/frame_{i}.png"); 90 | 91 | } 92 | catch (Exception e) 93 | { 94 | Console.WriteLine(e.Message); 95 | Console.WriteLine(e.StackTrace); 96 | } 97 | 98 | } 99 | 100 | var frames = Directory.EnumerateFiles($"{path}/temp").ToArray(); 101 | Utils.NumericalSort(frames); 102 | 103 | File.WriteAllBytes($"{path}/temp/audio.wav", Flipnote.Audio.GetWavBGM(Flipnote)); 104 | 105 | var a = FFMpegArguments 106 | .FromDemuxConcatInput(frames, options => options 107 | .WithFramerate(Flipnote.Framerate)) 108 | .AddFileInput($"{path}/temp/audio.wav", false) 109 | .OutputToFile($"{path}/{Flipnote.CurrentFilename}.mp4", true, o => 110 | { 111 | o.Resize(256 * scale, 192 * scale) 112 | .WithVideoCodec(VideoCodec.LibX264) 113 | .ForcePixelFormat("yuv420p") 114 | .ForceFormat("mp4"); 115 | }); 116 | 117 | a.ProcessSynchronously(); 118 | 119 | var mp4 = File.ReadAllBytes($"{path}/{Flipnote.CurrentFilename}.mp4"); 120 | 121 | Cleanup(path); 122 | 123 | return mp4; 124 | 125 | 126 | } 127 | catch (Exception e) 128 | { 129 | Cleanup(path); 130 | Console.WriteLine(e.Message); 131 | Console.WriteLine(e.StackTrace); 132 | return null; 133 | } 134 | } 135 | 136 | 137 | private void Cleanup(string path) 138 | { 139 | if (!Directory.Exists($"{path}/temp")) 140 | { 141 | return; 142 | } 143 | var files = Directory.EnumerateFiles($"{path}/temp"); 144 | 145 | files.ToList().ForEach(file => 146 | { 147 | try 148 | { 149 | File.Delete(file); 150 | } 151 | catch (Exception e) 152 | { 153 | // idk yet 154 | } 155 | 156 | }); 157 | Directory.Delete($"{path}/temp"); 158 | } 159 | } 160 | } -------------------------------------------------------------------------------- /LinuxNote.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | FlipnoteEncoder 4 | Exe 5 | net8.0 6 | false 7 | true 8 | embedded 9 | true 10 | true 11 | true 12 | 13 | 14 | 15 | /home/sarah/RiderProjects/PPMLib/bin/Debug/net8.0/PPMLib.dll 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /LinuxNote.csproj.user: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | <_LastSelectedProfileId>C:\Users\finti\source\repos\Encoder\LinuxNote\Properties\PublishProfiles\FolderProfile.pubxml 5 | 6 | -------------------------------------------------------------------------------- /LinuxNote.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.31112.23 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LinuxNote", "LinuxNote.csproj", "{6E14F056-8AE7-436A-A7FF-0A4A4F9D23FD}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Release|Any CPU = Release|Any CPU 12 | EndGlobalSection 13 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 14 | {6E14F056-8AE7-436A-A7FF-0A4A4F9D23FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {6E14F056-8AE7-436A-A7FF-0A4A4F9D23FD}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {6E14F056-8AE7-436A-A7FF-0A4A4F9D23FD}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {6E14F056-8AE7-436A-A7FF-0A4A4F9D23FD}.Release|Any CPU.Build.0 = Release|Any CPU 18 | EndGlobalSection 19 | GlobalSection(SolutionProperties) = preSolution 20 | HideSolutionNode = FALSE 21 | EndGlobalSection 22 | GlobalSection(ExtensibilityGlobals) = postSolution 23 | SolutionGuid = {FF7267F1-66E4-43EC-83F3-AC9700953ACC} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 |  2 | using FFMpegCore; 3 | using Newtonsoft.Json; 4 | using Octokit; 5 | using PPMLib; 6 | using LinuxNote.Encoders; 7 | using LinuxNote.Encoder; 8 | using LinuxNote.Utilities; 9 | using System; 10 | using System.Collections.Generic; 11 | using System.Diagnostics; 12 | using System.IO; 13 | using System.Linq; 14 | using System.Runtime.InteropServices; 15 | using System.Threading.Tasks; 16 | 17 | namespace LinuxNote 18 | { 19 | internal class Program 20 | { 21 | //Github Update Checks 22 | private static bool Update = false; 23 | private static async Task UpdateCheck() 24 | { 25 | //Get all releases from GitHub 26 | //Source: https://octokitnet.readthedocs.io/en/latest/getting-started/ 27 | GitHubClient client = new GitHubClient(new ProductHeaderValue("Update-Check")); 28 | IReadOnlyList releases = await client.Repository.Release.GetAll("RinLovesYou", "Flipnote-Encoder"); 29 | 30 | //Setup the versions 31 | Version latestGitHubVersion = new Version(releases[0].TagName); 32 | Version localVersion = new Version("5.0.5"); 33 | // weed release 34 | 35 | //Compare the Versions 36 | //Source: https://stackoverflow.com/questions/7568147/compare-version-numbers-without-using-split-function 37 | int versionComparison = localVersion.CompareTo(latestGitHubVersion); 38 | if (versionComparison < 0) 39 | { 40 | Update = true; 41 | } 42 | else if (versionComparison > 0) 43 | { 44 | //not gonna happen 45 | } 46 | else 47 | { 48 | Update = false; 49 | } 50 | } 51 | 52 | public static void OpenBrowser(string url) 53 | { 54 | try 55 | { 56 | Process.Start(url); 57 | } 58 | catch 59 | { 60 | // hack because of this: https://github.com/dotnet/corefx/issues/10361 61 | if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) 62 | { 63 | url = url.Replace("&", "^&"); 64 | Process.Start(new ProcessStartInfo("cmd", $"/c start {url}") { CreateNoWindow = true }); 65 | } 66 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) 67 | { 68 | Process.Start("xdg-open", url); 69 | } 70 | else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) 71 | { 72 | Process.Start("open", url); 73 | } 74 | else 75 | { 76 | throw; 77 | } 78 | } 79 | } 80 | 81 | private static bool Running { get; set; } 82 | public static EncodeConfig FlipnoteConfig { get; set; } 83 | 84 | private static void Main(string[] args) 85 | { 86 | try 87 | { 88 | var task = UpdateCheck(); 89 | task.Wait(); 90 | } 91 | catch (Exception e) 92 | { 93 | 94 | } 95 | 96 | if (Update) 97 | { 98 | Console.WriteLine("A Newer Version is available! would you like to update? y/n"); 99 | var answer = Console.ReadKey(true); 100 | if (answer.Key == ConsoleKey.Y) 101 | { 102 | OpenBrowser("http://www.github.com/RinLovesYou/Flipnote-Encoder/releases/latest"); 103 | return; 104 | } 105 | } 106 | 107 | 108 | Running = true; 109 | 110 | GlobalFFOptions.Configure(new FFOptions { BinaryFolder = "ffmpeg/bin" }); 111 | 112 | while (Running) 113 | { 114 | Console.ForegroundColor = ConsoleColor.Green; 115 | Console.WriteLine("Press Any Key to Continue..."); 116 | Console.ReadKey(); 117 | Console.Clear(); 118 | 119 | Console.WriteLine("What would you like to do?"); 120 | Console.WriteLine("1: Encode a video to Flipnote"); 121 | Console.WriteLine("2: Decode a Flipnote"); 122 | var Selection = Console.ReadKey(true); 123 | 124 | switch (Selection.Key) 125 | { 126 | case ConsoleKey.D1: EncodeFlipnote(); break; 127 | case ConsoleKey.D2: DecodeFlipnote(); break; 128 | default: break; 129 | } 130 | } 131 | } 132 | 133 | public static void EncodeFlipnote() 134 | { 135 | CreateEncodeConfig(); 136 | 137 | var encoder = new FlipnoteEncoder(); 138 | 139 | var encoded = encoder.Encode(); 140 | if (encoded != null) 141 | { 142 | Directory.CreateDirectory("tmp"); 143 | encoded.Save($"tmp/{encoded.CurrentFilename}.ppm"); 144 | 145 | if (FlipnoteConfig.Split) 146 | { 147 | double bytelength = new FileInfo($"tmp/{encoded.CurrentFilename}.ppm").Length; 148 | bytelength = bytelength / 1024; 149 | Console.WriteLine(bytelength); 150 | var MB = bytelength / 1024; 151 | if (MB >= 1) 152 | { 153 | List files = new List(); 154 | Console.WriteLine(MB); 155 | var framesframes = encoded.Frames.ToArray().Split((int)(encoded.Frames.Length / MB + 1)); 156 | var audioaudio = encoded.Audio.SoundData.RawBGM.ToArray().Split((int)(encoded.Audio.SoundData.RawBGM.Length / MB + 1)); 157 | if (MB > 1.3) 158 | { 159 | framesframes = encoded.Frames.ToArray().Split((int)(encoded.Frames.Length / MB + 2)); 160 | audioaudio = encoded.Audio.SoundData.RawBGM.ToArray().Split((int)(encoded.Audio.SoundData.RawBGM.Length / MB + 2)); 161 | } 162 | 163 | for (int i = 0; i < framesframes.Count(); i++) 164 | { 165 | var aaa = PPMFile.Create(encoded.CurrentAuthor, framesframes.ToList()[i].ToList(), audioaudio.ToList()[i].ToArray()); 166 | aaa.Save($"out/{aaa.CurrentFilename}_{i}.ppm"); 167 | } 168 | } 169 | else 170 | { 171 | encoded.Save($"out/{encoded.CurrentFilename}.ppm"); 172 | } 173 | } 174 | else 175 | { 176 | encoded.Save($"out/{encoded.CurrentFilename}.ppm"); 177 | } 178 | var mp4try = new PPMFile(); 179 | mp4try.LoadFrom($"tmp/{encoded.CurrentFilename}.ppm"); 180 | Mp4Encoder mp4 = new Mp4Encoder(mp4try); 181 | var a = mp4.EncodeMp4(Path.Combine(Directory.GetCurrentDirectory(), "out")); 182 | } 183 | else 184 | { 185 | Console.ForegroundColor = ConsoleColor.Red; 186 | Console.WriteLine("There was a problem creating the flipnote."); 187 | Console.WriteLine("Please join the support server for further assistance with this issue"); 188 | Console.ForegroundColor = ConsoleColor.White; 189 | Console.WriteLine("Press any Key to continue..."); 190 | Console.ReadKey(); 191 | } 192 | Cleanup(); 193 | } 194 | 195 | private static void Cleanup() 196 | { 197 | if (Directory.Exists("tmp")) 198 | { 199 | try 200 | { 201 | string[] files = Directory.EnumerateFiles("tmp").ToArray(); 202 | files.ToList().ForEach(f => 203 | { 204 | File.Delete(f); 205 | }); 206 | Directory.Delete("tmp"); 207 | } 208 | catch (Exception e) 209 | { 210 | 211 | } 212 | } 213 | 214 | try 215 | { 216 | string[] files = Directory.EnumerateFiles(FlipnoteConfig.InputFolder, "*.png").ToArray(); 217 | files.ToList().ForEach(f => 218 | { 219 | File.Delete(f); 220 | if (File.Exists($"{FlipnoteConfig.InputFolder}/{FlipnoteConfig.InputFilename}.wav")) 221 | { 222 | File.Delete($"{FlipnoteConfig.InputFolder}/{FlipnoteConfig.InputFilename}.wav"); 223 | } 224 | }); 225 | } 226 | catch (Exception e) 227 | { 228 | 229 | } 230 | } 231 | 232 | private static void DecodeFlipnote() 233 | { 234 | Console.WriteLine("Please enter the path to the flipnote you would like to decode"); 235 | var path = Console.ReadLine(); 236 | 237 | var mp4try = new PPMFile(); 238 | mp4try.LoadFrom(path); 239 | Mp4Encoder mp4 = new Mp4Encoder(mp4try); 240 | var a = mp4.EncodeMp4(Path.Combine(Directory.GetCurrentDirectory(), "out")); 241 | } 242 | 243 | private static void CreateEncodeConfig() 244 | { 245 | if (!File.Exists("config.json")) 246 | { 247 | var newEncodeConfig = new EncodeConfig(); 248 | newEncodeConfig.Accurate = true; 249 | newEncodeConfig.DitheringMode = 1; 250 | newEncodeConfig.ColorMode = 1; 251 | newEncodeConfig.Contrast = 0; 252 | newEncodeConfig.InputFilename = "input.mp4"; 253 | newEncodeConfig.InputFolder = "frames"; 254 | newEncodeConfig.Split = false; 255 | newEncodeConfig.DeleteOnFinish = true; 256 | newEncodeConfig.SplitAmount = 2; 257 | FlipnoteConfig = newEncodeConfig; 258 | 259 | JsonSerializer serializer = new JsonSerializer(); 260 | serializer.Formatting = Formatting.Indented; 261 | serializer.NullValueHandling = NullValueHandling.Ignore; 262 | 263 | using (StreamWriter sw = new StreamWriter("config.json")) 264 | using (JsonWriter writer = new JsonTextWriter(sw)) 265 | { 266 | serializer.Serialize(writer, newEncodeConfig); 267 | } 268 | } 269 | else 270 | { 271 | JsonSerializer serializer = new JsonSerializer(); 272 | serializer.Formatting = Formatting.Indented; 273 | serializer.NullValueHandling = NullValueHandling.Ignore; 274 | 275 | using (StreamReader sw = new StreamReader("config.json")) 276 | using (JsonReader writer = new JsonTextReader(sw)) 277 | { 278 | var read = serializer.Deserialize(writer); 279 | FlipnoteConfig = read; 280 | } 281 | } 282 | } 283 | } 284 | } 285 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/FolderProfile.pubxml: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | Release 8 | Any CPU 9 | bin\Release\net5.0\win-x64\publish\ 10 | FileSystem 11 | net5.0 12 | win-x64 13 | true 14 | True 15 | False 16 | False 17 | 18 | -------------------------------------------------------------------------------- /Properties/PublishProfiles/FolderProfile.pubxml.user: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | True|2021-05-21T10:02:27.8301878Z;False|2021-05-21T12:02:03.9491004+02:00;False|2021-05-21T12:01:49.6729563+02:00;True|2021-05-21T11:13:39.3240598+02:00;True|2021-05-21T11:06:33.1547242+02:00; 8 | 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LinuxNote 2 | 3 | LinuxNote aimst to be the cross-platform solution for Flipnote Encoding 4 | 5 | It is entirely based on the original [Flipnote Encoder](https://github.com/RinLovesYou/Flipnote-Encoder) 6 | 7 | It will henceforth be the actual source code destination for it. Releases will however still be published to the original Repo. 8 | -------------------------------------------------------------------------------- /Utilities/ArrayExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | 4 | namespace LinuxNote.Utilities 5 | { 6 | internal static class ArrayExtensions 7 | { 8 | 9 | 10 | /// 11 | /// Splits an array into several smaller arrays. 12 | /// 13 | /// The type of the array. 14 | /// The array to split. 15 | /// The size of the smaller arrays. 16 | /// An array containing smaller arrays. 17 | public static IEnumerable> Split(this T[] array, int size) 18 | { 19 | for (var i = 0; i < (float)array.Length / size; i++) 20 | { 21 | yield return array.Skip(i * size).Take(size); 22 | } 23 | } 24 | 25 | 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /Utilities/EncodeConfig.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace LinuxNote.Utilities 4 | { 5 | [JsonObject] 6 | public class EncodeConfig 7 | { 8 | 9 | public int DitheringMode { get; set; } 10 | public int ColorMode { get; set; } 11 | 12 | public bool Accurate { get; set; } 13 | public int Contrast { get; set; } 14 | 15 | public string InputFolder { get; set; } 16 | public string InputFilename { get; set; } 17 | 18 | public bool Split { get; set; } 19 | public int SplitAmount { get; set; } 20 | public bool DeleteOnFinish { get; set; } 21 | 22 | public EncodeConfig() { } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /Utilities/MathUtils.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text.RegularExpressions; 3 | 4 | namespace LinuxNote.Utilities 5 | { 6 | public class MathUtils 7 | { 8 | 9 | public static void NumericalSort(string[] ar) 10 | { 11 | Regex rgx = new Regex("([^0-9]*)([0-9]+)"); 12 | Array.Sort(ar, (a, b) => 13 | { 14 | var ma = rgx.Matches(a); 15 | var mb = rgx.Matches(b); 16 | for (int i = 0; i < ma.Count; ++i) 17 | { 18 | int ret = ma[i].Groups[1].Value.CompareTo(mb[i].Groups[1].Value); 19 | if (ret != 0) 20 | return ret; 21 | 22 | ret = int.Parse(ma[i].Groups[2].Value) - int.Parse(mb[i].Groups[2].Value); 23 | if (ret != 0) 24 | return ret; 25 | } 26 | 27 | return 0; 28 | }); 29 | } 30 | 31 | 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /runtimeconfig.template.json: -------------------------------------------------------------------------------- 1 | { 2 | "configProperties": { 3 | "System.Globalization.Invariant": true 4 | } 5 | } --------------------------------------------------------------------------------