├── .gitignore ├── App.config ├── DecryptionKeyListParser.cs ├── EPUB2 ├── BookGenerator.cs ├── Constants.cs ├── IEpubDocument.cs ├── NcxDocument.cs ├── OcfDocument.cs └── OpfDocument.cs ├── Helper.cs ├── HtmlGenerator.cs ├── MainForm.Designer.cs ├── MainForm.cs ├── MainForm.resx ├── Program.cs ├── Properties ├── AssemblyInfo.cs ├── Resources.Designer.cs ├── Resources.resx ├── Settings.Designer.cs └── Settings.settings ├── README.md ├── Scribd ├── Book.cs ├── BookConverter.cs ├── ChapterContentDeserializer.cs ├── Decryptor.cs ├── MetadataDeserializer.cs ├── StylesDeserializer.cs ├── TableOfContentsDeserializer.cs └── TagsDeserializer.cs ├── ScribdMpubToEpubConverter.csproj ├── ScribdMpubToEpubConverter.sln ├── ZipStorer.cs ├── image.png ├── packages.config └── scribd.ico /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # Newtonsoft.Json documentation 7 | Newtonsoft.Json.xml 8 | 9 | # User-specific files 10 | *.rsuser 11 | *.suo 12 | *.user 13 | *.userosscache 14 | *.sln.docstates 15 | 16 | # User-specific files (MonoDevelop/Xamarin Studio) 17 | *.userprefs 18 | 19 | # Mono auto generated files 20 | mono_crash.* 21 | 22 | # Build results 23 | [Dd]ebug/ 24 | [Dd]ebugPublic/ 25 | #[Rr]elease/ 26 | [Rr]eleases/ 27 | x64/ 28 | x86/ 29 | [Aa][Rr][Mm]/ 30 | [Aa][Rr][Mm]64/ 31 | bld/ 32 | #[Bb]in/ 33 | [Oo]bj/ 34 | [Ll]og/ 35 | [Ll]ogs/ 36 | 37 | # Visual Studio 2015/2017 cache/options directory 38 | .vs/ 39 | # Uncomment if you have tasks that create the project's static files in wwwroot 40 | #wwwroot/ 41 | 42 | # Visual Studio 2017 auto generated files 43 | Generated\ Files/ 44 | 45 | # MSTest test Results 46 | [Tt]est[Rr]esult*/ 47 | [Bb]uild[Ll]og.* 48 | 49 | # NUnit 50 | *.VisualState.xml 51 | TestResult.xml 52 | nunit-*.xml 53 | 54 | # Build Results of an ATL Project 55 | [Dd]ebugPS/ 56 | [Rr]eleasePS/ 57 | dlldata.c 58 | 59 | # Benchmark Results 60 | BenchmarkDotNet.Artifacts/ 61 | 62 | # .NET Core 63 | project.lock.json 64 | project.fragment.lock.json 65 | artifacts/ 66 | 67 | # StyleCop 68 | StyleCopReport.xml 69 | 70 | # Files built by Visual Studio 71 | *_i.c 72 | *_p.c 73 | *_h.h 74 | *.ilk 75 | *.meta 76 | *.obj 77 | *.iobj 78 | *.pch 79 | *.pdb 80 | *.ipdb 81 | *.pgc 82 | *.pgd 83 | *.rsp 84 | *.sbr 85 | *.tlb 86 | *.tli 87 | *.tlh 88 | *.tmp 89 | *.tmp_proj 90 | *_wpftmp.csproj 91 | *.log 92 | *.vspscc 93 | *.vssscc 94 | .builds 95 | *.pidb 96 | *.svclog 97 | *.scc 98 | 99 | # Chutzpah Test files 100 | _Chutzpah* 101 | 102 | # Visual C++ cache files 103 | ipch/ 104 | *.aps 105 | *.ncb 106 | *.opendb 107 | *.opensdf 108 | *.sdf 109 | *.cachefile 110 | *.VC.db 111 | *.VC.VC.opendb 112 | 113 | # Visual Studio profiler 114 | *.psess 115 | *.vsp 116 | *.vspx 117 | *.sap 118 | 119 | # Visual Studio Trace Files 120 | *.e2e 121 | 122 | # TFS 2012 Local Workspace 123 | $tf/ 124 | 125 | # Guidance Automation Toolkit 126 | *.gpState 127 | 128 | # ReSharper is a .NET coding add-in 129 | _ReSharper*/ 130 | *.[Rr]e[Ss]harper 131 | *.DotSettings.user 132 | 133 | # TeamCity is a build add-in 134 | _TeamCity* 135 | 136 | # DotCover is a Code Coverage Tool 137 | *.dotCover 138 | 139 | # AxoCover is a Code Coverage Tool 140 | .axoCover/* 141 | !.axoCover/settings.json 142 | 143 | # Visual Studio code coverage results 144 | *.coverage 145 | *.coveragexml 146 | 147 | # NCrunch 148 | _NCrunch_* 149 | .*crunch*.local.xml 150 | nCrunchTemp_* 151 | 152 | # MightyMoose 153 | *.mm.* 154 | AutoTest.Net/ 155 | 156 | # Web workbench (sass) 157 | .sass-cache/ 158 | 159 | # Installshield output folder 160 | [Ee]xpress/ 161 | 162 | # DocProject is a documentation generator add-in 163 | DocProject/buildhelp/ 164 | DocProject/Help/*.HxT 165 | DocProject/Help/*.HxC 166 | DocProject/Help/*.hhc 167 | DocProject/Help/*.hhk 168 | DocProject/Help/*.hhp 169 | DocProject/Help/Html2 170 | DocProject/Help/html 171 | 172 | # Click-Once directory 173 | publish/ 174 | 175 | # Publish Web Output 176 | *.[Pp]ublish.xml 177 | *.azurePubxml 178 | # Note: Comment the next line if you want to checkin your web deploy settings, 179 | # but database connection strings (with potential passwords) will be unencrypted 180 | *.pubxml 181 | *.publishproj 182 | 183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 184 | # checkin your Azure Web App publish settings, but sensitive information contained 185 | # in these scripts will be unencrypted 186 | PublishScripts/ 187 | 188 | # NuGet Packages 189 | *.nupkg 190 | # NuGet Symbol Packages 191 | *.snupkg 192 | # The packages folder can be ignored because of Package Restore 193 | **/[Pp]ackages/* 194 | # except build/, which is used as an MSBuild target. 195 | !**/[Pp]ackages/build/ 196 | # Uncomment if necessary however generally it will be regenerated when needed 197 | #!**/[Pp]ackages/repositories.config 198 | # NuGet v3's project.json files produces more ignorable files 199 | *.nuget.props 200 | *.nuget.targets 201 | 202 | # Microsoft Azure Build Output 203 | csx/ 204 | *.build.csdef 205 | 206 | # Microsoft Azure Emulator 207 | ecf/ 208 | rcf/ 209 | 210 | # Windows Store app package directories and files 211 | AppPackages/ 212 | BundleArtifacts/ 213 | Package.StoreAssociation.xml 214 | _pkginfo.txt 215 | *.appx 216 | *.appxbundle 217 | *.appxupload 218 | 219 | # Visual Studio cache files 220 | # files ending in .cache can be ignored 221 | *.[Cc]ache 222 | # but keep track of directories ending in .cache 223 | !?*.[Cc]ache/ 224 | 225 | # Others 226 | ClientBin/ 227 | ~$* 228 | *~ 229 | *.dbmdl 230 | *.dbproj.schemaview 231 | *.jfm 232 | *.pfx 233 | *.publishsettings 234 | orleans.codegen.cs 235 | 236 | # Including strong name files can present a security risk 237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 238 | #*.snk 239 | 240 | # Since there are multiple workflows, uncomment next line to ignore bower_components 241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 242 | #bower_components/ 243 | 244 | # RIA/Silverlight projects 245 | Generated_Code/ 246 | 247 | # Backup & report files from converting an old project file 248 | # to a newer Visual Studio version. Backup files are not needed, 249 | # because we have git ;-) 250 | _UpgradeReport_Files/ 251 | Backup*/ 252 | UpgradeLog*.XML 253 | UpgradeLog*.htm 254 | ServiceFabricBackup/ 255 | *.rptproj.bak 256 | 257 | # SQL Server files 258 | *.mdf 259 | *.ldf 260 | *.ndf 261 | 262 | # Business Intelligence projects 263 | *.rdl.data 264 | *.bim.layout 265 | *.bim_*.settings 266 | *.rptproj.rsuser 267 | *- [Bb]ackup.rdl 268 | *- [Bb]ackup ([0-9]).rdl 269 | *- [Bb]ackup ([0-9][0-9]).rdl 270 | 271 | # Microsoft Fakes 272 | FakesAssemblies/ 273 | 274 | # GhostDoc plugin setting file 275 | *.GhostDoc.xml 276 | 277 | # Node.js Tools for Visual Studio 278 | .ntvs_analysis.dat 279 | node_modules/ 280 | 281 | # Visual Studio 6 build log 282 | *.plg 283 | 284 | # Visual Studio 6 workspace options file 285 | *.opt 286 | 287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 288 | *.vbw 289 | 290 | # Visual Studio LightSwitch build output 291 | **/*.HTMLClient/GeneratedArtifacts 292 | **/*.DesktopClient/GeneratedArtifacts 293 | **/*.DesktopClient/ModelManifest.xml 294 | **/*.Server/GeneratedArtifacts 295 | **/*.Server/ModelManifest.xml 296 | _Pvt_Extensions 297 | 298 | # Paket dependency manager 299 | .paket/paket.exe 300 | paket-files/ 301 | 302 | # FAKE - F# Make 303 | .fake/ 304 | 305 | # CodeRush personal settings 306 | .cr/personal 307 | 308 | # Python Tools for Visual Studio (PTVS) 309 | __pycache__/ 310 | *.pyc 311 | 312 | # Cake - Uncomment if you are using it 313 | # tools/** 314 | # !tools/packages.config 315 | 316 | # Tabs Studio 317 | *.tss 318 | 319 | # Telerik's JustMock configuration file 320 | *.jmconfig 321 | 322 | # BizTalk build output 323 | *.btp.cs 324 | *.btm.cs 325 | *.odx.cs 326 | *.xsd.cs 327 | 328 | # OpenCover UI analysis results 329 | OpenCover/ 330 | 331 | # Azure Stream Analytics local run output 332 | ASALocalRun/ 333 | 334 | # MSBuild Binary and Structured Log 335 | *.binlog 336 | 337 | # NVidia Nsight GPU debugger configuration file 338 | *.nvuser 339 | 340 | # MFractors (Xamarin productivity tool) working folder 341 | .mfractor/ 342 | 343 | # Local History for Visual Studio 344 | .localhistory/ 345 | 346 | # BeatPulse healthcheck temp database 347 | healthchecksdb 348 | 349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 350 | MigrationBackup/ 351 | 352 | # Ionide (cross platform F# VS Code tools) working folder 353 | .ionide/ 354 | -------------------------------------------------------------------------------- /App.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /DecryptionKeyListParser.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Xml; 4 | 5 | namespace ScribdMpubToEpubConverter 6 | { 7 | public static class DecryptionKeyListParser 8 | { 9 | public static void Parse(string file_path, ref Dictionary DecryptionKeys) 10 | { 11 | using var reader = XmlReader.Create(File.OpenRead(file_path)); 12 | while (reader.Read()) 13 | { 14 | if (reader.NodeType != XmlNodeType.Element) 15 | continue; 16 | 17 | if (reader.Name != "string") 18 | continue; 19 | 20 | // Make sure to filter out the IS_MIGRATED option 21 | var name = reader.GetAttribute("name"); 22 | if (!name.StartsWith("KEY_")) 23 | { 24 | Helper.Information("Skipping XML entry, as it is not a key: " + name); 25 | continue; 26 | } 27 | 28 | // Try to read the value 29 | if (!reader.Read()) 30 | continue; 31 | 32 | if (reader.NodeType != XmlNodeType.Text) 33 | continue; 34 | 35 | // Sanity check, as the key length should be 32 bytes. 36 | var value = reader.Value.Trim(); 37 | if (value.Length != 32) 38 | { 39 | Helper.Warning("Skipping decryption key with incorrect value length: " + name); 40 | continue; 41 | } 42 | 43 | DecryptionKeys.Add(name.Remove(0, "KEY_".Length), value); 44 | } 45 | 46 | Helper.Debug("Decryption key entries: " + DecryptionKeys.Count); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /EPUB2/BookGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.IO.Compression; 4 | using System.Text; 5 | 6 | namespace ScribdMpubToEpubConverter.EPUB2 7 | { 8 | class BookGenerator 9 | { 10 | private Scribd.Book Book { get; set; } 11 | private string CurrentDirectory { get; set; } 12 | private string CurrentBookName { get; set; } 13 | 14 | private readonly NcxDocument Ncx = new NcxDocument(); 15 | private readonly OpfDocument Opf = new OpfDocument(); 16 | private readonly OcfDocument Ocf = new OcfDocument(); 17 | 18 | public BookGenerator(Scribd.Book book) 19 | { 20 | Book = book; 21 | 22 | // Set current book name in the format of: "Title - Book ID" 23 | CurrentBookName = Book.Title + " - " + Book.ID.ToString(); 24 | 25 | // Get Windows filesystem-compliant directory name 26 | var invalid_characters = Path.GetInvalidFileNameChars(); 27 | 28 | // Sanitize directory name 29 | CurrentBookName = string.Join( 30 | "_", 31 | CurrentBookName.Split( 32 | invalid_characters, 33 | System.StringSplitOptions.RemoveEmptyEntries 34 | ) 35 | ).TrimEnd('.'); 36 | 37 | // Delete directory if it already exists 38 | if (Directory.Exists(CurrentBookName)) 39 | DeleteDirectory(CurrentBookName); 40 | 41 | CurrentDirectory = Helper.CreateDirectory(CurrentBookName); 42 | 43 | // Set document titles 44 | Opf.Title = Ncx.Title = Book.Title; 45 | 46 | // Create required directories 47 | foreach (var subdirectory in EpubDirectories.Directories) 48 | Directory.CreateDirectory(CurrentDirectory + subdirectory); 49 | 50 | // Generate stylesheet file 51 | GenerateStylesheet(); 52 | } 53 | 54 | private void GenerateStylesheet() 55 | { 56 | Helper.Debug("Generating stylesheet"); 57 | 58 | string css = string.Empty; 59 | foreach (var style in Book.Styles) 60 | { 61 | if (style.Value.Trim().Length == 0) 62 | continue; 63 | 64 | css += "." + style.Key + "{ " + style.Value + "; }\r\n"; 65 | } 66 | 67 | if (css.Trim().Length == 0) 68 | Helper.Warning("CSS is empty! Please delete and unlink from all HTML files!"); 69 | 70 | File.WriteAllText( 71 | CurrentDirectory + EpubDirectories.OEBPS_Styles + "style.css", 72 | css 73 | ); 74 | } 75 | 76 | private void AddMimetype(ZipStorer zip) 77 | { 78 | Helper.Debug("Adding mimetype"); 79 | 80 | // Make sure there is no UTF-8 BOM 81 | var encoding = new UTF8Encoding(false); 82 | var mimetype = encoding.GetBytes("application/epub+zip"); 83 | 84 | File.WriteAllBytes(CurrentDirectory + "mimetype", mimetype); 85 | 86 | // Add mimetype without compression 87 | zip.AddFile( 88 | ZipStorer.Compression.Store, 89 | CurrentDirectory + "mimetype", 90 | "mimetype" 91 | ); 92 | } 93 | 94 | private void AddCoverPage() 95 | { 96 | if (Book.CoverPage.HTML == null) 97 | return; 98 | 99 | Helper.Debug("Adding cover page"); 100 | 101 | var html_page_name = "coverpage"; 102 | 103 | // NOTE: Relative to the EPUB root 104 | var epub_relative_path = EpubDirectories.OEBPS_Text + html_page_name + ".xhtml"; 105 | var absolute_path = CurrentDirectory + epub_relative_path; 106 | 107 | // Write HTML to disk 108 | File.WriteAllText(absolute_path, Book.CoverPage.HTML); 109 | 110 | // NOTE: Relative to the OEBPS/ folder 111 | var oebps_relative_path = epub_relative_path.Replace(EpubDirectories.OEBPS, ""); 112 | Ncx.AddNavigationItem(html_page_name, Book.CoverPage.Title, oebps_relative_path); 113 | Opf.AddManifestItem(html_page_name, oebps_relative_path); 114 | 115 | // NOTE: Path relative to the OEBPS/ folder 116 | var toc_relative_path = Ncx.Type.GetRelativePath().Replace(EpubDirectories.OEBPS, ""); 117 | Opf.AddManifestItem("ncx", toc_relative_path); 118 | } 119 | 120 | private void AddChapters() 121 | { 122 | Helper.Debug("Adding chapters and linking NCX"); 123 | 124 | foreach (var chapter in Book.Chapters) 125 | { 126 | var html_page_id = Path.GetFileNameWithoutExtension(chapter.OutputFileName); 127 | 128 | // NOTE: Relative to the EPUB root 129 | var epub_relative_path = EpubDirectories.OEBPS_Text + chapter.OutputFileName; 130 | var absolute_path = CurrentDirectory + epub_relative_path; 131 | 132 | // Write HTML to disk 133 | File.WriteAllText(absolute_path, chapter.HTML); 134 | 135 | // NOTE: Relative to the OEBPS/ folder 136 | var oebps_relative_path = epub_relative_path.Replace(EpubDirectories.OEBPS, ""); 137 | Ncx.AddNavigationItem(html_page_id, chapter.Title, oebps_relative_path); 138 | Opf.AddManifestItem(html_page_id, oebps_relative_path); 139 | } 140 | 141 | // NOTE: Path relative to the OEBPS/ folder 142 | var toc_relative_path = Ncx.Type.GetRelativePath().Replace(EpubDirectories.OEBPS, ""); 143 | Opf.AddManifestItem("ncx", toc_relative_path); 144 | } 145 | 146 | private void AddImages() 147 | { 148 | Helper.Debug("Adding images"); 149 | 150 | foreach (var image in Book.Images) 151 | { 152 | var epub_relative_path = EpubDirectories.OEBPS_Images + image.Key; 153 | File.Copy(image.Value.AbsoluteFilePath, CurrentDirectory + epub_relative_path); 154 | 155 | // NOTE: Path relative to the OEBPS/ folder 156 | var image_relative_path = epub_relative_path.Replace(EpubDirectories.OEBPS, ""); 157 | Opf.AddManifestItem(image.Value.ID, image_relative_path); 158 | } 159 | } 160 | 161 | private void AddStylesheet() 162 | { 163 | Helper.Debug("Adding stylesheet"); 164 | 165 | var epub_relative_path = EpubDirectories.OEBPS_Styles + "style.css"; 166 | 167 | // NOTE: Path relative to the OEBPS/ folder 168 | var css_relative_path = epub_relative_path.Replace(EpubDirectories.OEBPS, ""); 169 | Opf.AddManifestItem("css", css_relative_path); 170 | } 171 | 172 | // NOTE: When handling with the ZipStorer class, please use 173 | // \ as a directory separator instead of / 174 | // Otherwise, you are free to use, but the rule of thumb is: 175 | // - / as a separator for EPUB stuff 176 | // - \ as a separator for Windows filesystem stuff 177 | public void Generate() 178 | { 179 | var zip = ZipStorer.Create( 180 | Path.GetDirectoryName(CurrentDirectory) + ".epub" 181 | ); 182 | 183 | AddMimetype(zip); 184 | AddCoverPage(); 185 | AddChapters(); 186 | AddImages(); 187 | AddStylesheet(); 188 | 189 | var Documents = new Dictionary{ 190 | { DocumentType.NCX, Ncx }, 191 | { DocumentType.OPF, Opf }, 192 | { DocumentType.OCF, Ocf } 193 | }; 194 | 195 | foreach (var document in Documents) 196 | { 197 | var absolute_path = CurrentDirectory + document.Key.GetRelativePath(); 198 | 199 | using var stream = File.OpenWrite(absolute_path); 200 | document.Value.Write(stream); 201 | 202 | Helper.Debug("Generating " + document.Key.GetRelativePath()); 203 | } 204 | 205 | // Add META-INF/ folder 206 | var meta_inf = EpubDirectories.METAINF.Replace('/', '\\'); 207 | zip.AddDirectory( 208 | ZipStorer.Compression.Deflate, 209 | CurrentDirectory + meta_inf, 210 | meta_inf 211 | ); 212 | 213 | // Add OEBPS/ folder 214 | var oebps = EpubDirectories.OEBPS.Replace('/', '\\'); 215 | zip.AddDirectory( 216 | ZipStorer.Compression.Deflate, 217 | CurrentDirectory + oebps, 218 | oebps 219 | ); 220 | 221 | zip.Close(); 222 | 223 | // Delete temporary files 224 | DeleteDirectory(CurrentDirectory); 225 | 226 | Helper.Debug("Removed temporary files from the filesystem"); 227 | Helper.Information("Finished generating an EPUB2.0.1 file for \"" + CurrentBookName + "\""); 228 | Helper.Debug("---------------------------------"); 229 | } 230 | 231 | public static void DeleteDirectory(string path) 232 | { 233 | foreach (string directory in Directory.GetDirectories(path)) 234 | DeleteDirectory(directory); 235 | 236 | try 237 | { 238 | Directory.Delete(path, true); 239 | } 240 | catch (IOException) 241 | { 242 | Directory.Delete(path, true); 243 | } 244 | catch (System.UnauthorizedAccessException) 245 | { 246 | Directory.Delete(path, true); 247 | } 248 | } 249 | } 250 | } -------------------------------------------------------------------------------- /EPUB2/Constants.cs: -------------------------------------------------------------------------------- 1 | namespace ScribdMpubToEpubConverter.EPUB2 2 | { 3 | static class Constants 4 | { 5 | public static readonly string XmlNcxNamespaceURL = "http://www.daisy.org/z3986/2005/ncx/"; 6 | public static readonly string XmlOpfNamespaceURL = "http://www.idpf.org/2007/opf"; 7 | public static readonly string XmlDublinCoreURL = "http://purl.org/dc/elements/1.1/"; 8 | public static readonly string XmlDublinCoreTermsURL = "http://purl.org/dc/terms"; 9 | 10 | public static readonly string XmlContainerNamespaceURL = "urn:oasis:names:tc:opendocument:xmlns:container"; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /EPUB2/IEpubDocument.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | 4 | namespace ScribdMpubToEpubConverter.EPUB2 5 | { 6 | // NOTE: These are relative paths 7 | public static class EpubDirectories 8 | { 9 | public static readonly string METAINF = "META-INF/"; 10 | public static readonly string OEBPS = "OEBPS/"; 11 | public static readonly string OEBPS_Text = OEBPS + "Text/"; 12 | public static readonly string OEBPS_Styles = OEBPS + "Styles/"; 13 | public static readonly string OEBPS_Images = OEBPS + "Images/"; 14 | 15 | public static readonly string[] Directories = { 16 | METAINF, 17 | OEBPS, 18 | OEBPS_Text, 19 | OEBPS_Images, 20 | OEBPS_Styles 21 | }; 22 | } 23 | 24 | public enum DocumentType 25 | { 26 | OCF, 27 | OPF, 28 | NCX 29 | }; 30 | 31 | public struct DocumentInfo 32 | { 33 | public string Name { get; set; } 34 | public string FolderName { get; set; } 35 | } 36 | 37 | public static class DocumentTypeExtensions 38 | { 39 | public static readonly Dictionary Documents = new Dictionary 40 | { 41 | { 42 | DocumentType.OCF, 43 | new DocumentInfo{ Name = "container.xml", FolderName = EpubDirectories.METAINF } 44 | }, 45 | { 46 | DocumentType.OPF, 47 | new DocumentInfo{ Name = "content.opf", FolderName = EpubDirectories.OEBPS } 48 | }, 49 | { 50 | DocumentType.NCX, 51 | new DocumentInfo { Name = "toc.ncx", FolderName = EpubDirectories.OEBPS } 52 | } 53 | }; 54 | 55 | public static string GetRelativePath(this DocumentType type) 56 | { 57 | return Documents[type].FolderName + Documents[type].Name; 58 | } 59 | } 60 | 61 | public abstract class IEpubDocument 62 | { 63 | public abstract DocumentType Type { get; } 64 | 65 | public abstract void Write(Stream stream); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /EPUB2/NcxDocument.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Xml; 4 | 5 | namespace ScribdMpubToEpubConverter.EPUB2 6 | { 7 | struct NavigationItem 8 | { 9 | public string ID { get; set; } 10 | public string Label { get; set; } 11 | 12 | // NOTE: This is a relative file path 13 | public string FilePath { get; set; } 14 | } 15 | 16 | class NcxDocument : IEpubDocument 17 | { 18 | public string Title { get; set; } 19 | public List NavigationItems = new List(); 20 | 21 | public override DocumentType Type => DocumentType.NCX; 22 | 23 | private void WriteHead(XmlWriter writer) 24 | { 25 | void write_meta(string name, string content) 26 | { 27 | /** 28 | * 29 | */ 30 | writer.WriteStartElement("meta"); 31 | writer.WriteAttributeString("name", name); 32 | writer.WriteAttributeString("content", content); 33 | writer.WriteEndElement(); 34 | } 35 | 36 | /** 37 | * 38 | */ 39 | writer.WriteStartElement("head"); 40 | 41 | // 42 | //foreach (var metadata in MetadataItems) 43 | // write_meta(metadata.Name, metadata.Content); 44 | 45 | write_meta("dtb:uid", "0000000000000"); 46 | write_meta("dtb:depth", "1"); 47 | write_meta("dtb:totalPageCount", "0"); 48 | write_meta("dtb:maxPageNumber", "0"); 49 | 50 | /* 51 | * 52 | */ 53 | writer.WriteEndElement(); 54 | } 55 | 56 | private void WriteDocumentText(XmlWriter writer, string element_name, string content) 57 | { 58 | /* 59 | * <@element_name> 60 | * @content 61 | * 62 | */ 63 | writer.WriteStartElement(element_name); 64 | writer.WriteElementString("text", content); 65 | writer.WriteEndElement(); 66 | } 67 | 68 | private void WriteNavigationMap(XmlWriter writer) 69 | { 70 | void write_nav_item(NavigationItem item, int id) 71 | { 72 | /* 73 | * 74 | */ 75 | writer.WriteStartElement("navPoint"); 76 | writer.WriteAttributeString("id", "navpoint-" + item.ID); 77 | writer.WriteAttributeString("playOrder", id.ToString()); 78 | 79 | /* 80 | * 81 | * @label 82 | * 83 | */ 84 | writer.WriteStartElement("navLabel"); 85 | writer.WriteElementString("text", item.Label); 86 | writer.WriteEndElement(); 87 | 88 | /* 89 | * 90 | */ 91 | writer.WriteStartElement("content"); 92 | writer.WriteAttributeString("src", item.FilePath); 93 | writer.WriteEndElement(); 94 | 95 | /* 96 | * 97 | */ 98 | writer.WriteEndElement(); 99 | } 100 | 101 | /* 102 | * 103 | */ 104 | writer.WriteStartElement("navMap"); 105 | 106 | for (int i = 0; i < NavigationItems.Count; ++i) 107 | write_nav_item(NavigationItems[i], i); 108 | 109 | /* 110 | * 111 | */ 112 | writer.WriteEndElement(); 113 | } 114 | 115 | public void AddNavigationItem(string id, string label, string file_path) 116 | { 117 | NavigationItems.Add(new NavigationItem 118 | { 119 | ID = id, 120 | Label = label, 121 | FilePath = file_path 122 | }); 123 | } 124 | 125 | // NOTE: NCX DOCTYPE intentionally omitted. 126 | // It is not necessary as per the EPUB2.0.1 specification. 127 | public override void Write(Stream stream) 128 | { 129 | using var writer = XmlWriter.Create(stream, Helper.XmlWriterSettings); 130 | 131 | /* 132 | * 133 | */ 134 | writer.WriteStartDocument(); 135 | 136 | /* 137 | * 141 | */ 142 | writer.WriteDocType("ncx", "-//NISO//DTD ncx 2005-1//EN", "http://www.daisy.org/z3986/2005/ncx-2005-1.dtd", null); 143 | 144 | /* 145 | * 146 | */ 147 | writer.WriteStartElement("ncx", Constants.XmlNcxNamespaceURL); 148 | writer.WriteAttributeString("version", "2005-1"); 149 | 150 | WriteHead(writer); 151 | WriteDocumentText(writer, "docTitle", Title); 152 | WriteNavigationMap(writer); 153 | 154 | /* 155 | * 156 | */ 157 | writer.WriteEndElement(); 158 | writer.WriteEndDocument(); 159 | } 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /EPUB2/OcfDocument.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | using System.Xml; 3 | 4 | namespace ScribdMpubToEpubConverter.EPUB2 5 | { 6 | class OcfDocument : IEpubDocument 7 | { 8 | public override DocumentType Type => DocumentType.OCF; 9 | 10 | public override void Write(Stream stream) 11 | { 12 | using var writer = XmlWriter.Create(stream, Helper.XmlWriterSettings); 13 | 14 | /* 15 | * 16 | */ 17 | writer.WriteStartDocument(true); 18 | 19 | /* 20 | * 21 | */ 22 | writer.WriteStartElement("container", Constants.XmlContainerNamespaceURL); 23 | writer.WriteAttributeString("version", "1.0"); 24 | 25 | /* 26 | * 27 | */ 28 | writer.WriteStartElement("rootfiles"); 29 | 30 | /* 31 | * 32 | */ 33 | var opf_document_path = DocumentTypeExtensions.GetRelativePath(DocumentType.OPF); 34 | writer.WriteStartElement("rootfile"); 35 | writer.WriteAttributeString("full-path", opf_document_path); 36 | writer.WriteAttributeString("media-type", "application/oebps-package+xml"); 37 | writer.WriteEndElement(); 38 | 39 | /* 40 | * 41 | */ 42 | writer.WriteEndElement(); 43 | 44 | /* 45 | * 46 | */ 47 | writer.WriteEndElement(); 48 | writer.WriteEndDocument(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /EPUB2/OpfDocument.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Xml; 5 | 6 | namespace ScribdMpubToEpubConverter.EPUB2 7 | { 8 | struct ManifestItem 9 | { 10 | public string ID { get; set; } 11 | // NOTE: Relative to the OEBPS/ folder 12 | public string FilePath { get; set; } 13 | } 14 | 15 | struct GuideItem 16 | { 17 | // NOTE: Relative to the OEBPS/ folder 18 | public string Type { get; set; } 19 | public string Title { get; set; } 20 | public string FilePath { get; set; } 21 | } 22 | 23 | class OpfDocument : IEpubDocument 24 | { 25 | private static readonly Dictionary FileTypes = new Dictionary 26 | { 27 | { "html", "application/xhtml+xml" }, 28 | { "xhtml", "application/xhtml+xml" }, 29 | { "ncx", "application/x-dtbncx+xml" }, 30 | { "css", "text/css" }, 31 | { "jpg", "image/jpeg" }, 32 | { "jpeg", "image/jpeg" }, 33 | { "png", "image/png" }, 34 | { "gif", "image/gif" }, 35 | { "svg", "image/svg+xml" } 36 | }; 37 | 38 | public List GuideItems = new List(); 39 | public List ManifestItems = new List(); 40 | 41 | public string Title { get; set; } 42 | 43 | public override DocumentType Type => DocumentType.OPF; 44 | 45 | private string GetFileMimeType(string path) 46 | { 47 | foreach (var file_type in FileTypes) 48 | { 49 | if (path.EndsWith(file_type.Key)) 50 | return file_type.Value; 51 | } 52 | 53 | // Shouldn't ever happen 54 | throw new Exception("GetFileMimeType() encountered an invalid file type\n" + path); 55 | } 56 | 57 | private void WriteDocumentTitle(XmlWriter writer, string title) 58 | { 59 | /* 60 | * @title 61 | */ 62 | writer.WriteStartElement("dc", "title", null); 63 | writer.WriteAttributeString("id", "title"); 64 | writer.WriteString(title); 65 | writer.WriteEndElement(); 66 | } 67 | 68 | // TODO: Write additional metadata? 69 | private void WriteMetadata(XmlWriter writer) 70 | { 71 | /* 72 | * 76 | */ 77 | writer.WriteStartElement("metadata"); 78 | writer.WriteAttributeString("xmlns", "opf", null, Constants.XmlOpfNamespaceURL); 79 | writer.WriteAttributeString("xmlns", "dc", null, Constants.XmlDublinCoreURL); 80 | writer.WriteAttributeString("xmlns", "dcterms", null, Constants.XmlDublinCoreTermsURL); 81 | 82 | WriteDocumentTitle(writer, Title); 83 | 84 | /* 85 | * en-US 86 | */ 87 | writer.WriteElementString("dc", "language", null, "en-US"); 88 | 89 | /* 90 | * @CurrentTimeInISO8601 91 | */ 92 | writer.WriteStartElement("dc", "date", null); 93 | writer.WriteAttributeString("opf", "event", null, "modification"); 94 | writer.WriteString(DateTime.UtcNow.ToString("s", System.Globalization.CultureInfo.InvariantCulture)); 95 | writer.WriteEndElement(); 96 | 97 | /* 98 | * ScribdMpubToEpubConverter 99 | */ 100 | writer.WriteStartElement("dc", "contributor", null); 101 | writer.WriteAttributeString("opf", "role", null, "bkp"); 102 | writer.WriteString("ScribdMpubToEpubConverter"); 103 | writer.WriteEndElement(); 104 | 105 | /* 106 | * Scribd 107 | */ 108 | writer.WriteElementString("dc", "source", null, "Scribd"); 109 | 110 | /* 111 | * 0000000000000 112 | */ 113 | writer.WriteStartElement("dc", "identifier", null); 114 | writer.WriteAttributeString("id", "uid"); 115 | writer.WriteAttributeString("opf", "scheme", null, "ISBN"); 116 | writer.WriteString("0000000000000"); 117 | writer.WriteEndElement(); 118 | 119 | /* 120 | * 121 | */ 122 | writer.WriteEndElement(); 123 | } 124 | 125 | private void WriteManifest(XmlWriter writer) 126 | { 127 | /* 128 | * 129 | */ 130 | writer.WriteStartElement("manifest"); 131 | 132 | foreach (var item in ManifestItems) 133 | { 134 | /* 135 | * 136 | */ 137 | writer.WriteStartElement("item"); 138 | writer.WriteAttributeString("id", item.ID); 139 | writer.WriteAttributeString("href", item.FilePath); 140 | writer.WriteAttributeString("media-type", GetFileMimeType(item.FilePath)); 141 | writer.WriteEndElement(); 142 | } 143 | 144 | writer.WriteEndElement(); 145 | } 146 | 147 | private void WriteSpine(XmlWriter writer) 148 | { 149 | /* 150 | * 151 | */ 152 | writer.WriteStartElement("spine"); 153 | writer.WriteAttributeString("toc", "ncx"); 154 | 155 | foreach (var item in ManifestItems) 156 | { 157 | /* 158 | * The element can consist only of HTML files. 159 | * 160 | * NOTE: 161 | * Each itemref in spine must not reference media types other than 162 | * OPS Content Documents (or documents whose fallback chain includes an OPS Content Document). 163 | * An OPS Content Document must be of one of the following media types: 164 | * - application/xhtml+xml, 165 | * - application/x-dtbook+xml, 166 | * - the deprecated text/x-oeb1-document, 167 | * - and Out-Of-Line XML Island (with required fallback.) 168 | * When a document with a media type not from this list (or a document whose fallback chain doesn't include a document with a media type from this list) is referenced in spine, Reading Systems must not include it as part of the spine. 169 | */ 170 | var mimetype = GetFileMimeType(item.FilePath); 171 | if (mimetype != "application/xhtml+xml") 172 | continue; 173 | 174 | /* 175 | * 176 | */ 177 | writer.WriteStartElement("itemref"); 178 | writer.WriteAttributeString("idref", item.ID); 179 | writer.WriteEndElement(); 180 | } 181 | 182 | writer.WriteEndElement(); 183 | } 184 | 185 | private void WriteGuide(XmlWriter writer) 186 | { 187 | // Don't do anything if there are no guide items 188 | if (GuideItems.Count == 0) 189 | return; 190 | 191 | /* 192 | * 193 | */ 194 | writer.WriteStartElement("guide"); 195 | 196 | foreach (var item in GuideItems) 197 | { 198 | /* 199 | * 200 | */ 201 | writer.WriteStartElement("reference"); 202 | writer.WriteAttributeString("type", item.Type); 203 | writer.WriteAttributeString("title", item.Title); 204 | writer.WriteAttributeString("href", item.FilePath); 205 | writer.WriteEndElement(); 206 | } 207 | 208 | writer.WriteEndElement(); 209 | } 210 | 211 | public void AddManifestItem(string id, string file_path) 212 | { 213 | ManifestItems.Add(new ManifestItem 214 | { 215 | ID = id, 216 | FilePath = file_path 217 | }); 218 | } 219 | 220 | public override void Write(Stream stream) 221 | { 222 | using var writer = XmlWriter.Create(stream, Helper.XmlWriterSettings); 223 | 224 | /* 225 | * 226 | */ 227 | writer.WriteStartDocument(); 228 | 229 | /* 234 | */ 235 | writer.WriteStartElement("package", Constants.XmlOpfNamespaceURL); 236 | writer.WriteAttributeString("unique-identifier", "uid"); 237 | writer.WriteAttributeString("version", "2.0"); 238 | 239 | WriteMetadata(writer); 240 | WriteManifest(writer); 241 | WriteSpine(writer); 242 | WriteGuide(writer); 243 | 244 | /* 245 | * 246 | */ 247 | writer.WriteEndElement(); 248 | writer.WriteEndDocument(); 249 | } 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /Helper.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | using System.Diagnostics; 3 | using System.IO; 4 | using System.Text; 5 | using System.Windows.Forms; 6 | using System.Xml; 7 | 8 | namespace ScribdMpubToEpubConverter 9 | { 10 | public static class Logger 11 | { 12 | public static readonly string[] Levels = { "Warning", "Information", "Debug" }; 13 | 14 | public static readonly int Warning = 0; 15 | public static readonly int Information = 1; 16 | public static readonly int Debug = 2; 17 | 18 | // Set default level 19 | public static int CurrentLevel = Information; 20 | } 21 | 22 | public static class Helper 23 | { 24 | public static readonly XmlWriterSettings XmlWriterSettings = new XmlWriterSettings 25 | { 26 | Indent = true, 27 | IndentChars = "\t", 28 | CheckCharacters = true, 29 | Encoding = new UTF8Encoding(false) 30 | }; 31 | 32 | public static BindingList DebugLog = new BindingList(); 33 | public static string DebugLogPath { get; set; } 34 | public static int WarningCount { get; set; } 35 | 36 | // Little hack to keep track of warnings between conversions 37 | // and avoid unrequired branching 38 | public static void Warning(string message) => Log(Logger.Warning + (++WarningCount - WarningCount), message); 39 | public static void Information(string message) => Log(Logger.Information, message); 40 | public static void Debug(string message) => Log(Logger.Debug, message); 41 | 42 | private static void Log(int level, string message) 43 | { 44 | // Don't log anything 45 | if (level > Logger.CurrentLevel) 46 | return; 47 | 48 | var stack = new StackTrace(); 49 | 50 | // Get function class and name from 2 stack frames up 51 | var previous_method = stack.GetFrame(2).GetMethod(); 52 | var function_name = previous_method.DeclaringType.Name + "::" + previous_method.Name; 53 | 54 | var log_message = "[" + Logger.Levels[level] + "]" + 55 | "[" + function_name + "] " + message; 56 | 57 | // Don't log debug messages to application 58 | if (level != Logger.Debug) 59 | DebugLog.Add(log_message); 60 | 61 | // Log with timestamp to file 62 | File.AppendAllText( 63 | DebugLogPath, 64 | "[" + System.DateTime.Now.ToString() + "]" + 65 | log_message + "\r\n" 66 | ); 67 | } 68 | 69 | public static void ShowWarning(string message, string title = "Warning") 70 | { 71 | MessageBox.Show( 72 | message, 73 | title, 74 | MessageBoxButtons.OK, 75 | MessageBoxIcon.Warning 76 | ); 77 | } 78 | 79 | public static string CreateDirectory(string directory_name) 80 | { 81 | var output_directory_path = 82 | Directory.GetCurrentDirectory() + 83 | Path.DirectorySeparatorChar + 84 | directory_name; 85 | 86 | Directory.CreateDirectory(output_directory_path); 87 | return output_directory_path + Path.DirectorySeparatorChar; 88 | } 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /HtmlGenerator.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Text; 4 | using System.Xml; 5 | 6 | namespace ScribdMpubToEpubConverter 7 | { 8 | public sealed class UTF8StringWriter : StringWriter 9 | { 10 | public override Encoding Encoding 11 | { 12 | get { return Encoding.UTF8; } 13 | } 14 | } 15 | 16 | struct HtmlTableColumn 17 | { 18 | public string Text { get; set; } 19 | public string Style { get; set; } 20 | } 21 | 22 | struct HtmlAttribute 23 | { 24 | public string LocalName { get; set; } 25 | public string Value { get; set; } 26 | } 27 | 28 | class HtmlGenerator 29 | { 30 | private XmlWriter Writer { get; set; } 31 | 32 | public HtmlGenerator(XmlWriter writer, string title) 33 | { 34 | Writer = writer; 35 | 36 | /* 37 | * 38 | */ 39 | Writer.WriteStartDocument(false); 40 | 41 | /* 42 | * 46 | */ 47 | Writer.WriteDocType("html", "-//W3C//DTD XHTML 1.1//EN", "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd", null); 48 | 49 | /* 50 | * 51 | */ 52 | Writer.WriteStartElement("html", "http://www.w3.org/1999/xhtml"); 53 | 54 | /* 55 | * 56 | */ 57 | Writer.WriteStartElement("head"); 58 | 59 | /* 60 | * 61 | */ 62 | var styles_relative_path = EPUB2.EpubDirectories.OEBPS_Styles.Replace( 63 | EPUB2.EpubDirectories.OEBPS, 64 | "../" 65 | ); 66 | Writer.WriteStartElement("link"); 67 | Writer.WriteAttributeString("href", styles_relative_path + "style.css"); 68 | Writer.WriteAttributeString("rel", "stylesheet"); 69 | Writer.WriteAttributeString("type", "text/css"); 70 | Writer.WriteEndElement(); 71 | 72 | /* 73 | * @title 74 | */ 75 | Writer.WriteElementString("title", title); 76 | Writer.WriteEndElement(); 77 | 78 | /* 79 | * 80 | */ 81 | Writer.WriteStartElement("body"); 82 | } 83 | 84 | public void AddImage(string filename, string alt, bool should_center) 85 | { 86 | /* 87 | *
88 | * @alt 89 | *
90 | */ 91 | Writer.WriteStartElement("div"); 92 | 93 | // Center image if needed 94 | if (should_center) 95 | Writer.WriteAttributeString("style", "text-align: center;"); 96 | 97 | Writer.WriteStartElement("img"); 98 | Writer.WriteAttributeString("src", "../Images/" + filename); 99 | Writer.WriteAttributeString("alt", alt); 100 | Writer.WriteAttributeString("height", "100%"); 101 | Writer.WriteEndElement(); 102 | Writer.WriteEndElement(); 103 | } 104 | 105 | public void AddText(string element, string content, params HtmlAttribute[] attributes) 106 | { 107 | /* 108 | * <@element @attributes...>@content 109 | */ 110 | Writer.WriteStartElement(element); 111 | if (attributes != null) 112 | { 113 | foreach (var attribute in attributes) 114 | { 115 | // HACK! Don't add attributes with empty names. 116 | if (attribute.LocalName == null) 117 | continue; 118 | 119 | Writer.WriteAttributeString(attribute.LocalName, attribute.Value); 120 | } 121 | } 122 | Writer.WriteString(content); 123 | Writer.WriteEndElement(); 124 | } 125 | 126 | // NOTE: Only the attribute with the LocalName "href" will 127 | // be added to the element, all other will be added to the

