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

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@element>
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 | * @column.Text |
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 | *
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 | 
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 (131)
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 (059)
883 | 11-15 Hour (023 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
--------------------------------------------------------------------------------