element. 128 | public void AddLink(string content, params HtmlAttribute[] attributes) 129 | { 130 | /* 131 | *

132 | * @content 133 | *

134 | */ 135 | Writer.WriteStartElement("p"); 136 | if (attributes != null) 137 | { 138 | foreach (var attribute in attributes) 139 | { 140 | // HACK! Don't add attributes with empty names. 141 | if (attribute.LocalName == null) 142 | continue; 143 | 144 | // Don't add href attribute to

145 | if (attribute.LocalName == "href") 146 | continue; 147 | 148 | Writer.WriteAttributeString(attribute.LocalName, attribute.Value); 149 | } 150 | } 151 | 152 | Writer.WriteStartElement("a"); 153 | if (attributes != null) 154 | { 155 | foreach (var attribute in attributes) 156 | { 157 | // HACK! Don't add attributes with empty names. 158 | if (attribute.LocalName == null) 159 | continue; 160 | 161 | // Add only href attribute to 162 | if (attribute.LocalName != "href") 163 | continue; 164 | 165 | Writer.WriteAttributeString(attribute.LocalName, attribute.Value); 166 | } 167 | } 168 | Writer.WriteString(content); 169 | Writer.WriteEndElement(); 170 | Writer.WriteEndElement(); 171 | } 172 | 173 | public void AddPageBreak() 174 | { 175 | /* 176 | *

177 | */ 178 | Writer.WriteStartElement("div"); 179 | Writer.WriteAttributeString("style", "page-break-before: always;"); 180 | Writer.WriteEndElement(); 181 | } 182 | 183 | public void AddBorder(double width, string style) 184 | { 185 | /* 186 | *
187 | */ 188 | var style_attribute = "border-style: " + style + 189 | "; border-color: rgb(0, 0, 0); border-top-width: 1px; width: " + 190 | width.ToString() + ";"; 191 | 192 | Writer.WriteStartElement("div"); 193 | Writer.WriteAttributeString("style", style_attribute); 194 | Writer.WriteEndElement(); 195 | } 196 | 197 | public void AddHorizontalRule() 198 | { 199 | /* 200 | *
201 | */ 202 | Writer.WriteStartElement("hr"); 203 | Writer.WriteEndElement(); 204 | } 205 | 206 | public void AddSpacer(double size) 207 | { 208 | /* 209 | *
210 | */ 211 | Writer.WriteStartElement("br"); 212 | if (size != 1) 213 | Writer.WriteAttributeString("style", "line-height: " + size); 214 | Writer.WriteEndElement(); 215 | } 216 | 217 | public void StartTable(int column_count) 218 | { 219 | /* 220 | * 221 | * 222 | * ... 223 | * columns) 245 | { 246 | /* 247 | * 248 | * ... 249 | * 250 | * ... 251 | * 252 | */ 253 | Writer.WriteStartElement("tr"); 254 | 255 | foreach (var column in columns) 256 | { 257 | Writer.WriteStartElement("td"); 258 | 259 | // Set style attribute, if there is a style present 260 | if (column.Style != null) 261 | Writer.WriteAttributeString("style", column.Style); 262 | 263 | Writer.WriteString(column.Text); 264 | Writer.WriteEndElement(); 265 | } 266 | 267 | Writer.WriteEndElement(); 268 | } 269 | 270 | public void EndTable() 271 | { 272 | /* 273 | *
@column.Text
274 | */ 275 | Writer.WriteEndElement(); 276 | AddPageBreak(); 277 | } 278 | } 279 | } -------------------------------------------------------------------------------- /MainForm.Designer.cs: -------------------------------------------------------------------------------- 1 | namespace ScribdMpubToEpubConverter 2 | { 3 | partial class MainForm 4 | { 5 | /// 6 | /// Required designer variable. 7 | /// 8 | private System.ComponentModel.IContainer components = null; 9 | 10 | /// 11 | /// Clean up any resources being used. 12 | /// 13 | /// true if managed resources should be disposed; otherwise, false. 14 | protected override void Dispose(bool disposing) 15 | { 16 | if (disposing && (components != null)) 17 | { 18 | components.Dispose(); 19 | } 20 | base.Dispose(disposing); 21 | } 22 | 23 | #region Windows Form Designer generated code 24 | 25 | /// 26 | /// Required method for Designer support - do not modify 27 | /// the contents of this method with the code editor. 28 | /// 29 | private void InitializeComponent() 30 | { 31 | System.ComponentModel.ComponentResourceManager resources = new System.ComponentModel.ComponentResourceManager(typeof(MainForm)); 32 | this.table_layout_panel = new System.Windows.Forms.TableLayoutPanel(); 33 | this.listbox_debug_log = new System.Windows.Forms.ListBox(); 34 | this.groupbox_input_data = new System.Windows.Forms.GroupBox(); 35 | this.button_browse_filename_keys = new System.Windows.Forms.Button(); 36 | this.label_filename_keys_xml = new System.Windows.Forms.Label(); 37 | this.textbox_filename_keys_xml = new System.Windows.Forms.TextBox(); 38 | this.groupbox_conversion_options = new System.Windows.Forms.GroupBox(); 39 | this.checkbox_fix_off_by_one_page_references = new System.Windows.Forms.CheckBox(); 40 | this.checkbox_generate_cover_page = new System.Windows.Forms.CheckBox(); 41 | this.checkbox_show_messagebox_upon_completion = new System.Windows.Forms.CheckBox(); 42 | this.checkbox_enable_decryption = new System.Windows.Forms.CheckBox(); 43 | this.textbox_folder_path = new System.Windows.Forms.TextBox(); 44 | this.button_browse_dialog = new System.Windows.Forms.Button(); 45 | this.button_convert = new System.Windows.Forms.Button(); 46 | this.groupbox_logging_options = new System.Windows.Forms.GroupBox(); 47 | this.label_log_level = new System.Windows.Forms.Label(); 48 | this.combobox_log_level = new System.Windows.Forms.ComboBox(); 49 | this.table_layout_panel.SuspendLayout(); 50 | this.groupbox_input_data.SuspendLayout(); 51 | this.groupbox_conversion_options.SuspendLayout(); 52 | this.groupbox_logging_options.SuspendLayout(); 53 | this.SuspendLayout(); 54 | // 55 | // table_layout_panel 56 | // 57 | this.table_layout_panel.ColumnCount = 4; 58 | this.table_layout_panel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 40F)); 59 | this.table_layout_panel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 10F)); 60 | this.table_layout_panel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 40F)); 61 | this.table_layout_panel.ColumnStyles.Add(new System.Windows.Forms.ColumnStyle(System.Windows.Forms.SizeType.Percent, 10F)); 62 | this.table_layout_panel.Controls.Add(this.listbox_debug_log, 0, 0); 63 | this.table_layout_panel.Controls.Add(this.groupbox_input_data, 0, 1); 64 | this.table_layout_panel.Controls.Add(this.groupbox_conversion_options, 0, 2); 65 | this.table_layout_panel.Controls.Add(this.textbox_folder_path, 0, 3); 66 | this.table_layout_panel.Controls.Add(this.button_browse_dialog, 3, 3); 67 | this.table_layout_panel.Controls.Add(this.button_convert, 0, 4); 68 | this.table_layout_panel.Controls.Add(this.groupbox_logging_options, 2, 2); 69 | this.table_layout_panel.Dock = System.Windows.Forms.DockStyle.Fill; 70 | this.table_layout_panel.GrowStyle = System.Windows.Forms.TableLayoutPanelGrowStyle.FixedSize; 71 | this.table_layout_panel.Location = new System.Drawing.Point(0, 0); 72 | this.table_layout_panel.Margin = new System.Windows.Forms.Padding(0); 73 | this.table_layout_panel.MinimumSize = new System.Drawing.Size(500, 510); 74 | this.table_layout_panel.Name = "table_layout_panel"; 75 | this.table_layout_panel.RowCount = 5; 76 | this.table_layout_panel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 40F)); 77 | this.table_layout_panel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 17.5F)); 78 | this.table_layout_panel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 32.5F)); 79 | this.table_layout_panel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 5F)); 80 | this.table_layout_panel.RowStyles.Add(new System.Windows.Forms.RowStyle(System.Windows.Forms.SizeType.Percent, 5F)); 81 | this.table_layout_panel.Size = new System.Drawing.Size(549, 511); 82 | this.table_layout_panel.TabIndex = 0; 83 | // 84 | // listbox_debug_log 85 | // 86 | this.table_layout_panel.SetColumnSpan(this.listbox_debug_log, 4); 87 | this.listbox_debug_log.Dock = System.Windows.Forms.DockStyle.Fill; 88 | this.listbox_debug_log.Font = new System.Drawing.Font("Courier New", 9F, System.Drawing.FontStyle.Regular, System.Drawing.GraphicsUnit.Point, ((byte)(0))); 89 | this.listbox_debug_log.FormattingEnabled = true; 90 | this.listbox_debug_log.HorizontalScrollbar = true; 91 | this.listbox_debug_log.ItemHeight = 15; 92 | this.listbox_debug_log.Location = new System.Drawing.Point(5, 5); 93 | this.listbox_debug_log.Margin = new System.Windows.Forms.Padding(5); 94 | this.listbox_debug_log.Name = "listbox_debug_log"; 95 | this.listbox_debug_log.SelectionMode = System.Windows.Forms.SelectionMode.MultiExtended; 96 | this.listbox_debug_log.Size = new System.Drawing.Size(539, 194); 97 | this.listbox_debug_log.TabIndex = 0; 98 | // 99 | // groupbox_input_data 100 | // 101 | this.table_layout_panel.SetColumnSpan(this.groupbox_input_data, 4); 102 | this.groupbox_input_data.Controls.Add(this.button_browse_filename_keys); 103 | this.groupbox_input_data.Controls.Add(this.label_filename_keys_xml); 104 | this.groupbox_input_data.Controls.Add(this.textbox_filename_keys_xml); 105 | this.groupbox_input_data.Dock = System.Windows.Forms.DockStyle.Fill; 106 | this.groupbox_input_data.Location = new System.Drawing.Point(5, 204); 107 | this.groupbox_input_data.Margin = new System.Windows.Forms.Padding(5, 0, 5, 5); 108 | this.groupbox_input_data.Name = "groupbox_input_data"; 109 | this.groupbox_input_data.Padding = new System.Windows.Forms.Padding(0); 110 | this.groupbox_input_data.Size = new System.Drawing.Size(539, 84); 111 | this.groupbox_input_data.TabIndex = 1; 112 | this.groupbox_input_data.TabStop = false; 113 | this.groupbox_input_data.Text = "Input data"; 114 | // 115 | // button_browse_filename_keys 116 | // 117 | this.button_browse_filename_keys.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 118 | | System.Windows.Forms.AnchorStyles.Left) 119 | | System.Windows.Forms.AnchorStyles.Right))); 120 | this.button_browse_filename_keys.Location = new System.Drawing.Point(5, 60); 121 | this.button_browse_filename_keys.Margin = new System.Windows.Forms.Padding(5); 122 | this.button_browse_filename_keys.MaximumSize = new System.Drawing.Size(0, 25); 123 | this.button_browse_filename_keys.MinimumSize = new System.Drawing.Size(525, 20); 124 | this.button_browse_filename_keys.Name = "button_browse_filename_keys"; 125 | this.button_browse_filename_keys.Size = new System.Drawing.Size(525, 20); 126 | this.button_browse_filename_keys.TabIndex = 2; 127 | this.button_browse_filename_keys.Text = "..."; 128 | this.button_browse_filename_keys.UseVisualStyleBackColor = true; 129 | this.button_browse_filename_keys.Click += new System.EventHandler(this.OnButtonBrowseFilenameKeysClick); 130 | // 131 | // label_filename_keys_xml 132 | // 133 | this.label_filename_keys_xml.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 134 | | System.Windows.Forms.AnchorStyles.Left) 135 | | System.Windows.Forms.AnchorStyles.Right))); 136 | this.label_filename_keys_xml.AutoSize = true; 137 | this.label_filename_keys_xml.Location = new System.Drawing.Point(5, 15); 138 | this.label_filename_keys_xml.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); 139 | this.label_filename_keys_xml.Name = "label_filename_keys_xml"; 140 | this.label_filename_keys_xml.Size = new System.Drawing.Size(136, 13); 141 | this.label_filename_keys_xml.TabIndex = 1; 142 | this.label_filename_keys_xml.Text = "FILENAME_KEYS.xml path"; 143 | // 144 | // textbox_filename_keys_xml 145 | // 146 | this.textbox_filename_keys_xml.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 147 | | System.Windows.Forms.AnchorStyles.Left) 148 | | System.Windows.Forms.AnchorStyles.Right))); 149 | this.textbox_filename_keys_xml.Location = new System.Drawing.Point(5, 35); 150 | this.textbox_filename_keys_xml.Margin = new System.Windows.Forms.Padding(5); 151 | this.textbox_filename_keys_xml.MaxLength = 32; 152 | this.textbox_filename_keys_xml.MinimumSize = new System.Drawing.Size(4, 20); 153 | this.textbox_filename_keys_xml.Name = "textbox_filename_keys_xml"; 154 | this.textbox_filename_keys_xml.Size = new System.Drawing.Size(526, 20); 155 | this.textbox_filename_keys_xml.TabIndex = 0; 156 | // 157 | // groupbox_conversion_options 158 | // 159 | this.table_layout_panel.SetColumnSpan(this.groupbox_conversion_options, 2); 160 | this.groupbox_conversion_options.Controls.Add(this.checkbox_fix_off_by_one_page_references); 161 | this.groupbox_conversion_options.Controls.Add(this.checkbox_generate_cover_page); 162 | this.groupbox_conversion_options.Controls.Add(this.checkbox_show_messagebox_upon_completion); 163 | this.groupbox_conversion_options.Controls.Add(this.checkbox_enable_decryption); 164 | this.groupbox_conversion_options.Dock = System.Windows.Forms.DockStyle.Fill; 165 | this.groupbox_conversion_options.Location = new System.Drawing.Point(5, 293); 166 | this.groupbox_conversion_options.Margin = new System.Windows.Forms.Padding(5, 0, 5, 5); 167 | this.groupbox_conversion_options.Name = "groupbox_conversion_options"; 168 | this.groupbox_conversion_options.Padding = new System.Windows.Forms.Padding(0); 169 | this.groupbox_conversion_options.Size = new System.Drawing.Size(263, 161); 170 | this.groupbox_conversion_options.TabIndex = 2; 171 | this.groupbox_conversion_options.TabStop = false; 172 | this.groupbox_conversion_options.Text = "Conversion options"; 173 | // 174 | // checkbox_fix_off_by_one_page_references 175 | // 176 | this.checkbox_fix_off_by_one_page_references.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 177 | | System.Windows.Forms.AnchorStyles.Left) 178 | | System.Windows.Forms.AnchorStyles.Right))); 179 | this.checkbox_fix_off_by_one_page_references.AutoSize = true; 180 | this.checkbox_fix_off_by_one_page_references.Checked = true; 181 | this.checkbox_fix_off_by_one_page_references.CheckState = System.Windows.Forms.CheckState.Checked; 182 | this.checkbox_fix_off_by_one_page_references.Location = new System.Drawing.Point(10, 80); 183 | this.checkbox_fix_off_by_one_page_references.Margin = new System.Windows.Forms.Padding(5); 184 | this.checkbox_fix_off_by_one_page_references.Name = "checkbox_fix_off_by_one_page_references"; 185 | this.checkbox_fix_off_by_one_page_references.Size = new System.Drawing.Size(217, 17); 186 | this.checkbox_fix_off_by_one_page_references.TabIndex = 3; 187 | this.checkbox_fix_off_by_one_page_references.Text = "Attempt to fix off-by-one page references"; 188 | this.checkbox_fix_off_by_one_page_references.UseVisualStyleBackColor = true; 189 | // 190 | // checkbox_generate_cover_page 191 | // 192 | this.checkbox_generate_cover_page.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 193 | | System.Windows.Forms.AnchorStyles.Left) 194 | | System.Windows.Forms.AnchorStyles.Right))); 195 | this.checkbox_generate_cover_page.AutoSize = true; 196 | this.checkbox_generate_cover_page.Location = new System.Drawing.Point(10, 40); 197 | this.checkbox_generate_cover_page.Margin = new System.Windows.Forms.Padding(10, 20, 20, 10); 198 | this.checkbox_generate_cover_page.Name = "checkbox_generate_cover_page"; 199 | this.checkbox_generate_cover_page.Size = new System.Drawing.Size(127, 17); 200 | this.checkbox_generate_cover_page.TabIndex = 2; 201 | this.checkbox_generate_cover_page.Text = "Generate cover page"; 202 | this.checkbox_generate_cover_page.UseVisualStyleBackColor = true; 203 | // 204 | // checkbox_show_messagebox_upon_completion 205 | // 206 | this.checkbox_show_messagebox_upon_completion.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 207 | | System.Windows.Forms.AnchorStyles.Left) 208 | | System.Windows.Forms.AnchorStyles.Right))); 209 | this.checkbox_show_messagebox_upon_completion.AutoSize = true; 210 | this.checkbox_show_messagebox_upon_completion.Checked = true; 211 | this.checkbox_show_messagebox_upon_completion.CheckState = System.Windows.Forms.CheckState.Checked; 212 | this.checkbox_show_messagebox_upon_completion.Location = new System.Drawing.Point(10, 60); 213 | this.checkbox_show_messagebox_upon_completion.Margin = new System.Windows.Forms.Padding(5); 214 | this.checkbox_show_messagebox_upon_completion.Name = "checkbox_show_messagebox_upon_completion"; 215 | this.checkbox_show_messagebox_upon_completion.Size = new System.Drawing.Size(196, 17); 216 | this.checkbox_show_messagebox_upon_completion.TabIndex = 1; 217 | this.checkbox_show_messagebox_upon_completion.Text = "Show messagebox upon completion"; 218 | this.checkbox_show_messagebox_upon_completion.UseVisualStyleBackColor = true; 219 | // 220 | // checkbox_enable_decryption 221 | // 222 | this.checkbox_enable_decryption.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 223 | | System.Windows.Forms.AnchorStyles.Left) 224 | | System.Windows.Forms.AnchorStyles.Right))); 225 | this.checkbox_enable_decryption.AutoSize = true; 226 | this.checkbox_enable_decryption.Checked = true; 227 | this.checkbox_enable_decryption.CheckState = System.Windows.Forms.CheckState.Checked; 228 | this.checkbox_enable_decryption.Location = new System.Drawing.Point(10, 20); 229 | this.checkbox_enable_decryption.Margin = new System.Windows.Forms.Padding(10, 20, 20, 10); 230 | this.checkbox_enable_decryption.Name = "checkbox_enable_decryption"; 231 | this.checkbox_enable_decryption.Size = new System.Drawing.Size(111, 17); 232 | this.checkbox_enable_decryption.TabIndex = 0; 233 | this.checkbox_enable_decryption.Text = "Enable decryption"; 234 | this.checkbox_enable_decryption.UseVisualStyleBackColor = true; 235 | // 236 | // textbox_folder_path 237 | // 238 | this.table_layout_panel.SetColumnSpan(this.textbox_folder_path, 3); 239 | this.textbox_folder_path.Dock = System.Windows.Forms.DockStyle.Fill; 240 | this.textbox_folder_path.Enabled = false; 241 | this.textbox_folder_path.Location = new System.Drawing.Point(5, 464); 242 | this.textbox_folder_path.Margin = new System.Windows.Forms.Padding(5); 243 | this.textbox_folder_path.MinimumSize = new System.Drawing.Size(250, 20); 244 | this.textbox_folder_path.Name = "textbox_folder_path"; 245 | this.textbox_folder_path.Size = new System.Drawing.Size(482, 20); 246 | this.textbox_folder_path.TabIndex = 3; 247 | // 248 | // button_browse_dialog 249 | // 250 | this.button_browse_dialog.Dock = System.Windows.Forms.DockStyle.Fill; 251 | this.button_browse_dialog.Enabled = false; 252 | this.button_browse_dialog.Location = new System.Drawing.Point(497, 464); 253 | this.button_browse_dialog.Margin = new System.Windows.Forms.Padding(5); 254 | this.button_browse_dialog.MaximumSize = new System.Drawing.Size(0, 20); 255 | this.button_browse_dialog.MinimumSize = new System.Drawing.Size(0, 20); 256 | this.button_browse_dialog.Name = "button_browse_dialog"; 257 | this.button_browse_dialog.Size = new System.Drawing.Size(47, 20); 258 | this.button_browse_dialog.TabIndex = 4; 259 | this.button_browse_dialog.Text = "&..."; 260 | this.button_browse_dialog.UseVisualStyleBackColor = true; 261 | this.button_browse_dialog.Click += new System.EventHandler(this.OnButtonBrowseDialogClick); 262 | // 263 | // button_convert 264 | // 265 | this.table_layout_panel.SetColumnSpan(this.button_convert, 4); 266 | this.button_convert.Dock = System.Windows.Forms.DockStyle.Fill; 267 | this.button_convert.Enabled = false; 268 | this.button_convert.Location = new System.Drawing.Point(5, 489); 269 | this.button_convert.Margin = new System.Windows.Forms.Padding(5); 270 | this.button_convert.MaximumSize = new System.Drawing.Size(0, 25); 271 | this.button_convert.MinimumSize = new System.Drawing.Size(0, 20); 272 | this.button_convert.Name = "button_convert"; 273 | this.button_convert.Size = new System.Drawing.Size(539, 20); 274 | this.button_convert.TabIndex = 5; 275 | this.button_convert.Text = "&Convert"; 276 | this.button_convert.UseVisualStyleBackColor = true; 277 | this.button_convert.Click += new System.EventHandler(this.OnButtonConvertClick); 278 | // 279 | // groupbox_logging_options 280 | // 281 | this.table_layout_panel.SetColumnSpan(this.groupbox_logging_options, 2); 282 | this.groupbox_logging_options.Controls.Add(this.label_log_level); 283 | this.groupbox_logging_options.Controls.Add(this.combobox_log_level); 284 | this.groupbox_logging_options.Dock = System.Windows.Forms.DockStyle.Fill; 285 | this.groupbox_logging_options.Location = new System.Drawing.Point(278, 293); 286 | this.groupbox_logging_options.Margin = new System.Windows.Forms.Padding(5, 0, 5, 5); 287 | this.groupbox_logging_options.Name = "groupbox_logging_options"; 288 | this.groupbox_logging_options.Padding = new System.Windows.Forms.Padding(5); 289 | this.groupbox_logging_options.Size = new System.Drawing.Size(266, 161); 290 | this.groupbox_logging_options.TabIndex = 6; 291 | this.groupbox_logging_options.TabStop = false; 292 | this.groupbox_logging_options.Text = "Logging options"; 293 | // 294 | // label_log_level 295 | // 296 | this.label_log_level.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 297 | | System.Windows.Forms.AnchorStyles.Left) 298 | | System.Windows.Forms.AnchorStyles.Right))); 299 | this.label_log_level.AutoSize = true; 300 | this.label_log_level.Location = new System.Drawing.Point(5, 15); 301 | this.label_log_level.Margin = new System.Windows.Forms.Padding(5, 0, 5, 0); 302 | this.label_log_level.Name = "label_log_level"; 303 | this.label_log_level.Size = new System.Drawing.Size(50, 13); 304 | this.label_log_level.TabIndex = 3; 305 | this.label_log_level.Text = "Log level"; 306 | // 307 | // combobox_log_level 308 | // 309 | this.combobox_log_level.Anchor = ((System.Windows.Forms.AnchorStyles)((((System.Windows.Forms.AnchorStyles.Top | System.Windows.Forms.AnchorStyles.Bottom) 310 | | System.Windows.Forms.AnchorStyles.Left) 311 | | System.Windows.Forms.AnchorStyles.Right))); 312 | this.combobox_log_level.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList; 313 | this.combobox_log_level.FormattingEnabled = true; 314 | this.combobox_log_level.Location = new System.Drawing.Point(5, 35); 315 | this.combobox_log_level.Name = "combobox_log_level"; 316 | this.combobox_log_level.Size = new System.Drawing.Size(255, 21); 317 | this.combobox_log_level.TabIndex = 0; 318 | this.combobox_log_level.SelectedIndexChanged += new System.EventHandler(this.OnComboboxLogLevelSelectedIndexChanged); 319 | // 320 | // MainForm 321 | // 322 | this.AutoScaleDimensions = new System.Drawing.SizeF(6F, 13F); 323 | this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font; 324 | this.ClientSize = new System.Drawing.Size(549, 511); 325 | this.Controls.Add(this.table_layout_panel); 326 | this.Icon = ((System.Drawing.Icon)(resources.GetObject("$this.Icon"))); 327 | this.MinimumSize = new System.Drawing.Size(565, 550); 328 | this.Name = "MainForm"; 329 | this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen; 330 | this.Text = "SCRIMTEC - SCRIbd Mpub To Epub Converter"; 331 | this.table_layout_panel.ResumeLayout(false); 332 | this.table_layout_panel.PerformLayout(); 333 | this.groupbox_input_data.ResumeLayout(false); 334 | this.groupbox_input_data.PerformLayout(); 335 | this.groupbox_conversion_options.ResumeLayout(false); 336 | this.groupbox_conversion_options.PerformLayout(); 337 | this.groupbox_logging_options.ResumeLayout(false); 338 | this.groupbox_logging_options.PerformLayout(); 339 | this.ResumeLayout(false); 340 | 341 | } 342 | 343 | #endregion 344 | 345 | private System.Windows.Forms.TableLayoutPanel table_layout_panel; 346 | private System.Windows.Forms.ListBox listbox_debug_log; 347 | private System.Windows.Forms.GroupBox groupbox_input_data; 348 | private System.Windows.Forms.TextBox textbox_folder_path; 349 | private System.Windows.Forms.Button button_browse_dialog; 350 | private System.Windows.Forms.Button button_convert; 351 | private System.Windows.Forms.Button button_browse_filename_keys; 352 | private System.Windows.Forms.Label label_filename_keys_xml; 353 | private System.Windows.Forms.TextBox textbox_filename_keys_xml; 354 | private System.Windows.Forms.GroupBox groupbox_conversion_options; 355 | private System.Windows.Forms.CheckBox checkbox_enable_decryption; 356 | private System.Windows.Forms.GroupBox groupbox_logging_options; 357 | private System.Windows.Forms.Label label_log_level; 358 | private System.Windows.Forms.ComboBox combobox_log_level; 359 | private System.Windows.Forms.CheckBox checkbox_show_messagebox_upon_completion; 360 | private System.Windows.Forms.CheckBox checkbox_generate_cover_page; 361 | private System.Windows.Forms.CheckBox checkbox_fix_off_by_one_page_references; 362 | } 363 | } 364 | 365 | -------------------------------------------------------------------------------- /MainForm.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Windows.Forms; 6 | 7 | namespace ScribdMpubToEpubConverter 8 | { 9 | public partial class MainForm : Form 10 | { 11 | private readonly Scribd.Decryptor Decryptor = new Scribd.Decryptor(); 12 | private Dictionary DecryptionKeys = new Dictionary(); 13 | 14 | public MainForm() 15 | { 16 | InitializeComponent(); 17 | 18 | // Overwrite debug log buffer 19 | listbox_debug_log.DataSource = Helper.DebugLog; 20 | 21 | // Add logger levels 22 | combobox_log_level.Items.AddRange(Logger.Levels); 23 | 24 | // Set logger level 25 | combobox_log_level.SelectedIndex = Logger.CurrentLevel; 26 | 27 | // Set debug log path 28 | Helper.DebugLogPath = Directory.GetCurrentDirectory() + 29 | Path.DirectorySeparatorChar + 30 | "debug_log.txt"; 31 | 32 | // Delete previous debug log file 33 | File.Delete(Helper.DebugLogPath); 34 | 35 | Helper.Information("Logging session to " + Helper.DebugLogPath); 36 | Helper.Information("---------------------------------"); 37 | Helper.Information("https://github.com/BELGRADE-OUTLAW/SCRIMTEC"); 38 | } 39 | 40 | private void ToggleInputElements(bool state) 41 | { 42 | button_browse_dialog.Enabled = textbox_folder_path.Enabled = state; 43 | } 44 | 45 | private void DecryptDirectories() 46 | { 47 | var used_keys = new Dictionary(); 48 | 49 | var subdirectory_list = Directory.GetDirectories(textbox_folder_path.Text); 50 | foreach (var subdirectory in subdirectory_list) 51 | { 52 | var subdirectory_name = Path.GetFileName(subdirectory); 53 | foreach (var decryption_key in DecryptionKeys) 54 | { 55 | if (subdirectory_name != decryption_key.Key) 56 | continue; 57 | 58 | Helper.Debug("Decrypting directory: " + subdirectory); 59 | 60 | Decryptor.GeneratePrivateKey(decryption_key.Value); 61 | Decryptor.Decrypt(subdirectory); 62 | 63 | used_keys.Add(decryption_key.Key, decryption_key.Value); 64 | } 65 | } 66 | 67 | var unused_keys = DecryptionKeys.Except(used_keys); 68 | if (unused_keys.Count() != 0) 69 | { 70 | Helper.Information("Unused decryption keys: " + unused_keys.Count()); 71 | foreach (var decryption_key in unused_keys) 72 | { 73 | Helper.Information( 74 | "- " + decryption_key.Key + 75 | " with value: " + decryption_key.Value 76 | ); 77 | } 78 | } 79 | } 80 | 81 | private void ConvertDirectoryToEPUB(string subdirectory) 82 | { 83 | Helper.Debug("Converting directory: " + subdirectory); 84 | 85 | // Parse book content 86 | var book = new Scribd.Book(subdirectory); 87 | 88 | // Convert parsed content to EPUB 89 | var book_converter = new Scribd.BookConverter( 90 | book, 91 | checkbox_generate_cover_page.Checked, 92 | checkbox_fix_off_by_one_page_references.Checked 93 | ); 94 | 95 | book_converter.Convert(); 96 | 97 | // Generate and save EPUB 98 | var epub = new EPUB2.BookGenerator(book); 99 | epub.Generate(); 100 | } 101 | 102 | private void ConvertDirectories() 103 | { 104 | var subdirectory_list = Directory.GetDirectories(textbox_folder_path.Text); 105 | foreach (var subdirectory in subdirectory_list) 106 | { 107 | var subdirectory_name = Path.GetFileName(subdirectory); 108 | foreach (var decryption_key in DecryptionKeys) 109 | { 110 | if (subdirectory_name != decryption_key.Key) 111 | continue; 112 | 113 | // Every book must have book_metadata.json 114 | var is_book = File.Exists( 115 | subdirectory + Path.DirectorySeparatorChar + "book_metadata.json" 116 | ); 117 | 118 | if (!is_book) 119 | continue; 120 | 121 | ConvertDirectoryToEPUB(subdirectory); 122 | } 123 | } 124 | } 125 | 126 | private void OnButtonConvertClick(object sender, EventArgs e) 127 | { 128 | if (textbox_folder_path.Text.Length == 0) 129 | { 130 | Helper.ShowWarning("Selected directory path is empty!"); 131 | return; 132 | } 133 | 134 | if (!Directory.Exists(textbox_folder_path.Text)) 135 | { 136 | Helper.ShowWarning("Selected directory doesn't exist!"); 137 | return; 138 | } 139 | 140 | // Reset warning count to see how many warnings happened during 141 | // decryption/conversion/both and finally inform the user if they did. 142 | Helper.WarningCount = 0; 143 | 144 | try 145 | { 146 | if (checkbox_enable_decryption.Checked) 147 | DecryptDirectories(); 148 | 149 | ConvertDirectories(); 150 | 151 | switch (Helper.WarningCount) 152 | { 153 | case 0: 154 | if (checkbox_show_messagebox_upon_completion.Checked) 155 | { 156 | MessageBox.Show( 157 | "The conversion job has been finished.", 158 | "Information", 159 | MessageBoxButtons.OK, 160 | MessageBoxIcon.Information 161 | ); 162 | } 163 | break; 164 | default: 165 | var message = "The conversion job has encountered " + 166 | Helper.WarningCount + " warning" + 167 | (Helper.WarningCount != 1 ? "s" : "") + 168 | " during conversion!"; 169 | Helper.ShowWarning(message + "\nPlease check the debug log for details."); 170 | Helper.Warning(message); 171 | break; 172 | } 173 | } 174 | catch (Exception exception) 175 | { 176 | var message = exception.Message + "\n" + 177 | exception.StackTrace + "\n" + 178 | exception.Source + "\n" + 179 | exception.ToString() + "\n"; 180 | 181 | Helper.ShowWarning(message, "An exception was thrown!"); 182 | Helper.Warning(message); 183 | } 184 | } 185 | 186 | private void OnButtonBrowseFilenameKeysClick(object sender, EventArgs e) 187 | { 188 | try 189 | { 190 | using var dialog = new OpenFileDialog 191 | { 192 | InitialDirectory = Directory.GetCurrentDirectory(), 193 | RestoreDirectory = true, 194 | Multiselect = false, 195 | Filter = "Scribd decryption key list|FILENAME_KEYS.xml" 196 | }; 197 | 198 | if (dialog.ShowDialog() == DialogResult.OK) 199 | { 200 | textbox_filename_keys_xml.Text = dialog.FileName; 201 | 202 | // Remove all decryption keys, just in case 203 | if (DecryptionKeys.Count != 0) 204 | { 205 | DecryptionKeys.Clear(); 206 | Helper.Debug("Cleaning up leftover decryption keys"); 207 | } 208 | 209 | DecryptionKeyListParser.Parse( 210 | dialog.FileName, 211 | ref DecryptionKeys 212 | ); 213 | 214 | ToggleInputElements(true); 215 | } 216 | } 217 | catch (Exception exception) 218 | { 219 | var message = exception.Message + "\n" + 220 | exception.StackTrace + "\n" + 221 | exception.Source + "\n" + 222 | exception.ToString() + "\n"; 223 | 224 | Helper.ShowWarning(message, "An exception was thrown!"); 225 | Helper.Warning(message); 226 | } 227 | } 228 | 229 | private void OnButtonBrowseDialogClick(object sender, EventArgs e) 230 | { 231 | using var dialog = new FolderBrowserDialog 232 | { 233 | Description = "Select your document_cache directory", 234 | SelectedPath = Directory.GetCurrentDirectory() 235 | }; 236 | 237 | if (dialog.ShowDialog() == DialogResult.OK) 238 | { 239 | var directory_name = Path.GetFileName(dialog.SelectedPath); 240 | if (directory_name != "document_cache") 241 | { 242 | Helper.ShowWarning("Please select your document_cache directory!"); 243 | OnButtonBrowseDialogClick(sender, e); 244 | return; 245 | } 246 | 247 | textbox_folder_path.Text = dialog.SelectedPath; 248 | button_convert.Enabled = true; 249 | } 250 | } 251 | 252 | private void OnComboboxLogLevelSelectedIndexChanged(object sender, EventArgs e) 253 | { 254 | Logger.CurrentLevel = (sender as ComboBox).SelectedIndex; 255 | } 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Windows.Forms; 3 | 4 | namespace ScribdMpubToEpubConverter 5 | { 6 | static class Program 7 | { 8 | /// 9 | /// The main entry point for the application. 10 | /// 11 | [STAThread] 12 | static void Main() 13 | { 14 | Application.EnableVisualStyles(); 15 | Application.SetCompatibleTextRenderingDefault(false); 16 | Application.Run(new MainForm()); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("ScribdMpubToEpubConverter")] 8 | [assembly: AssemblyDescription("Makes the conversion from Scribd's internal MPUB format to the widely-used EPUB2.0.1 format possible.")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("ScribdMpubToEpubConverter")] 12 | [assembly: AssemblyCopyright("")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("00000000-0000-0000-0000-000000000000")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("1.0.*")] 34 | [assembly: AssemblyVersion("1.0.0.0")] 35 | [assembly: AssemblyFileVersion("1.0.0.0")] 36 | -------------------------------------------------------------------------------- /Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ScribdMpubToEpubConverter.Properties 12 | { 13 | 14 | 15 | /// 16 | /// A strongly-typed resource class, for looking up localized strings, etc. 17 | /// 18 | // This class was auto-generated by the StronglyTypedResourceBuilder 19 | // class via a tool like ResGen or Visual Studio. 20 | // To add or remove a member, edit your .ResX file then rerun ResGen 21 | // with the /str option, or rebuild your VS project. 22 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] 23 | [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] 24 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 25 | internal class Resources 26 | { 27 | 28 | private static global::System.Resources.ResourceManager resourceMan; 29 | 30 | private static global::System.Globalization.CultureInfo resourceCulture; 31 | 32 | [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] 33 | internal Resources() 34 | { 35 | } 36 | 37 | /// 38 | /// Returns the cached ResourceManager instance used by this class. 39 | /// 40 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 41 | internal static global::System.Resources.ResourceManager ResourceManager 42 | { 43 | get 44 | { 45 | if ((resourceMan == null)) 46 | { 47 | global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("ScribdMpubToEpubConverter.Properties.Resources", typeof(Resources).Assembly); 48 | resourceMan = temp; 49 | } 50 | return resourceMan; 51 | } 52 | } 53 | 54 | /// 55 | /// Overrides the current thread's CurrentUICulture property for all 56 | /// resource lookups using this strongly typed resource class. 57 | /// 58 | [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] 59 | internal static global::System.Globalization.CultureInfo Culture 60 | { 61 | get 62 | { 63 | return resourceCulture; 64 | } 65 | set 66 | { 67 | resourceCulture = value; 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /Properties/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | text/microsoft-resx 107 | 108 | 109 | 2.0 110 | 111 | 112 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 113 | 114 | 115 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | -------------------------------------------------------------------------------- /Properties/Settings.Designer.cs: -------------------------------------------------------------------------------- 1 | //------------------------------------------------------------------------------ 2 | // 3 | // This code was generated by a tool. 4 | // Runtime Version:4.0.30319.42000 5 | // 6 | // Changes to this file may cause incorrect behavior and will be lost if 7 | // the code is regenerated. 8 | // 9 | //------------------------------------------------------------------------------ 10 | 11 | namespace ScribdMpubToEpubConverter.Properties 12 | { 13 | 14 | 15 | [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] 16 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.VisualStudio.Editors.SettingsDesigner.SettingsSingleFileGenerator", "11.0.0.0")] 17 | internal sealed partial class Settings : global::System.Configuration.ApplicationSettingsBase 18 | { 19 | 20 | private static Settings defaultInstance = ((Settings)(global::System.Configuration.ApplicationSettingsBase.Synchronized(new Settings()))); 21 | 22 | public static Settings Default 23 | { 24 | get 25 | { 26 | return defaultInstance; 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Properties/Settings.settings: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SCRIMTEC - SCRIbd MPUB to EPUB Converter 2 | ![Screenshot](image.png) 3 | 4 | ## Download 5 | https://github.com/BELGRADE-OUTLAW/SCRIMTEC/releases 6 | 7 | ## Features 8 | - Converts MPUB to EPUB 9 | - Decrypts DRM-encrypted books 10 | - Decodes DRM-encoded PDF files*, such as sheet music 11 | 12 | _* only files that don't start with `%PDF` and are named **content** (without extension)_ 13 | 14 | ## Compiling 15 | - Visual Studio 2019 16 | - .NET Framework 4.7.2 17 | - C# 8.0 18 | 19 | When you open the solution, make sure to install Newtonsoft.JSON by going to "Tools -> NuGet Package Manager -> Manage NuGet Packages for Solution...", then a "Restore" button should appear in the top right corner, click on it and compile. 20 | 21 | ## Third-party code 22 | - [Newtonsoft.Json](https://www.newtonsoft.com/json) from NuGet 23 | - Slightly modified [ZipStorer](https://github.com/jaime-olivares/zipstorer) 24 | 25 | ## Acquiring book data 26 | _(on Android with root access; you can use an emulator)_ 27 | 28 | 1. Download the book(s) from the Scribd application and skip through a few chapters for each one (just so you're sure it has been successfully downloaded; isn't necessary, but recommended) 29 | 2. Exit the Scribd application 30 | 3. Go to `/data/data/com.scribd.app.reader0/shared_prefs/` and copy `FILENAME_KEYS.xml` to your PC 31 | 4. Go to `/storage/emulated/0/Android/data/com.scribd.app.reader0/files/` and copy the entire `document_cache` folder to your PC 32 | 33 | ## Usage 34 | 5. Start SCRIMTEC 35 | 6. Click on the big "..." button beneath "`FILENAME_KEYS.xml` path" and locate your `FILENAME_KEYS.xml` file 36 | 7. Click on the little "..." button above "Convert" and locate your `document_cache` folder 37 | 8. Click convert 38 | 9. ??? 39 | 10. Profit! 40 | 41 | ## Troubleshooting 42 | If conversion fails, then double check if the files are intact. Files get corrupted oftenly during Android -> PC transfer. 43 | 44 | ## Android guide using MEmu 45 | 1. Download and install MEmu from http://memuplay.com/ 46 | 2. Download APKPure APK from https://apkpure.com/apkpure-app.html to your PC 47 | 3. Start Multi-MEmu 48 | 4. Install Android 7.1 x64 by clicking "New" in the bottom right corner 49 | 5. Start the newly installed Android 7.1 instance 50 | 6. Go to Settings -> Security 51 | - Scroll down until you see "Unknown sources" 52 | - Enable this option. A warning should appear, click *OK* 53 | 7. Install APKPure APK that you previously downloaded by clicking "APK" in the right-side pane of MEmu 54 | 8. Start APKPure and install Scribd 55 | - If a warning from Google Play Protect shows up, click *ALLOW* 56 | 9. Start Scribd and sign in 57 | 10. Find a book that you want to convert and download it 58 | - Optionally, if you run into issues, open the book and drag through each chapter 59 | - When the download is finished, go back to your Home screen 60 | 11. Open APKPure again and install Total Commander, or a file manager of your choice 61 | 12. Add bookmarks for faster future navigation. See video on Streamable 62 | - To add bookmarks in Total Commander, on the right-hand side of the header you will find 4 icons. Click on the one with the star, located left of the search icon 63 | - Bookmark `/data/data/com.scribd.app.reader0/shared_prefs/` 64 | - Bookmark `/storage/emulated/0/Android/data/com.scribd.app.reader0/files/` 65 | - Bookmark the Downloads folder 66 | 13. Navigate to the files folder bookmark 67 | - Copy `document_cache` to clipboard by long-pressing on the folder 68 | - Click the blue diskette icon next to the big "Total Commander" text 69 | - Go to the Download folder bookmark and paste the folder there 70 | 14. Navigate to the shared_prefs bookmark 71 | - Copy `FILENAME_KEYS.xml` to clipboard by long-pressing on the file 72 | - Click the blue diskette icon next to the big "Total Commander" text 73 | - Go to the Download folder bookmark and paste the file there 74 | 75 | After you've done all this, the files can be found in C:/Users/`User`/Downloads/MEmu Download/ 76 | 77 | Now you can continue with the conversion as described in **Usage**. 78 | -------------------------------------------------------------------------------- /Scribd/Book.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | 5 | namespace ScribdMpubToEpubConverter.Scribd 6 | { 7 | struct HtmlImage 8 | { 9 | public string AbsoluteFilePath { get; set; } 10 | public byte[] Content { get; set; } 11 | public string ID { get; set; } 12 | } 13 | 14 | struct Chapter 15 | { 16 | // Content of this chapter 17 | public ChapterContent Content { get; set; } 18 | 19 | // Title of this chapter 20 | public string Title { get; set; } 21 | 22 | // The file path is relative to the current table of content file directory 23 | public string FilePath { get; set; } 24 | 25 | // Type of this chapter (i.e. frontmatter, content) 26 | public string Type { get; set; } 27 | 28 | // HTML content of this chapter, generated by the parser 29 | public string HTML { get; set; } 30 | 31 | // Name of this HTML file, generated by the parser 32 | public string OutputFileName { get; set; } 33 | } 34 | 35 | // TODO: Compare number of folders found in chapters/ to 36 | // the number of chapters found in the Table of Content? 37 | // TODO: Check the name of titles found in each Chapter and those 38 | // reported by Table of Content? 39 | /** 40 | * This class loads information in the following order: 41 | * 1) Styles (AcquireStyles) 42 | * 2) Metadata (AcquireMetadata) 43 | * 3) Tags (AcquireTags) 44 | * 3) Book title (AcquireBookTitle) 45 | * 4) Table of Content (AcquireContent) 46 | * 5) Chapters (AcquireContent) 47 | */ 48 | class Book 49 | { 50 | // Main directory of this book 51 | // Where the table of contents, metadata and other JSON files are located 52 | public string Directory { get; set; } 53 | 54 | // Title of this book, extracted from book_metadata.json 55 | public string Title { get; set; } 56 | 57 | // ID of this book, extracted from folder name 58 | public string ID { get; set; } 59 | 60 | // Cover page is listed as a separate chapter 61 | // NOTE: Only HTML and Title fields are populated for the cover page 62 | public Chapter CoverPage { get; set; } 63 | 64 | // Holds information on all of the chapters 65 | public AdditionalChapterInformation[] TableOfContents { get; set; } 66 | 67 | // List of all chapters 68 | public Chapter[] Chapters { get; set; } 69 | 70 | // Holds a list of predefined CSS styles in the following way: 71 | // Style name, Style content 72 | public Dictionary Styles { get; set; } 73 | 74 | // List of all metadata information 75 | public Dictionary Metadata { get; set; } 76 | 77 | // Holds a list of predefined page tags that we're going 78 | // to use to relink link attributes 79 | public Dictionary Tags = new Dictionary(); 80 | 81 | // List of all images, in the following way: 82 | // Image name (without extension), HtmlImage 83 | public Dictionary Images = new Dictionary(); 84 | 85 | public Book(string book_directory) 86 | { 87 | Directory = book_directory; 88 | ID = Path.GetFileName(book_directory); 89 | 90 | // styles.json should be present in a book 91 | var styles_path = book_directory + Path.DirectorySeparatorChar + "styles.json"; 92 | if (File.Exists(styles_path)) 93 | AcquireStyles(styles_path); 94 | 95 | // metadata.json should be present in a book 96 | var metadata_path = book_directory + Path.DirectorySeparatorChar + "metadata.json"; 97 | if (File.Exists(metadata_path)) 98 | AcquireMetadata(metadata_path); 99 | 100 | // tags.json should be present in a book 101 | var tags_path = book_directory + Path.DirectorySeparatorChar + "tags.json"; 102 | if (File.Exists(tags_path)) 103 | AcquireTags(tags_path); 104 | 105 | // book_metadata.json and toc.json are always present 106 | // (unless something went horribly wrong, and then we intentionally let havoc wreak) 107 | AcquireBookTitle(book_directory + Path.DirectorySeparatorChar + "book_metadata.json"); 108 | AcquireTableOfContents(book_directory + Path.DirectorySeparatorChar + "toc.json"); 109 | AcquireContent(); 110 | } 111 | 112 | private void AcquireBookTitle(string book_metadata_path) 113 | { 114 | Helper.Debug("Reading book_metadata.json"); 115 | 116 | var book_metadata_content = File.ReadAllText(book_metadata_path); 117 | if (book_metadata_content.Length < 4) 118 | throw new Exception("File too short! Path: " + book_metadata_path); 119 | 120 | JObject jObject = JObject.Parse(book_metadata_content); 121 | var title = jObject["title"]; 122 | if (0 == title.Children().Count()) 123 | { 124 | Title = (string)title; 125 | } 126 | else 127 | { 128 | Title = (string)title["#text"]; 129 | } 130 | Helper.Debug("Book title: " + Title); 131 | } 132 | 133 | private void AcquireTableOfContents(string table_of_content_path) 134 | { 135 | Helper.Debug("Reading toc.json"); 136 | 137 | var table_of_contents_content = File.ReadAllText(table_of_content_path); 138 | if (table_of_contents_content.Length < 4) 139 | throw new Exception("File too short! Path: " + table_of_content_path); 140 | 141 | TableOfContents = TableOfContentsDeserializer.Deserialize(table_of_contents_content); 142 | } 143 | 144 | private void AcquireContent() 145 | { 146 | // The number of chapters is equal to the number of entries in the table of content 147 | Chapters = new Chapter[TableOfContents.Length]; 148 | 149 | // Add chapters 150 | for (int i = 0; i < TableOfContents.Length; ++i) 151 | { 152 | Chapters[i].Type = TableOfContents[i].Type; 153 | Chapters[i].Title = TableOfContents[i].Title; 154 | Chapters[i].FilePath = TableOfContents[i].FilePath; 155 | Chapters[i].OutputFileName = "chapter" + i.ToString("D4") + ".html"; 156 | 157 | var chapter_path = Directory + 158 | Path.DirectorySeparatorChar + 159 | Chapters[i].FilePath + 160 | Path.DirectorySeparatorChar + 161 | "contents.json"; 162 | 163 | Helper.Debug("Reading chapter contents: " + Chapters[i].FilePath); 164 | 165 | var chapter_content = File.ReadAllText(chapter_path); 166 | if (chapter_content.Length < 4) 167 | throw new Exception("File too short! Path: " + Chapters[i].FilePath); 168 | 169 | using var reader = new StreamReader(File.OpenRead(chapter_path)); 170 | Chapters[i].Content = ChapterContentDeserializer.Deserialize(reader); 171 | } 172 | 173 | Helper.Debug("Chapter entries: " + Chapters.Length); 174 | } 175 | 176 | private void AcquireMetadata(string metadata_path) 177 | { 178 | Helper.Debug("Reading metadata.json"); 179 | 180 | var metadata_content = File.ReadAllText(metadata_path); 181 | if (metadata_content.Length < 4) 182 | throw new Exception("File too short! Path: " + metadata_path); 183 | 184 | Metadata = MetadataDeserializer.Deserialize(metadata_content); 185 | Helper.Debug("Metadata entries: " + Metadata.Count); 186 | } 187 | 188 | private void AcquireStyles(string styles_path) 189 | { 190 | Helper.Debug("Reading styles.json"); 191 | 192 | var styles_content = File.ReadAllText(styles_path); 193 | if (styles_content.Length < 4) 194 | throw new Exception("File too short! Path: " + styles_path); 195 | 196 | Styles = StylesDeserializer.Deserialize(styles_content); 197 | Helper.Debug("Styles entries: " + Styles.Count); 198 | } 199 | 200 | private void AcquireTags(string tags_path) 201 | { 202 | Helper.Debug("Reading tags.json"); 203 | 204 | var tags_content = File.ReadAllText(tags_path); 205 | if (tags_content.Length < 4) 206 | throw new Exception("File too short! Path: " + tags_path); 207 | 208 | Tags = TagsDeserializer.Deserialize(tags_content).Tags; 209 | 210 | Helper.Debug("Tags entries: " + Tags.Count); 211 | } 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /Scribd/BookConverter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Xml; 5 | 6 | namespace ScribdMpubToEpubConverter.Scribd 7 | { 8 | struct HtmlPageInfo 9 | { 10 | public Chapter Chapter { get; set; } 11 | public HtmlGenerator Html { get; set; } 12 | 13 | // Indicates whether we're currently populating a table 14 | public bool InTable { get; set; } 15 | 16 | // Prevent spamming of a warning about a style property with a -scribd* substring 17 | // Indicates that the warning has already been displayed for this chapter 18 | public bool WarnedUnsupportedStyle { get; set; } 19 | } 20 | 21 | class BookConverter 22 | { 23 | private Book Book { get; set; } 24 | private bool ShouldGenerateCoverPage { get; set; } 25 | private bool ShouldFixOffByOnePageReferences { get; set; } 26 | 27 | private HtmlPageInfo CurrentPage; 28 | 29 | public BookConverter( 30 | Book book, 31 | bool should_generate_cover_page, 32 | bool should_fix_off_by_one_page_references 33 | ) 34 | { 35 | Book = book; 36 | ShouldGenerateCoverPage = should_generate_cover_page; 37 | ShouldFixOffByOnePageReferences = should_fix_off_by_one_page_references; 38 | } 39 | 40 | private static bool IsCorruptImage(byte[] content) 41 | { 42 | try 43 | { 44 | using var stream = new MemoryStream(content); 45 | using var bitmap = new System.Drawing.Bitmap(stream); 46 | return false; 47 | } 48 | catch 49 | { 50 | return true; 51 | } 52 | } 53 | 54 | private bool AcquireCoverPage() 55 | { 56 | // Find the corresponding chapter 57 | // NOTE: Logically, one would assume that the cover should 58 | // be found when the chapter type is "frontmatter", but not every 59 | // book has a chapter as such. Therefore, we are going to use the first 60 | // image we find, as the cover image. 61 | for (int i = 0; i < Book.Chapters.Length; ++i) 62 | { 63 | // Find the first image 64 | foreach (var block in Book.Chapters[i].Content.Blocks) 65 | { 66 | if (block.Type != "image") 67 | continue; 68 | 69 | Helper.Debug("Adding cover page"); 70 | 71 | // Obtain image path and filename 72 | var image_path = Book.Directory + 73 | Path.DirectorySeparatorChar + 74 | Book.Chapters[i].FilePath + 75 | Path.DirectorySeparatorChar + 76 | block.Src; 77 | 78 | var image_name = Path.GetFileName(image_path); 79 | var image_content = File.ReadAllBytes(image_path); 80 | 81 | // Shouldn't happen 82 | if (IsCorruptImage(image_content)) 83 | Helper.Warning("You should redownload the book. Found corrupt image: " + image_name); 84 | 85 | Helper.Debug("Adding image as cover: " + image_name); 86 | 87 | Book.Images.Add(image_name, new HtmlImage 88 | { 89 | AbsoluteFilePath = image_path, 90 | Content = image_content, 91 | ID = "cover" 92 | }); 93 | 94 | using var string_writer = new UTF8StringWriter(); 95 | using var writer = XmlWriter.Create(string_writer, Helper.XmlWriterSettings); 96 | 97 | var title = "Cover Page"; 98 | var html = new HtmlGenerator(writer, title); 99 | html.AddImage(image_name, block.Alt ?? image_name, true); 100 | 101 | writer.Close(); 102 | 103 | // Only HTML and Title fields are populated for the cover page 104 | Book.CoverPage = new Chapter 105 | { 106 | HTML = string_writer.ToString(), 107 | Title = title 108 | }; 109 | 110 | // We found the cover, return from function 111 | return true; 112 | } 113 | } 114 | 115 | return false; 116 | } 117 | 118 | private string HandleWordContent(WordContent word_content) 119 | { 120 | var word_text = string.Empty; 121 | switch (word_content.Type) 122 | { 123 | case "composite": 124 | foreach (var word in word_content.Words) 125 | word_text += HandleWordContent(word); 126 | break; 127 | case "image": 128 | Helper.Warning( 129 | "Found image inside a word block! Ignoring..." 130 | ); 131 | break; 132 | default: 133 | word_text += word_content.Text; 134 | break; 135 | } 136 | 137 | return word_text; 138 | } 139 | 140 | // TODO: Parse arrays? 141 | private HtmlAttribute GetClassAttribute(dynamic style) 142 | { 143 | // The same logic applies, just as with metadata. 144 | // NOTE: This abuses an underlying hack in HtmlGenerator.AddText()! 145 | // Specifically, when LocalName is set to null, then the attribute won't be added. 146 | // Meaning, that the attribute will only be added if the if cases underneath succeed. 147 | HtmlAttribute class_attribute = new HtmlAttribute(); 148 | if (style != null) 149 | { 150 | var style_name = style as string; 151 | if (style_name == null) 152 | return class_attribute; 153 | 154 | // Include the style, only if it was successfully added (i.e. not empty) 155 | if (Book.Styles.ContainsKey(style_name)) 156 | { 157 | class_attribute.LocalName = "class"; 158 | class_attribute.Value = style; 159 | } 160 | } 161 | 162 | return class_attribute; 163 | } 164 | 165 | private HtmlAttribute GetStyleAttribute(BlockContent block) 166 | { 167 | // The same logic applies, just as with metadata. 168 | // NOTE: This abuses an underlying hack in HtmlGenerator.AddText()! 169 | // Specifically, when LocalName is set to null, then the attribute won't be added. 170 | // Meaning, that the attribute will only be added if the if cases underneath succeed. 171 | HtmlAttribute style_attribute = new HtmlAttribute(); 172 | 173 | if (block.Align != null) 174 | { 175 | var align_type = block.Align as string; 176 | if (align_type == null) 177 | return style_attribute; 178 | 179 | style_attribute.LocalName = "style"; 180 | style_attribute.Value = "text-align: " + align_type; 181 | } 182 | 183 | return style_attribute; 184 | } 185 | 186 | private bool GetProcessedLink(MetadataContent metadata, ref string link) 187 | { 188 | int find_chapter_from_block_index(int block_index) 189 | { 190 | for (int i = 0; i < Book.TableOfContents.Length; ++i) 191 | { 192 | var toc_info = Book.TableOfContents[i]; 193 | if (block_index >= toc_info.BlockStart && block_index < toc_info.BlockEnd) 194 | { 195 | // Fix an edge case where some books have a TOC 196 | // that links to the end of the previous chapter 197 | if (ShouldFixOffByOnePageReferences) 198 | { 199 | // Check if the block is within the chapter 200 | // If it is, then return the next chapter 201 | if (block_index + 1 >= toc_info.BlockEnd) 202 | { 203 | Helper.Information( 204 | "Fixing probable off-by-one reference from chapter " + i + " to chapter " + (i + 1) 205 | ); 206 | return i + 1; 207 | } 208 | } 209 | return i; 210 | } 211 | } 212 | 213 | return -1; 214 | } 215 | 216 | int find_block_index_from_link(string link) 217 | { 218 | try 219 | { 220 | return Book.Tags[link].BlockIndex; 221 | } 222 | catch 223 | { 224 | return -1; 225 | } 226 | } 227 | 228 | // The initial assignment is a fallback 229 | var is_external_link = (metadata.Href.StartsWith("http") || metadata.Href.StartsWith("www")); 230 | if (metadata.ExternalLink != null) 231 | is_external_link = metadata.ExternalLink.Value; 232 | 233 | Helper.Debug( 234 | "Adding an " + (is_external_link ? "external" : "internal") + " link: " + metadata.Href 235 | ); 236 | 237 | // Don't process external links 238 | if (is_external_link) 239 | { 240 | link = metadata.Href; 241 | return true; 242 | } 243 | 244 | var block_index = find_block_index_from_link(metadata.Href); 245 | if (block_index == -1) 246 | { 247 | Helper.Warning("Failed to find block index for given link: " + metadata.Href); 248 | return false; 249 | } 250 | 251 | var chapter_index = find_chapter_from_block_index(block_index); 252 | if (chapter_index == -1) 253 | { 254 | Helper.Warning("Failed to find chapter index for given block index: " + block_index); 255 | return false; 256 | } 257 | 258 | // Link to appropriate chapter 259 | link = Book.Chapters[chapter_index].OutputFileName; 260 | return true; 261 | } 262 | 263 | // TODO: Parse arrays? 264 | private HtmlAttribute GetHrefAttribute(dynamic metadata) 265 | { 266 | // NOTE: This abuses an underlying hack in HtmlGenerator.AddText()! 267 | // Specifically, when LocalName is set to null, then the attribute won't be added. 268 | // Meaning, that the attribute will only be added if the if cases underneath succeed. 269 | HtmlAttribute href_attribute = new HtmlAttribute(); 270 | 271 | // If the first word contains metadata, then the others should also. 272 | // Even if they don't, it's better to make a link out of a few more words than fewer. 273 | if (metadata != null) 274 | { 275 | var metadata_name = metadata as string; 276 | if (metadata_name == null) 277 | return href_attribute; 278 | 279 | var word_metadata = Book.Metadata[metadata_name]; 280 | if (word_metadata.Href == null) 281 | return href_attribute; 282 | 283 | // A link will be processed only if it is internal 284 | // In that case, it will be relinked to appropriate chapter 285 | // If the function fails for some reason, then no link will be added 286 | var attribute_link = string.Empty; 287 | if (!GetProcessedLink(word_metadata, ref attribute_link)) 288 | return href_attribute; 289 | 290 | href_attribute.LocalName = "href"; 291 | href_attribute.Value = attribute_link; 292 | } 293 | 294 | return href_attribute; 295 | } 296 | 297 | private string GetTextElementType(BlockContent block) 298 | { 299 | if (block.Size == null) 300 | return "p"; 301 | 302 | if ((block.Size as string) != "headline") 303 | return "p"; 304 | 305 | var header_level = 2; 306 | if (block.SizeClass != null) 307 | { 308 | const int MAX_HEADLINE_SIZE = 4; 309 | header_level = MAX_HEADLINE_SIZE - block.SizeClass.Value + 1; 310 | header_level = Math.Min(Math.Max(1, header_level), MAX_HEADLINE_SIZE); 311 | header_level += 1; 312 | } 313 | 314 | return "h" + header_level; 315 | } 316 | 317 | private void HandleText(BlockContent block) 318 | { 319 | // Don't do anything when the word count is 0. 320 | // Should not ever happen with text-type elements. 321 | if (block.WordCount == 0) 322 | return; 323 | 324 | var text = new List(); 325 | foreach (var word in block.Words) 326 | text.Add(HandleWordContent(word)); 327 | 328 | var style_attribute = GetStyleAttribute(block); 329 | var class_attribute = GetClassAttribute(block.Words[0].Style); 330 | var href_attribute = GetHrefAttribute(block.Words[0].Metadata); 331 | 332 | var text_string = string.Join(" ", text); 333 | 334 | // If we found an URL in the metadata, then we will create a link. 335 | // If not, and there are size attributes, then we will create a headline 336 | // and, finally, if there are no size attributes, then we will create a paragraph. 337 | if (href_attribute.LocalName != null) 338 | { 339 | CurrentPage.Html.AddLink( 340 | text_string, 341 | href_attribute, 342 | class_attribute, 343 | style_attribute 344 | ); 345 | } 346 | else 347 | { 348 | CurrentPage.Html.AddText( 349 | GetTextElementType(block), 350 | text_string, 351 | class_attribute, 352 | style_attribute 353 | ); 354 | } 355 | } 356 | 357 | private void HandleImage(BlockContent block) 358 | { 359 | // Obtain image path and filename 360 | var image_path = Book.Directory + 361 | Path.DirectorySeparatorChar + 362 | CurrentPage.Chapter.FilePath + 363 | Path.DirectorySeparatorChar + 364 | block.Src; 365 | 366 | var image_name = Path.GetFileName(image_path); 367 | var image_content = File.ReadAllBytes(image_path); 368 | 369 | // Shouldn't happen 370 | if (IsCorruptImage(image_content)) 371 | Helper.Warning("You should redownload the book. Found corrupt image: " + image_name); 372 | 373 | // Only add this image to our image list if it doesn't already exist 374 | // So we can later add it to the EPUB file 375 | if (!Book.Images.ContainsKey(image_name)) 376 | { 377 | Helper.Debug("Adding image: " + image_name); 378 | 379 | Book.Images.Add(image_name, new HtmlImage 380 | { 381 | AbsoluteFilePath = image_path, 382 | Content = image_content, 383 | ID = "img" + Book.Images.Count.ToString("D4") 384 | }); 385 | } 386 | 387 | // Add image element to HTML 388 | CurrentPage.Html.AddImage( 389 | image_name, 390 | block.Alt ?? image_name, 391 | block.Center ?? false 392 | ); 393 | } 394 | 395 | private void HandleRow(BlockContent block) 396 | { 397 | if (!CurrentPage.InTable) 398 | { 399 | // Don't estimate the table column width. Just average it. 400 | // Leave it to the person post-processing the book. 401 | CurrentPage.Html.StartTable(block.Cells.Length); 402 | CurrentPage.InTable = true; 403 | } 404 | 405 | var columns = new List(); 406 | 407 | // NOTE: Only text is supported inside tables 408 | // Meaning, no images, links, etc. are allowed. 409 | foreach (var cell in block.Cells) 410 | { 411 | var text = new List(); 412 | 413 | // Similar to the BlockContent type 414 | foreach (var node in cell.Nodes) 415 | { 416 | if (node.WordCount == 0 or node.Words == null) 417 | continue; 418 | 419 | foreach (var word in node.Words) 420 | text.Add(HandleWordContent(word)); 421 | } 422 | 423 | // Can occurr from time to time 424 | if (cell.Style != null) 425 | { 426 | if ((cell.Style as string).Contains("-scribd") && !CurrentPage.WarnedUnsupportedStyle) 427 | { 428 | Helper.Warning("Double-check table style attribute for an unsupported -scribd* property."); 429 | CurrentPage.WarnedUnsupportedStyle = true; 430 | } 431 | } 432 | 433 | columns.Add(new HtmlTableColumn 434 | { 435 | Text = string.Join(" ", text), 436 | Style = cell.Style 437 | }); 438 | } 439 | 440 | CurrentPage.Html.AddTableRow(columns); 441 | } 442 | 443 | private void HandleBlocks(BlockContent block) 444 | { 445 | // Upon the finding of the first non-table element 446 | // stop populating the table with rows and end it. 447 | if (CurrentPage.InTable && block.Type != "row") 448 | { 449 | CurrentPage.Html.EndTable(); 450 | CurrentPage.InTable = false; 451 | } 452 | 453 | switch (block.Type) 454 | { 455 | case "text": 456 | // Don't log when a text block is found 457 | HandleText(block); 458 | break; 459 | case "image": 460 | // Don't log when an image block is found 461 | HandleImage(block); 462 | break; 463 | case "raw": 464 | Helper.Warning("Ignoring \"raw\" block"); 465 | break; 466 | case "spacer": 467 | CurrentPage.Html.AddSpacer((block.Size as double?) ?? 1.0); 468 | break; 469 | case "page_break": 470 | CurrentPage.Html.AddPageBreak(); 471 | break; 472 | case "border": 473 | CurrentPage.Html.AddBorder((block.Width as double?) ?? 1.0, block.Style); 474 | break; 475 | case "row": 476 | HandleRow(block); 477 | break; 478 | case "hr": 479 | CurrentPage.Html.AddHorizontalRule(); 480 | break; 481 | default: 482 | // Shouldn't ever happen 483 | Helper.Warning("Found an unknown block type: " + block.Type); 484 | break; 485 | } 486 | } 487 | 488 | public void Convert() 489 | { 490 | if (ShouldGenerateCoverPage) 491 | { 492 | if (!AcquireCoverPage()) 493 | throw new Exception("An error occurred while aquiring the cover page!"); 494 | } 495 | 496 | for (int i = 0; i < Book.Chapters.Length; ++i) 497 | { 498 | using var string_writer = new UTF8StringWriter(); 499 | using var writer = XmlWriter.Create(string_writer, Helper.XmlWriterSettings); 500 | 501 | // Reset current page info 502 | CurrentPage = new HtmlPageInfo 503 | { 504 | Chapter = Book.Chapters[i], 505 | Html = new HtmlGenerator(writer, Book.Chapters[i].Title), 506 | InTable = false, 507 | WarnedUnsupportedStyle = false 508 | }; 509 | 510 | // Iterate over blocks for this chapter 511 | foreach (var block in CurrentPage.Chapter.Content.Blocks) 512 | HandleBlocks(block); 513 | 514 | // Make sure to close the table (just in case) 515 | if (CurrentPage.InTable) 516 | CurrentPage.Html.EndTable(); 517 | 518 | // Close the stream, so we can finally obtain the XML result 519 | writer.Close(); 520 | 521 | // Copy generated HTML, so we can later save it to EPUB 522 | Book.Chapters[i].HTML = string_writer.ToString(); 523 | } 524 | 525 | Helper.Debug("Finished converting MPUB blocks to HTML"); 526 | } 527 | } 528 | } 529 | -------------------------------------------------------------------------------- /Scribd/ChapterContentDeserializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ScribdMpubToEpubConverter.Scribd 4 | { 5 | #nullable enable 6 | /* 7 | * The following properties are not deserialized: 8 | * 9 | * [JsonProperty("break_map")] 10 | * public BreakMapContent? BreakMap { get; set; } 11 | * 12 | * [JsonProperty("font")] 13 | * public string? Font { get; set; } 14 | */ 15 | struct WordContent 16 | { 17 | // break_map not included 18 | [JsonProperty("id")] 19 | public int? ID { get; set; } 20 | 21 | [JsonProperty("metadata")] 22 | public string? Metadata { get; set; } 23 | 24 | // Incompetency at it's finest. When you include an image in a Words[] array element. 25 | // The use of dynamic here is _absolutely required_. 26 | [JsonProperty("style")] 27 | public dynamic? Style { get; set; } 28 | 29 | [JsonProperty("text")] 30 | public string? Text { get; set; } 31 | 32 | // i.e. simple, composite, image 33 | [JsonProperty("type")] 34 | public string? Type { get; set; } 35 | 36 | // This element can be recursive, when the type is set to composite 37 | [JsonProperty("words")] 38 | public WordContent[]? Words { get; set; } 39 | } 40 | 41 | /* 42 | * The following property is not deserialized: 43 | * 44 | * [JsonProperty("word_count")] 45 | * public int? WordCount { get; set; } 46 | */ 47 | struct CellContent 48 | { 49 | [JsonProperty("nodes")] 50 | public BlockContent[]? Nodes { get; set; } 51 | 52 | [JsonProperty("style")] 53 | public dynamic Style { get; set; } 54 | } 55 | 56 | /* 57 | * The following properties are not deserialized: 58 | * 59 | * [JsonProperty("color")] 60 | * public string? Color { get; set; } 61 | * 62 | * [JsonProperty("orientation")] 63 | * public int Orientation { get; set; } 64 | * 65 | * [JsonProperty("rgb_color")] 66 | * public int[]? RgbColor { get; set; } 67 | * 68 | * [JsonProperty("height")] 69 | * public int? Height { get; set; } 70 | * 71 | * [JsonProperty("right_indent")] 72 | * public float? RightIndent { get; set; } 73 | * 74 | * [JsonProperty("block_indent")] 75 | * public float? BlockIndent { get; set; } 76 | */ 77 | struct BlockContent 78 | { 79 | [JsonProperty("align")] 80 | public string? Align { get; set; } 81 | 82 | [JsonProperty("alt")] 83 | public string? Alt { get; set; } 84 | 85 | // Indicates that this is a table row 86 | [JsonProperty("cells")] 87 | public CellContent[]? Cells { get; set; } 88 | 89 | // Only images have this property 90 | [JsonProperty("center")] 91 | public bool? Center { get; set; } 92 | 93 | // When type is equal to "text", then size is a string. 94 | // Otherwise, it's an int. 95 | [JsonProperty("size")] 96 | public dynamic? Size { get; set; } 97 | 98 | [JsonProperty("size_class")] 99 | public int? SizeClass { get; set; } 100 | 101 | [JsonProperty("src")] 102 | public string? Src { get; set; } 103 | 104 | // Usually the type is Dictionary 105 | // Unless the type is i.e. border, then the type is just string 106 | [JsonProperty("style")] 107 | public dynamic? Style { get; set; } 108 | 109 | [JsonProperty("type")] 110 | public string Type { get; set; } 111 | 112 | // Should be int 113 | [JsonProperty("width")] 114 | public dynamic? Width { get; set; } 115 | 116 | [JsonProperty("word_count")] 117 | public int WordCount { get; set; } 118 | 119 | [JsonProperty("words")] 120 | public WordContent[]? Words { get; set; } 121 | } 122 | 123 | struct ChapterContent 124 | { 125 | [JsonProperty("blocks")] 126 | public BlockContent[]? Blocks { get; set; } 127 | 128 | [JsonProperty("title")] 129 | public string Title { get; set; } 130 | } 131 | #nullable restore 132 | 133 | class ChapterContentDeserializer 134 | { 135 | // Read larger files in fragments 136 | public static ChapterContent Deserialize(System.IO.StreamReader reader) 137 | { 138 | using var json_reader = new JsonTextReader(reader); 139 | var serializer = new JsonSerializer(); 140 | try 141 | { 142 | return serializer.Deserialize(json_reader); 143 | } 144 | catch 145 | { 146 | // They HAD to mess up something so simple! 147 | return new ChapterContent 148 | { 149 | Blocks = serializer.Deserialize(json_reader), 150 | Title = System.IO.Path.GetRandomFileName().Replace(".", "") 151 | }; 152 | } 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /Scribd/Decryptor.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Security.Cryptography; 4 | 5 | namespace ScribdMpubToEpubConverter.Scribd 6 | { 7 | // NOTE: AES-ECB does not require an IV. 8 | class Decryptor 9 | { 10 | private static string[] ProbablyEncryptedFiles = 11 | { 12 | "contents.json", ".jpg", ".jpeg", ".png", ".gif", ".svg" 13 | }; 14 | 15 | public const int KEY_SIZE = 32; 16 | 17 | public byte[] Key { get; set; } 18 | 19 | // Check if file is named simply "content" 20 | // If it is not, then the file is probably not encoded 21 | // If it is, then check if the first line starts with %PDF 22 | // If it does not, then the file is encoded, otherwise it's not encoded 23 | private bool IsFileEncoded(string file_path) 24 | { 25 | if (!file_path.EndsWith("content")) 26 | return false; 27 | 28 | using var stream_reader = new StreamReader(File.OpenRead(file_path)); 29 | return !stream_reader.ReadLine().StartsWith("%PDF"); 30 | } 31 | 32 | private bool IsFileEncrypted(string file_path) 33 | { 34 | // Check if a file is encrypted based on it's name/extension 35 | foreach (var file in ProbablyEncryptedFiles) 36 | { 37 | if (file_path.EndsWith(file)) 38 | return true; 39 | } 40 | 41 | return false; 42 | } 43 | 44 | public void Decrypt(string input_directory_path) 45 | { 46 | var file_list = Directory.GetFiles(input_directory_path); 47 | foreach (var file_path in file_list) 48 | { 49 | // Decode/Decrypt as needed 50 | if (IsFileEncoded(file_path)) 51 | { 52 | DecodeFile(file_path); 53 | } 54 | else if (IsFileEncrypted(file_path)) 55 | { 56 | DecryptFile(file_path); 57 | } 58 | } 59 | 60 | var subdirectory_list = Directory.GetDirectories(input_directory_path); 61 | foreach (var subdirectory in subdirectory_list) 62 | Decrypt(subdirectory); 63 | } 64 | 65 | public void GeneratePrivateKey(string input_string) 66 | { 67 | var input_length = input_string.Length; 68 | 69 | Key = new byte[input_length / 2]; 70 | for (var i = 0; i < input_length; i += 2) 71 | { 72 | var var4 = i / 2; 73 | 74 | Key[var4] = (byte)( 75 | (Convert.ToInt32(input_string[i].ToString(), 16) << 4) + 76 | Convert.ToInt32(input_string[i + 1].ToString(), 16) 77 | ); 78 | 79 | var var6 = (i == 0) ? 31 : Key[var4 - 1]; 80 | Key[var4] = (byte)(Key[var4] ^ var6); 81 | } 82 | 83 | Helper.Debug("Generated private key for entry " + input_string); 84 | } 85 | 86 | private void DecryptFile(string input_file_path) 87 | { 88 | Helper.Debug("Decrypting file: " + input_file_path); 89 | 90 | try 91 | { 92 | using var aes = new RijndaelManaged 93 | { 94 | // Set appropriate key size in bits 95 | KeySize = KEY_SIZE * 8, 96 | 97 | // The correct mode is AES-ECB PKCS5, 98 | // but PKCS7 and PKCS5 backwards-compatible 99 | Mode = CipherMode.ECB, 100 | Padding = PaddingMode.PKCS7, 101 | 102 | // Set key data 103 | Key = Key 104 | }; 105 | 106 | // Get encrypted file content 107 | var encrypted_content = File.ReadAllBytes(input_file_path); 108 | 109 | var decryptor = aes.CreateDecryptor(); 110 | var decrypted_data = decryptor.TransformFinalBlock( 111 | encrypted_content, 0, 112 | encrypted_content.Length 113 | ); 114 | 115 | File.WriteAllBytes(input_file_path, decrypted_data); 116 | } 117 | catch 118 | { 119 | // Most likely the document wasn't encrypted and the 120 | // AES decryptor threw an exception. Just handle it quietly. 121 | Helper.Debug("Skipping decryption, as the file is probably already decrypted"); 122 | } 123 | } 124 | 125 | private void DecodeFile(string input_file_path) 126 | { 127 | Helper.Debug("Decoding file: " + input_file_path); 128 | 129 | // Get encoded file content 130 | var encoded_content = File.ReadAllBytes(input_file_path); 131 | for (int i = 0; i < encoded_content.Length; ++i) 132 | encoded_content[i] ^= Key[i % Key.Length]; 133 | 134 | File.WriteAllBytes(input_file_path + ".pdf", encoded_content); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /Scribd/MetadataDeserializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Linq; 3 | using System.Collections.Generic; 4 | 5 | namespace ScribdMpubToEpubConverter.Scribd 6 | { 7 | // We can use the dynamic keyword, but I find it a matter of preference. 8 | #nullable enable 9 | struct MetadataContent 10 | { 11 | [JsonProperty("color")] 12 | public int[]? Color { get; set; } 13 | 14 | [JsonProperty("external_link")] 15 | public bool? ExternalLink { get; set; } 16 | 17 | [JsonProperty("href")] 18 | public string? Href { get; set; } 19 | 20 | [JsonProperty("superscript")] 21 | public int? Superscript { get; set; } 22 | 23 | [JsonProperty("underline")] 24 | public bool? Underline { get; set; } 25 | 26 | [JsonProperty("overline")] 27 | public bool? Overline { get; set; } 28 | } 29 | #nullable restore 30 | 31 | class MetadataDeserializer 32 | { 33 | public static Dictionary Deserialize(string content) 34 | { 35 | var json = JObject.Parse(content); 36 | 37 | var results = new Dictionary(); 38 | foreach (var metadata_entry in json["metadata"].Children()) 39 | { 40 | var entry_name = metadata_entry.Value(); 41 | 42 | results.Add( 43 | entry_name, 44 | JsonConvert.DeserializeObject( 45 | json[entry_name].ToString() 46 | ) 47 | ); 48 | } 49 | 50 | return results; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Scribd/StylesDeserializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System.Collections.Generic; 3 | 4 | namespace ScribdMpubToEpubConverter.Scribd 5 | { 6 | class StylesDeserializer 7 | { 8 | public static Dictionary Deserialize(string content) 9 | { 10 | var json = JObject.Parse(content); 11 | 12 | var results = new Dictionary(); 13 | foreach (var metadata_entry in json["styles"].Children()) 14 | { 15 | var entry_name = metadata_entry.Value(); 16 | var entry_value = json[entry_name].Value().Trim(); 17 | 18 | // Don't add empty styles 19 | if (entry_value.Length == 0) 20 | { 21 | Helper.Information("Skipping empty style entry: " + entry_name); 22 | continue; 23 | } 24 | 25 | results.Add(entry_name, entry_value); 26 | } 27 | 28 | return results; 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /Scribd/TableOfContentsDeserializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace ScribdMpubToEpubConverter.Scribd 4 | { 5 | struct AdditionalChapterInformation 6 | { 7 | [JsonProperty("title")] 8 | public string Title { get; set; } 9 | 10 | [JsonProperty("filepath")] 11 | public string FilePath { get; set; } 12 | 13 | [JsonProperty("type")] 14 | public string Type { get; set; } 15 | 16 | [JsonProperty("block_start")] 17 | public int BlockStart { get; set; } 18 | 19 | [JsonProperty("block_end")] 20 | public int BlockEnd { get; set; } 21 | } 22 | 23 | class TableOfContentsDeserializer 24 | { 25 | public static AdditionalChapterInformation[] Deserialize(string content) 26 | { 27 | return JsonConvert.DeserializeObject(content); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Scribd/TagsDeserializer.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System.Collections.Generic; 3 | 4 | namespace ScribdMpubToEpubConverter.Scribd 5 | { 6 | struct TagContent 7 | { 8 | [JsonProperty("block_idx")] 9 | public int BlockIndex { get; set; } 10 | } 11 | 12 | struct TagsContent 13 | { 14 | [JsonProperty("tags")] 15 | public Dictionary Tags { get; set; } 16 | } 17 | 18 | class TagsDeserializer 19 | { 20 | public static TagsContent Deserialize(string content) 21 | { 22 | return JsonConvert.DeserializeObject(content); 23 | } 24 | } 25 | } -------------------------------------------------------------------------------- /ScribdMpubToEpubConverter.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | Debug 6 | AnyCPU 7 | {7E35AC13-76D7-4B7A-BF03-099FDF4ADE28} 8 | WinExe 9 | ScribdMpubToEpubConverter 10 | ScribdMpubToEpubConverter 11 | v4.7.2 12 | preview 13 | 512 14 | true 15 | true 16 | publish\ 17 | true 18 | Disk 19 | false 20 | Foreground 21 | 7 22 | Days 23 | false 24 | false 25 | true 26 | 0 27 | 1.0.0.%2a 28 | false 29 | false 30 | true 31 | 32 | 33 | AnyCPU 34 | true 35 | full 36 | false 37 | bin\Debug\ 38 | DEBUG;TRACE 39 | prompt 40 | 4 41 | 42 | 43 | AnyCPU 44 | embedded 45 | true 46 | bin\Release\ 47 | TRACE 48 | prompt 49 | 4 50 | true 51 | 52 | 53 | ScribdMpubToEpubConverter.Program 54 | 55 | 56 | scribd.ico 57 | 58 | 59 | true 60 | 61 | 62 | 63 | packages\Newtonsoft.Json.12.0.3\lib\net45\Newtonsoft.Json.dll 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | Form 89 | 90 | 91 | MainForm.cs 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | MainForm.cs 106 | 107 | 108 | ResXFileCodeGenerator 109 | Resources.Designer.cs 110 | Designer 111 | 112 | 113 | True 114 | Resources.resx 115 | 116 | 117 | 118 | SettingsSingleFileGenerator 119 | Settings.Designer.cs 120 | 121 | 122 | True 123 | Settings.settings 124 | True 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | False 136 | Microsoft .NET Framework 4.7.2 %28x86 and x64%29 137 | true 138 | 139 | 140 | False 141 | .NET Framework 3.5 SP1 142 | false 143 | 144 | 145 | 146 | 147 | -------------------------------------------------------------------------------- /ScribdMpubToEpubConverter.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29503.13 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ScribdMpubToEpubConverter", "ScribdMpubToEpubConverter.csproj", "{7E35AC13-76D7-4B7A-BF03-099FDF4ADE28}" 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 | {7E35AC13-76D7-4B7A-BF03-099FDF4ADE28}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 15 | {7E35AC13-76D7-4B7A-BF03-099FDF4ADE28}.Debug|Any CPU.Build.0 = Debug|Any CPU 16 | {7E35AC13-76D7-4B7A-BF03-099FDF4ADE28}.Release|Any CPU.ActiveCfg = Release|Any CPU 17 | {7E35AC13-76D7-4B7A-BF03-099FDF4ADE28}.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 = {223A935A-B97E-4C80-95D2-BD79D9B7AAE0} 24 | EndGlobalSection 25 | EndGlobal 26 | -------------------------------------------------------------------------------- /ZipStorer.cs: -------------------------------------------------------------------------------- 1 | // ZipStorer, by Jaime Olivares 2 | // Website: http://github.com/jaime-olivares/zipstorer 3 | // NOTE: Modified slightly to support mimetype without ZIP extra fields. 4 | #define NOASYNC 5 | 6 | using System.Collections.Generic; 7 | using System.Text; 8 | 9 | #if !NOASYNC 10 | using System.Threading.Tasks; 11 | #endif 12 | 13 | namespace System.IO.Compression 14 | { 15 | /// 16 | /// Unique class for compression/decompression file. Represents a Zip file. 17 | /// 18 | public class ZipStorer : IDisposable 19 | { 20 | /// 21 | /// Compression method enumeration 22 | /// 23 | public enum Compression : ushort 24 | { 25 | /// Uncompressed storage 26 | Store = 0, 27 | /// Deflate compression method 28 | Deflate = 8 29 | } 30 | 31 | /// 32 | /// Represents an entry in Zip file directory 33 | /// 34 | public class ZipFileEntry 35 | { 36 | /// Compression method 37 | public Compression Method; 38 | /// Full path and filename as stored in Zip 39 | public string FilenameInZip; 40 | /// Original file size 41 | public long FileSize; 42 | /// Compressed file size 43 | public long CompressedSize; 44 | /// Offset of header information inside Zip storage 45 | public long HeaderOffset; 46 | /// Offset of file inside Zip storage 47 | public long FileOffset; 48 | /// Size of header information 49 | public uint HeaderSize; 50 | /// 32-bit checksum of entire file 51 | public uint Crc32; 52 | /// Last modification time of file 53 | public DateTime ModifyTime; 54 | /// Creation time of file 55 | public DateTime CreationTime; 56 | /// Last access time of file 57 | public DateTime AccessTime; 58 | /// User comment for file 59 | public string Comment; 60 | /// True if UTF8 encoding for filename and comments, false if default (CP 437) 61 | public bool EncodeUTF8; 62 | 63 | /// Overriden method 64 | /// Filename in Zip 65 | public override string ToString() 66 | { 67 | return this.FilenameInZip; 68 | } 69 | } 70 | 71 | #region Public fields 72 | /// True if UTF8 encoding for filename and comments, false if default (CP 437) 73 | public bool EncodeUTF8 = false; 74 | /// Force deflate algotithm even if it inflates the stored file. Off by default. 75 | public bool ForceDeflating = false; 76 | #endregion 77 | 78 | #region Private fields 79 | // List of files to store 80 | private List Files = new List(); 81 | // Filename of storage file 82 | private string FileName; 83 | // Stream object of storage file 84 | private Stream ZipFileStream; 85 | // General comment 86 | private string Comment = string.Empty; 87 | // Central dir image 88 | private byte[] CentralDirImage = null; 89 | // Existing files in zip 90 | private long ExistingFiles = 0; 91 | // File access for Open method 92 | private FileAccess Access; 93 | // leave the stream open after the ZipStorer object is disposed 94 | private bool leaveOpen; 95 | // Static CRC32 Table 96 | private static UInt32[] CrcTable = null; 97 | // Default filename encoder 98 | private static Encoding DefaultEncoding = Encoding.GetEncoding(437); 99 | #endregion 100 | 101 | #region Public methods 102 | // Static constructor. Just invoked once in order to create the CRC32 lookup table. 103 | static ZipStorer() 104 | { 105 | // Generate CRC32 table 106 | CrcTable = new UInt32[256]; 107 | for (int i = 0; i < CrcTable.Length; i++) 108 | { 109 | UInt32 c = (UInt32)i; 110 | for (int j = 0; j < 8; j++) 111 | { 112 | if ((c & 1) != 0) 113 | c = 3988292384 ^ (c >> 1); 114 | else 115 | c >>= 1; 116 | } 117 | CrcTable[i] = c; 118 | } 119 | } 120 | 121 | /// 122 | /// Method to create a new storage file 123 | /// 124 | /// Full path of Zip file to create 125 | /// General comment for Zip file 126 | /// A valid ZipStorer object 127 | public static ZipStorer Create(string _filename, string _comment = null) 128 | { 129 | Stream stream = new FileStream(_filename, FileMode.Create, FileAccess.ReadWrite); 130 | 131 | ZipStorer zip = Create(stream, _comment); 132 | zip.Comment = _comment ?? string.Empty; 133 | zip.FileName = _filename; 134 | 135 | return zip; 136 | } 137 | 138 | /// 139 | /// Method to create a new zip storage in a stream 140 | /// 141 | /// 142 | /// 143 | /// true to leave the stream open after the ZipStorer object is disposed; otherwise, false (default). 144 | /// A valid ZipStorer object 145 | public static ZipStorer Create(Stream _stream, string _comment = null, bool _leaveOpen = false) 146 | { 147 | ZipStorer zip = new ZipStorer() 148 | { 149 | Comment = _comment ?? string.Empty, 150 | ZipFileStream = _stream, 151 | Access = FileAccess.Write, 152 | leaveOpen = _leaveOpen 153 | }; 154 | 155 | return zip; 156 | } 157 | 158 | /// 159 | /// Method to open an existing storage file 160 | /// 161 | /// Full path of Zip file to open 162 | /// File access mode as used in FileStream constructor 163 | /// A valid ZipStorer object 164 | public static ZipStorer Open(string _filename, FileAccess _access) 165 | { 166 | Stream stream = (Stream)new FileStream(_filename, FileMode.Open, _access == FileAccess.Read ? FileAccess.Read : FileAccess.ReadWrite); 167 | 168 | ZipStorer zip = Open(stream, _access); 169 | zip.FileName = _filename; 170 | 171 | return zip; 172 | } 173 | 174 | /// 175 | /// Method to open an existing storage from stream 176 | /// 177 | /// Already opened stream with zip contents 178 | /// File access mode for stream operations 179 | /// true to leave the stream open after the ZipStorer object is disposed; otherwise, false (default). 180 | /// A valid ZipStorer object 181 | public static ZipStorer Open(Stream _stream, FileAccess _access, bool _leaveOpen = false) 182 | { 183 | if (!_stream.CanSeek && _access != FileAccess.Read) 184 | throw new InvalidOperationException("Stream cannot seek"); 185 | 186 | ZipStorer zip = new ZipStorer() 187 | { 188 | ZipFileStream = _stream, 189 | Access = _access, 190 | leaveOpen = _leaveOpen 191 | }; 192 | 193 | if (zip.ReadFileInfo()) 194 | return zip; 195 | 196 | if (!_leaveOpen) 197 | zip.Close(); 198 | 199 | throw new System.IO.InvalidDataException(); 200 | } 201 | 202 | /// 203 | /// Add full contents of a file into the Zip storage 204 | /// 205 | /// Compression method 206 | /// Full path of file to add to Zip storage 207 | /// Filename and path as desired in Zip directory 208 | /// Comment for stored file 209 | public ZipFileEntry AddFile(Compression _method, string _pathname, string _filenameInZip, string _comment = null) 210 | { 211 | if (Access == FileAccess.Read) 212 | throw new InvalidOperationException("Writing is not alowed"); 213 | 214 | using (var stream = new FileStream(_pathname, FileMode.Open, FileAccess.Read)) 215 | { 216 | return this.AddStream(_method, _filenameInZip, stream, File.GetLastWriteTime(_pathname), _comment); 217 | } 218 | } 219 | 220 | /// 221 | /// Add full contents of a stream into the Zip storage 222 | /// 223 | /// Same parameters and return value as AddStreamAsync() 224 | public ZipFileEntry AddStream(Compression _method, string _filenameInZip, Stream _source, DateTime _modTime, string _comment = null) 225 | { 226 | #if NOASYNC 227 | return this.AddStreamAsync(_method, _filenameInZip, _source, _modTime, _comment); 228 | #else 229 | return Task.Run(() => this.AddStreamAsync(_method, _filenameInZip, _source, _modTime, _comment)).Result; 230 | #endif 231 | } 232 | 233 | /// 234 | /// Add full contents of a stream into the Zip storage 235 | /// 236 | /// Compression method 237 | /// Filename and path as desired in Zip directory 238 | /// Stream object containing the data to store in Zip 239 | /// Modification time of the data to store 240 | /// Comment for stored file 241 | #if NOASYNC 242 | private ZipFileEntry 243 | #else 244 | public async Task 245 | #endif 246 | AddStreamAsync(Compression _method, string _filenameInZip, Stream _source, DateTime _modTime, string _comment = null) 247 | { 248 | if (Access == FileAccess.Read) 249 | throw new InvalidOperationException("Writing is not alowed"); 250 | 251 | // Prepare the fileinfo 252 | ZipFileEntry zfe = new ZipFileEntry() 253 | { 254 | Method = _method, 255 | EncodeUTF8 = this.EncodeUTF8, 256 | FilenameInZip = NormalizedFilename(_filenameInZip), 257 | Comment = _comment ?? string.Empty, 258 | Crc32 = 0, // to be updated later 259 | HeaderOffset = (uint)this.ZipFileStream.Position, // offset within file of the start of this local record 260 | CreationTime = _modTime, 261 | ModifyTime = _modTime, 262 | AccessTime = _modTime 263 | }; 264 | 265 | // Write local header 266 | this.WriteLocalHeader(zfe); 267 | zfe.FileOffset = (uint)this.ZipFileStream.Position; 268 | 269 | // Write file to zip (store) 270 | #if NOASYNC 271 | Store(zfe, _source); 272 | #else 273 | await Store(zfe, _source); 274 | #endif 275 | 276 | _source.Close(); 277 | this.UpdateCrcAndSizes(zfe); 278 | Files.Add(zfe); 279 | 280 | return zfe; 281 | } 282 | 283 | /// 284 | /// Add full contents of a directory into the Zip storage 285 | /// 286 | /// Compression method 287 | /// Full path of directory to add to Zip storage 288 | /// Path name as desired in Zip directory 289 | /// Comment for stored directory 290 | public void AddDirectory(Compression _method, string _pathname, string _pathnameInZip, string _comment = null) 291 | { 292 | if (Access == FileAccess.Read) 293 | throw new InvalidOperationException("Writing is not allowed"); 294 | 295 | string foldername; 296 | int pos = _pathname.LastIndexOf(Path.DirectorySeparatorChar); 297 | string separator = Path.DirectorySeparatorChar.ToString(); 298 | 299 | if (pos >= 0) 300 | foldername = _pathname.Remove(0, pos + 1); 301 | else 302 | foldername = _pathname; 303 | 304 | if (!string.IsNullOrEmpty(_pathnameInZip)) 305 | foldername = _pathnameInZip + foldername; 306 | 307 | if (!foldername.EndsWith(separator, StringComparison.CurrentCulture)) 308 | foldername = foldername + separator; 309 | 310 | // this.AddStream(_method, foldername, null, File.GetLastWriteTime(_pathname), _comment); 311 | 312 | // Process the list of files found in the directory. 313 | string[] fileEntries = Directory.GetFiles(_pathname); 314 | 315 | foreach (string fileName in fileEntries) 316 | this.AddFile(_method, fileName, foldername + Path.GetFileName(fileName), ""); 317 | 318 | // Recurse into subdirectories of this directory. 319 | string[] subdirectoryEntries = Directory.GetDirectories(_pathname); 320 | 321 | foreach (string subdirectory in subdirectoryEntries) 322 | this.AddDirectory(_method, subdirectory, foldername, ""); 323 | } 324 | 325 | /// 326 | /// Updates central directory (if pertinent) and close the Zip storage 327 | /// 328 | /// This is a required step, unless automatic dispose is used 329 | public void Close() 330 | { 331 | if (this.Access != FileAccess.Read) 332 | { 333 | uint centralOffset = (uint)this.ZipFileStream.Position; 334 | uint centralSize = 0; 335 | 336 | if (this.CentralDirImage != null) 337 | this.ZipFileStream.Write(CentralDirImage, 0, CentralDirImage.Length); 338 | 339 | for (int i = 0; i < Files.Count; i++) 340 | { 341 | long pos = this.ZipFileStream.Position; 342 | this.WriteCentralDirRecord(Files[i]); 343 | centralSize += (uint)(this.ZipFileStream.Position - pos); 344 | } 345 | 346 | if (this.CentralDirImage != null) 347 | this.WriteEndRecord(centralSize + (uint)CentralDirImage.Length, centralOffset); 348 | else 349 | this.WriteEndRecord(centralSize, centralOffset); 350 | } 351 | 352 | if (this.ZipFileStream != null && !this.leaveOpen) 353 | { 354 | this.ZipFileStream.Flush(); 355 | this.ZipFileStream.Dispose(); 356 | this.ZipFileStream = null; 357 | } 358 | } 359 | 360 | /// 361 | /// Read all the file records in the central directory 362 | /// 363 | /// List of all entries in directory 364 | public List ReadCentralDir() 365 | { 366 | if (this.CentralDirImage == null) 367 | throw new InvalidOperationException("Central directory currently does not exist"); 368 | 369 | List result = new List(); 370 | 371 | for (int pointer = 0; pointer < this.CentralDirImage.Length;) 372 | { 373 | uint signature = BitConverter.ToUInt32(CentralDirImage, pointer); 374 | if (signature != 0x02014b50) 375 | break; 376 | 377 | bool encodeUTF8 = (BitConverter.ToUInt16(CentralDirImage, pointer + 8) & 0x0800) != 0; 378 | ushort method = BitConverter.ToUInt16(CentralDirImage, pointer + 10); 379 | uint modifyTime = BitConverter.ToUInt32(CentralDirImage, pointer + 12); 380 | uint crc32 = BitConverter.ToUInt32(CentralDirImage, pointer + 16); 381 | long comprSize = BitConverter.ToUInt32(CentralDirImage, pointer + 20); 382 | long fileSize = BitConverter.ToUInt32(CentralDirImage, pointer + 24); 383 | ushort filenameSize = BitConverter.ToUInt16(CentralDirImage, pointer + 28); 384 | ushort extraSize = BitConverter.ToUInt16(CentralDirImage, pointer + 30); 385 | ushort commentSize = BitConverter.ToUInt16(CentralDirImage, pointer + 32); 386 | uint headerOffset = BitConverter.ToUInt32(CentralDirImage, pointer + 42); 387 | uint headerSize = (uint)(46 + filenameSize + extraSize + commentSize); 388 | DateTime modifyTimeDT = DosTimeToDateTime(modifyTime) ?? DateTime.Now; 389 | 390 | Encoding encoder = encodeUTF8 ? Encoding.UTF8 : DefaultEncoding; 391 | 392 | ZipFileEntry zfe = new ZipFileEntry() 393 | { 394 | Method = (Compression)method, 395 | FilenameInZip = encoder.GetString(CentralDirImage, pointer + 46, filenameSize), 396 | FileOffset = GetFileOffset(headerOffset), 397 | FileSize = fileSize, 398 | CompressedSize = comprSize, 399 | HeaderOffset = headerOffset, 400 | HeaderSize = headerSize, 401 | Crc32 = crc32, 402 | ModifyTime = modifyTimeDT, 403 | CreationTime = modifyTimeDT, 404 | AccessTime = DateTime.Now, 405 | }; 406 | 407 | if (commentSize > 0) 408 | zfe.Comment = encoder.GetString(CentralDirImage, pointer + 46 + filenameSize + extraSize, commentSize); 409 | 410 | if (extraSize > 0) 411 | { 412 | this.ReadExtraInfo(CentralDirImage, pointer + 46 + filenameSize, zfe); 413 | } 414 | 415 | result.Add(zfe); 416 | pointer += (46 + filenameSize + extraSize + commentSize); 417 | } 418 | 419 | return result; 420 | } 421 | 422 | /// 423 | /// Copy the contents of a stored file into a physical file 424 | /// 425 | /// Entry information of file to extract 426 | /// Name of file to store uncompressed data 427 | /// True if success, false if not. 428 | /// Unique compression methods are Store and Deflate 429 | public bool ExtractFile(ZipFileEntry _zfe, string _filename) 430 | { 431 | // Make sure the parent directory exist 432 | string path = Path.GetDirectoryName(_filename); 433 | 434 | if (!Directory.Exists(path)) 435 | Directory.CreateDirectory(path); 436 | 437 | // Check if it is a directory. If so, do nothing. 438 | if (Directory.Exists(_filename)) 439 | return true; 440 | 441 | bool result; 442 | using (var output = new FileStream(_filename, FileMode.Create, FileAccess.Write)) 443 | { 444 | result = this.ExtractFile(_zfe, output); 445 | } 446 | 447 | if (result) 448 | { 449 | File.SetCreationTime(_filename, _zfe.CreationTime); 450 | File.SetLastWriteTime(_filename, _zfe.ModifyTime); 451 | File.SetLastAccessTime(_filename, _zfe.AccessTime); 452 | } 453 | 454 | return result; 455 | } 456 | 457 | /// 458 | /// Copy the contents of a stored file into an opened stream 459 | /// 460 | /// Same parameters and return value as ExtractFileAsync 461 | public bool ExtractFile(ZipFileEntry _zfe, Stream _stream) 462 | { 463 | #if NOASYNC 464 | return this.ExtractFileAsync(_zfe, _stream); 465 | #else 466 | return Task.Run(() => ExtractFileAsync(_zfe, _stream)).Result; 467 | #endif 468 | } 469 | 470 | /// 471 | /// Copy the contents of a stored file into an opened stream 472 | /// 473 | /// Entry information of file to extract 474 | /// Stream to store the uncompressed data 475 | /// True if success, false if not. 476 | /// Unique compression methods are Store and Deflate 477 | #if NOASYNC 478 | private bool 479 | #else 480 | public async Task 481 | #endif 482 | ExtractFileAsync(ZipFileEntry _zfe, Stream _stream) 483 | { 484 | if (!_stream.CanWrite) 485 | throw new InvalidOperationException("Stream cannot be written"); 486 | 487 | // check signature 488 | byte[] signature = new byte[4]; 489 | this.ZipFileStream.Seek(_zfe.HeaderOffset, SeekOrigin.Begin); 490 | 491 | #if NOASYNC 492 | this.ZipFileStream.Read(signature, 0, 4); 493 | #else 494 | await this.ZipFileStream.ReadAsync(signature, 0, 4); 495 | #endif 496 | 497 | if (BitConverter.ToUInt32(signature, 0) != 0x04034b50) 498 | return false; 499 | 500 | // Select input stream for inflating or just reading 501 | Stream inStream; 502 | 503 | if (_zfe.Method == Compression.Store) 504 | inStream = this.ZipFileStream; 505 | else if (_zfe.Method == Compression.Deflate) 506 | inStream = new DeflateStream(this.ZipFileStream, CompressionMode.Decompress, true); 507 | else 508 | return false; 509 | 510 | // Buffered copy 511 | byte[] buffer = new byte[65535]; 512 | this.ZipFileStream.Seek(_zfe.FileOffset, SeekOrigin.Begin); 513 | long bytesPending = _zfe.FileSize; 514 | 515 | while (bytesPending > 0) 516 | { 517 | #if NOASYNC 518 | int bytesRead = inStream.Read(buffer, 0, (int)Math.Min(bytesPending, buffer.Length)); 519 | _stream.Write(buffer, 0, bytesRead); 520 | #else 521 | int bytesRead = await inStream.ReadAsync(buffer, 0, (int)Math.Min(bytesPending, buffer.Length)); 522 | await _stream.WriteAsync(buffer, 0, bytesRead); 523 | #endif 524 | 525 | bytesPending -= (uint)bytesRead; 526 | } 527 | _stream.Flush(); 528 | 529 | if (_zfe.Method == Compression.Deflate) 530 | inStream.Dispose(); 531 | 532 | return true; 533 | } 534 | 535 | /// 536 | /// Copy the contents of a stored file into a byte array 537 | /// 538 | /// Entry information of file to extract 539 | /// Byte array with uncompressed data 540 | /// True if success, false if not. 541 | /// Unique compression methods are Store and Deflate 542 | public bool ExtractFile(ZipFileEntry _zfe, out byte[] _file) 543 | { 544 | using (MemoryStream ms = new MemoryStream()) 545 | { 546 | if (ExtractFile(_zfe, ms)) 547 | { 548 | _file = ms.ToArray(); 549 | return true; 550 | } 551 | else 552 | { 553 | _file = null; 554 | return false; 555 | } 556 | } 557 | } 558 | 559 | /// 560 | /// Removes one of many files in storage. It creates a new Zip file. 561 | /// 562 | /// Reference to the current Zip object 563 | /// List of Entries to remove from storage 564 | /// True if success, false if not 565 | /// This method only works for storage of type FileStream 566 | public static bool RemoveEntries(ref ZipStorer _zip, List _zfes) 567 | { 568 | if (!(_zip.ZipFileStream is FileStream)) 569 | throw new InvalidOperationException("RemoveEntries is allowed just over streams of type FileStream"); 570 | 571 | //Get full list of entries 572 | var fullList = _zip.ReadCentralDir(); 573 | 574 | //In order to delete we need to create a copy of the zip file excluding the selected items 575 | var tempZipName = Path.GetTempFileName(); 576 | var tempEntryName = Path.GetTempFileName(); 577 | 578 | try 579 | { 580 | var tempZip = ZipStorer.Create(tempZipName, string.Empty); 581 | 582 | foreach (ZipFileEntry zfe in fullList) 583 | { 584 | if (!_zfes.Contains(zfe)) 585 | { 586 | if (_zip.ExtractFile(zfe, tempEntryName)) 587 | { 588 | tempZip.AddFile(zfe.Method, tempEntryName, zfe.FilenameInZip, zfe.Comment); 589 | } 590 | } 591 | } 592 | 593 | _zip.Close(); 594 | tempZip.Close(); 595 | 596 | File.Delete(_zip.FileName); 597 | File.Move(tempZipName, _zip.FileName); 598 | 599 | _zip = ZipStorer.Open(_zip.FileName, _zip.Access); 600 | } 601 | catch 602 | { 603 | return false; 604 | } 605 | finally 606 | { 607 | if (File.Exists(tempZipName)) 608 | File.Delete(tempZipName); 609 | if (File.Exists(tempEntryName)) 610 | File.Delete(tempEntryName); 611 | } 612 | return true; 613 | } 614 | #endregion 615 | 616 | #region Private methods 617 | // Calculate the file offset by reading the corresponding local header 618 | private uint GetFileOffset(uint _headerOffset) 619 | { 620 | byte[] buffer = new byte[2]; 621 | 622 | this.ZipFileStream.Seek(_headerOffset + 26, SeekOrigin.Begin); 623 | this.ZipFileStream.Read(buffer, 0, 2); 624 | ushort filenameSize = BitConverter.ToUInt16(buffer, 0); 625 | this.ZipFileStream.Read(buffer, 0, 2); 626 | ushort extraSize = BitConverter.ToUInt16(buffer, 0); 627 | 628 | return (uint)(30 + filenameSize + extraSize + _headerOffset); 629 | } 630 | 631 | /* Local file header: 632 | local file header signature 4 bytes (0x04034b50) 633 | version needed to extract 2 bytes 634 | general purpose bit flag 2 bytes 635 | compression method 2 bytes 636 | last mod file time 2 bytes 637 | last mod file date 2 bytes 638 | crc-32 4 bytes 639 | compressed size 4 bytes 640 | uncompressed size 4 bytes 641 | filename length 2 bytes 642 | extra field length 2 bytes 643 | 644 | filename (variable size) 645 | extra field (variable size) 646 | */ 647 | private void WriteLocalHeader(ZipFileEntry _zfe) 648 | { 649 | long pos = this.ZipFileStream.Position; 650 | Encoding encoder = _zfe.EncodeUTF8 ? Encoding.UTF8 : DefaultEncoding; 651 | byte[] encodedFilename = encoder.GetBytes(_zfe.FilenameInZip); 652 | byte[] extraInfo = this.CreateExtraInfo(_zfe); 653 | 654 | this.ZipFileStream.Write(new byte[] { 80, 75, 3, 4, 20, 0 }, 0, 6); // No extra header 655 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)(_zfe.EncodeUTF8 ? 0x0800 : 0)), 0, 2); // filename and comment encoding 656 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)_zfe.Method), 0, 2); // zipping method 657 | this.ZipFileStream.Write(BitConverter.GetBytes(DateTimeToDosTime(_zfe.ModifyTime)), 0, 4); // zipping date and time 658 | this.ZipFileStream.Write(new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }, 0, 12); // unused CRC, un/compressed size, updated later 659 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)encodedFilename.Length), 0, 2); // filename length 660 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)extraInfo.Length), 0, 2); // extra length 661 | 662 | this.ZipFileStream.Write(encodedFilename, 0, encodedFilename.Length); 663 | this.ZipFileStream.Write(extraInfo, 0, extraInfo.Length); 664 | _zfe.HeaderSize = (uint)(this.ZipFileStream.Position - pos); 665 | } 666 | 667 | /* Central directory's File header: 668 | central file header signature 4 bytes (0x02014b50) 669 | version made by 2 bytes 670 | version needed to extract 2 bytes 671 | general purpose bit flag 2 bytes 672 | compression method 2 bytes 673 | last mod file time 2 bytes 674 | last mod file date 2 bytes 675 | crc-32 4 bytes 676 | compressed size 4 bytes 677 | uncompressed size 4 bytes 678 | filename length 2 bytes 679 | extra field length 2 bytes 680 | file comment length 2 bytes 681 | disk number start 2 bytes 682 | internal file attributes 2 bytes 683 | external file attributes 4 bytes 684 | relative offset of local header 4 bytes 685 | 686 | filename (variable size) 687 | extra field (variable size) 688 | file comment (variable size) 689 | */ 690 | private void WriteCentralDirRecord(ZipFileEntry _zfe) 691 | { 692 | Encoding encoder = _zfe.EncodeUTF8 ? Encoding.UTF8 : DefaultEncoding; 693 | byte[] encodedFilename = encoder.GetBytes(_zfe.FilenameInZip); 694 | byte[] encodedComment = encoder.GetBytes(_zfe.Comment); 695 | byte[] extraInfo = this.CreateExtraInfo(_zfe); 696 | 697 | this.ZipFileStream.Write(new byte[] { 80, 75, 1, 2, 23, 0xB, 20, 0 }, 0, 8); 698 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)(_zfe.EncodeUTF8 ? 0x0800 : 0)), 0, 2); // filename and comment encoding 699 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)_zfe.Method), 0, 2); // zipping method 700 | this.ZipFileStream.Write(BitConverter.GetBytes(DateTimeToDosTime(_zfe.ModifyTime)), 0, 4); // zipping date and time 701 | this.ZipFileStream.Write(BitConverter.GetBytes(_zfe.Crc32), 0, 4); // file CRC 702 | this.ZipFileStream.Write(BitConverter.GetBytes(get32bitSize(_zfe.CompressedSize)), 0, 4); // compressed file size 703 | this.ZipFileStream.Write(BitConverter.GetBytes(get32bitSize(_zfe.FileSize)), 0, 4); // uncompressed file size 704 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)encodedFilename.Length), 0, 2); // Filename in zip 705 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)extraInfo.Length), 0, 2); // extra length 706 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)encodedComment.Length), 0, 2); 707 | 708 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)0), 0, 2); // disk=0 709 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)0), 0, 2); // file type: binary 710 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)0), 0, 2); // Internal file attributes 711 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)0x8100), 0, 2); // External file attributes (normal/readable) 712 | this.ZipFileStream.Write(BitConverter.GetBytes(get32bitSize(_zfe.HeaderOffset)), 0, 4); // Offset of header 713 | 714 | this.ZipFileStream.Write(encodedFilename, 0, encodedFilename.Length); 715 | this.ZipFileStream.Write(extraInfo, 0, extraInfo.Length); 716 | this.ZipFileStream.Write(encodedComment, 0, encodedComment.Length); 717 | } 718 | 719 | private uint get32bitSize(long size) 720 | { 721 | return size >= 0xFFFFFFFF ? 0xFFFFFFFF : (uint)size; 722 | } 723 | 724 | /* 725 | Zip64 end of central directory record 726 | zip64 end of central dir 727 | signature 4 bytes (0x06064b50) 728 | size of zip64 end of central 729 | directory record 8 bytes 730 | version made by 2 bytes 731 | version needed to extract 2 bytes 732 | number of this disk 4 bytes 733 | number of the disk with the 734 | start of the central directory 4 bytes 735 | total number of entries in the 736 | central directory on this disk 8 bytes 737 | total number of entries in the 738 | central directory 8 bytes 739 | size of the central directory 8 bytes 740 | offset of start of central 741 | directory with respect to 742 | the starting disk number 8 bytes 743 | zip64 extensible data sector (variable size) 744 | 745 | Zip64 end of central directory locator 746 | 747 | zip64 end of central dir locator 748 | signature 4 bytes (0x07064b50) 749 | number of the disk with the 750 | start of the zip64 end of 751 | central directory 4 bytes 752 | relative offset of the zip64 753 | end of central directory record 8 bytes 754 | total number of disks 4 bytes 755 | 756 | End of central dir record: 757 | end of central dir signature 4 bytes (0x06054b50) 758 | number of this disk 2 bytes 759 | number of the disk with the 760 | start of the central directory 2 bytes 761 | total number of entries in 762 | the central dir on this disk 2 bytes 763 | total number of entries in 764 | the central dir 2 bytes 765 | size of the central directory 4 bytes 766 | offset of start of central 767 | directory with respect to 768 | the starting disk number 4 bytes 769 | zipfile comment length 2 bytes 770 | zipfile comment (variable size) 771 | */ 772 | private void WriteEndRecord(long _size, long _offset) 773 | { 774 | long dirOffset = ZipFileStream.Length; 775 | 776 | // Zip64 end of central directory record 777 | this.ZipFileStream.Position = dirOffset; 778 | this.ZipFileStream.Write(new byte[] { 80, 75, 6, 6 }, 0, 4); 779 | this.ZipFileStream.Write(BitConverter.GetBytes((Int64)44), 0, 8); // size of zip64 end of central directory 780 | this.ZipFileStream.Write(BitConverter.GetBytes((UInt16)45), 0, 2); // version made by 781 | this.ZipFileStream.Write(BitConverter.GetBytes((UInt16)45), 0, 2); // version needed to extract 782 | this.ZipFileStream.Write(BitConverter.GetBytes((UInt32)0), 0, 4); // current disk 783 | this.ZipFileStream.Write(BitConverter.GetBytes((UInt32)0), 0, 4); // start of central directory 784 | this.ZipFileStream.Write(BitConverter.GetBytes((Int64)Files.Count + ExistingFiles), 0, 8); // total number of entries in the central directory in disk 785 | this.ZipFileStream.Write(BitConverter.GetBytes((Int64)Files.Count + ExistingFiles), 0, 8); // total number of entries in the central directory 786 | this.ZipFileStream.Write(BitConverter.GetBytes(_size), 0, 8); // size of the central directory 787 | this.ZipFileStream.Write(BitConverter.GetBytes(_offset), 0, 8); // offset of start of central directory with respect to the starting disk number 788 | 789 | // Zip64 end of central directory locator 790 | this.ZipFileStream.Write(new byte[] { 80, 75, 6, 7 }, 0, 4); 791 | this.ZipFileStream.Write(BitConverter.GetBytes((UInt32)0), 0, 4); // number of the disk 792 | this.ZipFileStream.Write(BitConverter.GetBytes(dirOffset), 0, 8); // relative offset of the zip64 end of central directory record 793 | this.ZipFileStream.Write(BitConverter.GetBytes((UInt32)1), 0, 4); // total number of disks 794 | 795 | Encoding encoder = this.EncodeUTF8 ? Encoding.UTF8 : DefaultEncoding; 796 | byte[] encodedComment = encoder.GetBytes(this.Comment); 797 | 798 | this.ZipFileStream.Write(new byte[] { 80, 75, 5, 6, 0, 0, 0, 0 }, 0, 8); 799 | this.ZipFileStream.Write(new byte[] { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF }, 0, 12); 800 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)encodedComment.Length), 0, 2); 801 | this.ZipFileStream.Write(encodedComment, 0, encodedComment.Length); 802 | } 803 | 804 | // Copies all the source file into the zip storage 805 | #if NOASYNC 806 | private Compression 807 | #else 808 | private async Task 809 | #endif 810 | Store(ZipFileEntry _zfe, Stream _source) 811 | { 812 | byte[] buffer = new byte[16384]; 813 | int bytesRead; 814 | uint totalRead = 0; 815 | Stream outStream; 816 | 817 | long posStart = this.ZipFileStream.Position; 818 | long sourceStart = _source.CanSeek ? _source.Position : 0; 819 | 820 | if (_zfe.Method == Compression.Store) 821 | outStream = this.ZipFileStream; 822 | else 823 | outStream = new DeflateStream(this.ZipFileStream, CompressionMode.Compress, true); 824 | 825 | _zfe.Crc32 = 0 ^ 0xffffffff; 826 | 827 | do 828 | { 829 | #if NOASYNC 830 | bytesRead = _source.Read(buffer, 0, buffer.Length); 831 | if (bytesRead > 0) 832 | outStream.Write(buffer, 0, bytesRead); 833 | #else 834 | bytesRead = await _source.ReadAsync(buffer, 0, buffer.Length); 835 | if (bytesRead > 0) 836 | await outStream.WriteAsync(buffer, 0, bytesRead); 837 | #endif 838 | 839 | for (uint i = 0; i < bytesRead; i++) 840 | { 841 | _zfe.Crc32 = ZipStorer.CrcTable[(_zfe.Crc32 ^ buffer[i]) & 0xFF] ^ (_zfe.Crc32 >> 8); 842 | } 843 | 844 | totalRead += (uint)bytesRead; 845 | } while (bytesRead > 0); 846 | 847 | outStream.Flush(); 848 | 849 | if (_zfe.Method == Compression.Deflate) 850 | outStream.Dispose(); 851 | 852 | _zfe.Crc32 ^= 0xFFFFFFFF; 853 | _zfe.FileSize = totalRead; 854 | _zfe.CompressedSize = (uint)(this.ZipFileStream.Position - posStart); 855 | 856 | // Verify for real compression 857 | if (_zfe.Method == Compression.Deflate && !this.ForceDeflating && _source.CanSeek && _zfe.CompressedSize > _zfe.FileSize) 858 | { 859 | // Start operation again with Store algorithm 860 | _zfe.Method = Compression.Store; 861 | this.ZipFileStream.Position = posStart; 862 | this.ZipFileStream.SetLength(posStart); 863 | _source.Position = sourceStart; 864 | 865 | #if NOASYNC 866 | return this.Store(_zfe, _source); 867 | #else 868 | return await this.Store(_zfe, _source); 869 | #endif 870 | } 871 | 872 | return _zfe.Method; 873 | } 874 | 875 | /* DOS Date and time: 876 | MS-DOS date. The date is a packed value with the following format. Bits Description 877 | 0-4 Day of the month (1–31) 878 | 5-8 Month (1 = January, 2 = February, and so on) 879 | 9-15 Year offset from 1980 (add 1980 to get actual year) 880 | MS-DOS time. The time is a packed value with the following format. Bits Description 881 | 0-4 Second divided by 2 882 | 5-10 Minute (0–59) 883 | 11-15 Hour (0–23 on a 24-hour clock) 884 | */ 885 | private uint DateTimeToDosTime(DateTime _dt) 886 | { 887 | return (uint)( 888 | (_dt.Second / 2) | (_dt.Minute << 5) | (_dt.Hour << 11) | 889 | (_dt.Day << 16) | (_dt.Month << 21) | ((_dt.Year - 1980) << 25)); 890 | } 891 | 892 | private DateTime? DosTimeToDateTime(uint _dt) 893 | { 894 | int year = (int)(_dt >> 25) + 1980; 895 | int month = (int)(_dt >> 21) & 15; 896 | int day = (int)(_dt >> 16) & 31; 897 | int hours = (int)(_dt >> 11) & 31; 898 | int minutes = (int)(_dt >> 5) & 63; 899 | int seconds = (int)(_dt & 31) * 2; 900 | 901 | if (month == 0 || day == 0 || year >= 2107) 902 | return DateTime.Now; 903 | 904 | return new DateTime(year, month, day, hours, minutes, seconds); 905 | } 906 | 907 | private byte[] CreateExtraInfo(ZipFileEntry _zfe) 908 | { 909 | // No extra fields are allowed for STORE files 910 | if (_zfe.Method == Compression.Store) 911 | return Array.Empty(); 912 | 913 | byte[] buffer = new byte[36 + 36]; 914 | BitConverter.GetBytes((ushort)0x0001).CopyTo(buffer, 0); // ZIP64 Information 915 | BitConverter.GetBytes((ushort)32).CopyTo(buffer, 2); // Length 916 | BitConverter.GetBytes((ushort)1).CopyTo(buffer, 8); // Tag 1 917 | BitConverter.GetBytes((ushort)24).CopyTo(buffer, 10); // Size 1 918 | BitConverter.GetBytes(_zfe.FileSize).CopyTo(buffer, 12); // MTime 919 | BitConverter.GetBytes(_zfe.CompressedSize).CopyTo(buffer, 20); // ATime 920 | BitConverter.GetBytes(_zfe.HeaderOffset).CopyTo(buffer, 28); // CTime 921 | 922 | BitConverter.GetBytes((ushort)0x000A).CopyTo(buffer, 36); // NTFS FileTime 923 | BitConverter.GetBytes((ushort)32).CopyTo(buffer, 38); // Length 924 | BitConverter.GetBytes((ushort)1).CopyTo(buffer, 44); // Tag 1 925 | BitConverter.GetBytes((ushort)24).CopyTo(buffer, 46); // Size 1 926 | BitConverter.GetBytes(_zfe.ModifyTime.ToFileTime()).CopyTo(buffer, 48); // MTime 927 | BitConverter.GetBytes(_zfe.AccessTime.ToFileTime()).CopyTo(buffer, 56); // ATime 928 | BitConverter.GetBytes(_zfe.CreationTime.ToFileTime()).CopyTo(buffer, 64); // CTime 929 | 930 | return buffer; 931 | } 932 | 933 | private void ReadExtraInfo(byte[] buffer, int offset, ZipFileEntry _zfe) 934 | { 935 | if (buffer.Length < 4) 936 | return; 937 | 938 | int pos = offset; 939 | uint tag, size; 940 | 941 | while (pos < buffer.Length - 4) 942 | { 943 | uint extraId = BitConverter.ToUInt16(buffer, pos); 944 | uint length = BitConverter.ToUInt16(buffer, pos + 2); 945 | 946 | if (extraId == 0x0001) // ZIP64 Information 947 | { 948 | tag = BitConverter.ToUInt16(buffer, pos + 8); 949 | size = BitConverter.ToUInt16(buffer, pos + 10); 950 | 951 | if (tag == 1 && size >= 24) 952 | { 953 | if (_zfe.FileSize == 0xFFFFFFFF) 954 | _zfe.FileSize = BitConverter.ToInt64(buffer, pos + 12); 955 | if (_zfe.CompressedSize == 0xFFFFFFFF) 956 | _zfe.CompressedSize = BitConverter.ToInt64(buffer, pos + 20); 957 | if (_zfe.HeaderOffset == 0xFFFFFFFF) 958 | _zfe.HeaderOffset = BitConverter.ToInt64(buffer, pos + 28); 959 | } 960 | } 961 | 962 | if (extraId == 0x000A) // NTFS FileTime 963 | { 964 | tag = BitConverter.ToUInt16(buffer, pos + 8); 965 | size = BitConverter.ToUInt16(buffer, pos + 10); 966 | 967 | if (tag == 1 && size == 24) 968 | { 969 | _zfe.ModifyTime = DateTime.FromFileTime(BitConverter.ToInt64(buffer, pos + 12)); 970 | _zfe.AccessTime = DateTime.FromFileTime(BitConverter.ToInt64(buffer, pos + 20)); 971 | _zfe.CreationTime = DateTime.FromFileTime(BitConverter.ToInt64(buffer, pos + 28)); 972 | } 973 | } 974 | 975 | pos += (int)length + 4; 976 | } 977 | } 978 | 979 | /* CRC32 algorithm 980 | The 'magic number' for the CRC is 0xdebb20e3. 981 | The proper CRC pre and post conditioning is used, meaning that the CRC register is 982 | pre-conditioned with all ones (a starting value of 0xffffffff) and the value is post-conditioned by 983 | taking the one's complement of the CRC residual. 984 | If bit 3 of the general purpose flag is set, this field is set to zero in the local header and the correct 985 | value is put in the data descriptor and in the central directory. 986 | */ 987 | private void UpdateCrcAndSizes(ZipFileEntry _zfe) 988 | { 989 | long lastPos = this.ZipFileStream.Position; // remember position 990 | 991 | this.ZipFileStream.Position = _zfe.HeaderOffset + 8; 992 | this.ZipFileStream.Write(BitConverter.GetBytes((ushort)_zfe.Method), 0, 2); // zipping method 993 | 994 | this.ZipFileStream.Position = _zfe.HeaderOffset + 14; 995 | this.ZipFileStream.Write(BitConverter.GetBytes(_zfe.Crc32), 0, 4); // Update CRC 996 | this.ZipFileStream.Write(BitConverter.GetBytes(get32bitSize(_zfe.CompressedSize)), 0, 4); // Compressed size 997 | this.ZipFileStream.Write(BitConverter.GetBytes(get32bitSize(_zfe.FileSize)), 0, 4); // Uncompressed size 998 | 999 | this.ZipFileStream.Position = lastPos; // restore position 1000 | } 1001 | 1002 | // Replaces backslashes with slashes to store in zip header 1003 | private string NormalizedFilename(string _filename) 1004 | { 1005 | string filename = _filename.Replace('\\', '/'); 1006 | 1007 | int pos = filename.IndexOf(':'); 1008 | if (pos >= 0) 1009 | filename = filename.Remove(0, pos + 1); 1010 | 1011 | return filename.Trim('/'); 1012 | } 1013 | 1014 | // Reads the end-of-central-directory record 1015 | private bool ReadFileInfo() 1016 | { 1017 | if (this.ZipFileStream.Length < 22) 1018 | return false; 1019 | 1020 | try 1021 | { 1022 | this.ZipFileStream.Seek(-17, SeekOrigin.End); 1023 | BinaryReader br = new BinaryReader(this.ZipFileStream); 1024 | do 1025 | { 1026 | this.ZipFileStream.Seek(-5, SeekOrigin.Current); 1027 | UInt32 sig = br.ReadUInt32(); 1028 | 1029 | if (sig == 0x06054b50) // It is central dir 1030 | { 1031 | long dirPosition = ZipFileStream.Position - 4; 1032 | 1033 | this.ZipFileStream.Seek(6, SeekOrigin.Current); 1034 | 1035 | long entries = br.ReadUInt16(); 1036 | long centralSize = br.ReadInt32(); 1037 | long centralDirOffset = br.ReadUInt32(); 1038 | UInt16 commentSize = br.ReadUInt16(); 1039 | 1040 | var commentPosition = ZipFileStream.Position; 1041 | 1042 | if (centralDirOffset == 0xffffffff) // It is a Zip64 file 1043 | { 1044 | this.ZipFileStream.Position = dirPosition - 20; 1045 | 1046 | sig = br.ReadUInt32(); 1047 | 1048 | if (sig != 0x07064b50) // Not a Zip64 central dir locator 1049 | return false; 1050 | 1051 | this.ZipFileStream.Seek(4, SeekOrigin.Current); 1052 | 1053 | long dir64Position = br.ReadInt64(); 1054 | this.ZipFileStream.Position = dir64Position; 1055 | 1056 | sig = br.ReadUInt32(); 1057 | 1058 | if (sig != 0x06064b50) // Not a Zip64 central dir record 1059 | return false; 1060 | 1061 | this.ZipFileStream.Seek(28, SeekOrigin.Current); 1062 | entries = br.ReadInt64(); 1063 | centralSize = br.ReadInt64(); 1064 | centralDirOffset = br.ReadInt64(); 1065 | } 1066 | 1067 | // check if comment field is the very last data in file 1068 | if (commentPosition + commentSize != this.ZipFileStream.Length) 1069 | return false; 1070 | 1071 | // Copy entire central directory to a memory buffer 1072 | this.ExistingFiles = entries; 1073 | this.CentralDirImage = new byte[centralSize]; 1074 | this.ZipFileStream.Seek(centralDirOffset, SeekOrigin.Begin); 1075 | this.ZipFileStream.Read(this.CentralDirImage, 0, (int)centralSize); 1076 | 1077 | // Leave the pointer at the begining of central dir, to append new files 1078 | this.ZipFileStream.Seek(centralDirOffset, SeekOrigin.Begin); 1079 | return true; 1080 | } 1081 | } while (this.ZipFileStream.Position > 0); 1082 | } 1083 | catch { } 1084 | 1085 | return false; 1086 | } 1087 | #endregion 1088 | 1089 | #region IDisposable Members 1090 | /// 1091 | /// Closes the Zip file stream 1092 | /// 1093 | public void Dispose() 1094 | { 1095 | this.Close(); 1096 | } 1097 | #endregion 1098 | } 1099 | } 1100 | -------------------------------------------------------------------------------- /image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BELGRADE-OUTLAW/SCRIMTEC/3bd7966de63dd61b9fce3a1e19bb64e0053807f9/image.png -------------------------------------------------------------------------------- /packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /scribd.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BELGRADE-OUTLAW/SCRIMTEC/3bd7966de63dd61b9fce3a1e19bb64e0053807f9/scribd.ico --------------------------------------------------------------------------------