Skip to content

Commit 3f5d7d2

Browse files
authored
Merge pull request #1 from nelinory/development
Development to Main
2 parents 3f79717 + b5cb6b8 commit 3f5d7d2

27 files changed

Lines changed: 620 additions & 88 deletions

README.md

Lines changed: 51 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ This project is heavily inspired by https://github.com/jya-dev/supernote-tool.
1212

1313
### Supported file formats
1414
- [ ] `*.note` file created on Supernote A5
15-
- [X] `*.note` file created on Supernote A5X (firmware Chauvet 2.8.22)
16-
- [ ] `*.mark` file created on Supernote A5X (firmware Chauvet 2.8.22)
17-
- [X] `*.note` file created on Supernote A6X (firmware Chauvet 2.8.22)
18-
- [ ] `*.mark` file created on Supernote A6X (firmware Chauvet 2.8.22)
15+
- [X] `*.note` file created on Supernote A5X/A6X (firmware Chauvet 2.8.22)
16+
- [ ] `*.mark` file created on Supernote A5X/A6X (firmware Chauvet 2.8.22)
1917

2018
### Key Features - A5X/A6X models only
2119
- [X] Export the Supernote file structure (metadata)
2220
```C#
2321
using (FileStream fileStream = new FileStream(NOTE_FILE_PATH, FileMode.Open, FileAccess.Read))
2422
{
2523
Parser parser = new Parser();
26-
Metadata metadata = parser.ParseMetadata(_fileStream, Policy.Strict);
24+
Metadata metadata = parser.ParseMetadata(fileStream, Policy.Strict);
25+
26+
// metadata
2727
string metadataJson = metadata.ToJson();
2828
}
2929
```
@@ -32,22 +32,63 @@ This project is heavily inspired by https://github.com/jya-dev/supernote-tool.
3232
using (FileStream fileStream = new FileStream(NOTE_FILE_PATH, FileMode.Open, FileAccess.Read))
3333
{
3434
Parser parser = new Parser();
35-
Notebook notebook = parser.LoadNotebook(_fileStream, Policy.Strict);
35+
Notebook notebook = parser.LoadNotebook(fileStream, Policy.Strict);
3636
ImageConverter converter = new Converter.ImageConverter(notebook, DefaultColorPalette.Grayscale);
3737

38-
// convert a page
38+
// convert a page to PNG
3939
Image page_0 = converter.Convert(0, VisibilityOverlay.Default);
40+
// save the result
41+
page_0.SaveAsPng(PNG_FILE_LOCATION);
4042

41-
// convert all pages
43+
// convert all pages to PNG
4244
List<Image> images = converter.ConvertAll(VisibilityOverlay.Default);
45+
// save the result
46+
...
47+
}
48+
```
49+
- [X] Export individual pages/all pages to pdf file format
50+
```C#
51+
using (FileStream fileStream = new FileStream(NOTE_FILE_PATH, FileMode.Open, FileAccess.Read))
52+
{
53+
Parser parser = new Parser();
54+
Notebook notebook = parser.LoadNotebook(fileStream, Policy.Strict);
55+
PdfConverter converter = new PdfConverter(notebook, DefaultColorPalette.Grayscale);
56+
57+
// convert a page to PDF
58+
byte[] page_0 = converter.Convert(0);
59+
// save the result
60+
File.WriteAllBytes(PDF_FILE_LOCATION, page_0);
61+
62+
// convert all pages to PDF
63+
byte[] allPages = converter.ConvertAll();
64+
// save the result
65+
...
66+
67+
// convert all pages to PDF and build all links
68+
byte[] allPages = converter.ConvertAll(enableLinks: true);
69+
// save the result
70+
...
4371
}
4472
```
4573
- [ ] Export individual pages/all pages to svg file format
46-
- [ ] Export individual pages/all pages to pdf file format
4774
- [ ] Export individual pages/all pages to vector pdf file format
4875
- [ ] Export all text from realtime recognition note to text file format
4976
- [ ] Export individual annotation/all annotations for a pdf file format
5077

5178
### Tested on
5279
- Windows 10 version 22H2 (OS Build 19045.2846)
53-
- Windows 11 version 22H2 (OS Build 22621.1413)
80+
- Windows 11 version 22H2 (OS Build 22621.1413)
81+
82+
### Used Nuget Packages
83+
- SupernoteSharp
84+
- SixLabors.ImageSharp: https://github.com/SixLabors/ImageSharp
85+
- VectSharp: https://github.com/arklumpus/VectSharp
86+
- VectSharp.PDF: https://github.com/arklumpus/VectSharp
87+
- SupernoteSharpUnitTests
88+
- SixLabors.ImageSharp: https://github.com/SixLabors/ImageSharp
89+
- Codeuctivity.ImageSharpCompare: https://github.com/Codeuctivity/ImageSharp.Compare
90+
- coverlet.collector: https://github.com/coverlet-coverage/coverlet
91+
- FluentAssertions: https://fluentassertions.com
92+
- Microsoft.NET.Test.Sdk: https://github.com/microsoft/vstest
93+
- MSTest.TestAdapter: https://github.com/microsoft/testfx
94+
- MSTest.TestFramework: https://github.com/microsoft/testfx

SupernoteSharp.sln

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
1111
ProjectSection(SolutionItems) = preProject
1212
.editorconfig = .editorconfig
1313
.gitignore = .gitignore
14-
SupernoteSharp\A5X_Metadata_Schema.md = SupernoteSharp\A5X_Metadata_Schema.md
1514
LICENSE = LICENSE
1615
README.md = README.md
1716
EndProjectSection

SupernoteSharp/A5X_Metadata_Schema.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,8 @@ footer
4141
"TITLEPROTOCOL"
4242
"TITLESTYLE"
4343
links []
44-
"LINKTYPE"
45-
"LINKINOUT"
44+
"LINKTYPE" - 0 is "Page", 1 is "File", 4 is "Web"
45+
"LINKINOUT" - 0 is "Out", 1 is "In"
4646
"LINKBITMAP"
4747
"LINKSTYLE"
4848
"LINKTIMESTAMP"

SupernoteSharp/AssemblyInfo.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
using System.Reflection;
22

3-
[assembly: AssemblyVersion("0.1.*")]
3+
[assembly: AssemblyVersion("0.2.*")]
44
[assembly: AssemblyTitle("SupernoteSharp")]
55
[assembly: AssemblyProduct("SupernoteSharp")]
66
[assembly: AssemblyCopyright("Copyright © nelinory 2023")]

SupernoteSharp/Business/Converter.cs

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,13 @@
55
using SupernoteSharp.Entities;
66
using System;
77
using System.Collections.Generic;
8+
using System.IO;
9+
using System.Linq;
10+
using System.Runtime.CompilerServices;
811
using System.Text.Json;
12+
using VectSharp;
13+
using VectSharp.PDF;
14+
using Page = SupernoteSharp.Entities.Page;
915

1016
namespace SupernoteSharp.Business
1117
{
@@ -212,5 +218,147 @@ private Image<Rgba32> FlattenImage(Image<Rgba32> foreground, Image<Rgba32> backg
212218
return background;
213219
}
214220
}
221+
222+
public class PdfConverter
223+
{
224+
private const int ALL_PAGES = -1;
225+
226+
private Notebook _notebook;
227+
private ColorPalette _palette;
228+
229+
public PdfConverter(Notebook notebook, ColorPalette palette)
230+
{
231+
_notebook = notebook;
232+
_palette = palette;
233+
}
234+
235+
public byte[] Convert(int pageNumber, bool vectorize = false, bool enableLinks = false)
236+
{
237+
Dictionary<int, Image> pageImages = new Dictionary<int, Image>();
238+
239+
if (vectorize == true)
240+
// TODO: Implement vectorized image
241+
throw new NotImplementedException();
242+
else
243+
{
244+
ImageConverter converter = new Converter.ImageConverter(_notebook, DefaultColorPalette.Grayscale);
245+
if (pageNumber == ALL_PAGES)
246+
{
247+
List<Image> images = converter.ConvertAll(VisibilityOverlay.Default);
248+
for (int i = 0; i < images.Count; i++)
249+
pageImages.Add(i, images[i]);
250+
}
251+
else
252+
pageImages.Add(pageNumber, converter.Convert(pageNumber, VisibilityOverlay.Default));
253+
}
254+
255+
return CreatePdf(pageImages, vectorize, enableLinks);
256+
}
257+
258+
public byte[] ConvertAll(bool vectorize = false, bool enableLinks = false)
259+
{
260+
return Convert(ALL_PAGES, vectorize, enableLinks);
261+
}
262+
263+
private byte[] CreatePdf(Dictionary<int, Image> pageImages, bool vectorize, bool enableLinks)
264+
{
265+
Dictionary<int, VectSharp.Page> pdfPages = new Dictionary<int, VectSharp.Page>();
266+
267+
// A4 page size is 11.01" x 15.58"
268+
// For a PDF document, each dot is 1/72nd of an inch
269+
double pageWidth = 210 * 72 / 25.4; // width in pixels
270+
double pageHeight = 297 * 72 / 25.4; // height in pixels
271+
272+
foreach (KeyValuePair<int, Image> kvp in pageImages)
273+
{
274+
Image pageImage = kvp.Value;
275+
VectSharp.Page pdfPage = new VectSharp.Page(pageWidth, pageHeight);
276+
277+
// set image scale to fit the pdf page
278+
pdfPage.Graphics.Scale(pageWidth / pageImage.Width, pageHeight / pageImage.Height);
279+
280+
// convert image to byte[]
281+
byte[] imageBytes = new byte[pageImage.Width * pageImage.Height * Unsafe.SizeOf<Rgba32>()];
282+
pageImage.CloneAs<Rgba32>().CopyPixelDataTo(imageBytes);
283+
284+
// draw the image onto the pdf page
285+
pdfPage.Graphics.DrawRasterImage(0, 0, new RasterImage(imageBytes, pageImage.Width, pageImage.Height, PixelFormats.RGBA, false));
286+
287+
// add completed page
288+
pdfPages.Add(kvp.Key, pdfPage);
289+
}
290+
291+
// add links if requested
292+
Dictionary<string, string> links = null;
293+
if (enableLinks == true)
294+
links = AddLinks(pdfPages);
295+
296+
// create the final pdf document
297+
Document pdfDocument = new Document();
298+
pdfDocument.Pages.AddRange(pdfPages.Values);
299+
300+
using (MemoryStream memoryStream = new MemoryStream())
301+
{
302+
pdfDocument.SaveAsPDF(memoryStream, linkDestinations: links);
303+
return memoryStream.ToArray();
304+
}
305+
}
306+
307+
private Dictionary<string, string> AddLinks(Dictionary<int, VectSharp.Page> pdfPages)
308+
{
309+
List<Link> noteLinks = _notebook.Links;
310+
Dictionary<string, string> links = new Dictionary<string, string>();
311+
312+
foreach (KeyValuePair<int, VectSharp.Page> kvp in pdfPages)
313+
{
314+
// get all outbound links for the current page
315+
List<Link> outboundLinks = noteLinks.Where(x => x.PageNumber == kvp.Key && x.InOut == (int)LinkDirection.Out).ToList();
316+
if (outboundLinks.Count == 0)
317+
continue;
318+
319+
// link all web links
320+
List<Link> webLinks = outboundLinks.Where(x => x.Type == (int)LinkType.Web).ToList();
321+
foreach (Link webLink in webLinks)
322+
{
323+
string webLinkTag = $"WebLink_{webLink.Metadata["LINKBITMAP"]}";
324+
string webLinkUrl = System.Text.Encoding.UTF8.GetString(System.Convert.FromBase64String(webLink.FilePath));
325+
326+
kvp.Value.Graphics.StrokeRectangle(webLink.Rect.left, webLink.Rect.top, webLink.Rect.width, webLink.Rect.height,
327+
Colour.FromRgba(0, 0, 0, 0), tag: webLinkTag);
328+
329+
links.Add(webLinkTag, webLinkUrl);
330+
}
331+
332+
// if we only have one page, do not build the links between pages
333+
if (pdfPages.Count == 1)
334+
continue;
335+
336+
// link all internal page links
337+
List<Link> sourceLinks = outboundLinks.Where(x => x.Type == (int)LinkType.Page).ToList();
338+
foreach (Link sourceLink in sourceLinks)
339+
{
340+
bool isInternalLink = (sourceLink.FileId == _notebook.FileId);
341+
if (isInternalLink == true)
342+
{
343+
// each internal link is a pair of outbound and inbound, they have the same timestamp and rect coordinates
344+
Link targetLink = noteLinks.Where(x => x.InOut == (int)LinkDirection.In && x.Timestamp == sourceLink.Timestamp && x.Rect.Equals(sourceLink.Rect)).FirstOrDefault();
345+
346+
string sourceLinkTag = $"SourceLink_{sourceLink.Metadata["LINKBITMAP"]}";
347+
string targetLinkTag = $"TargetLink_{targetLink.Metadata["LINKBITMAP"]}";
348+
349+
pdfPages[sourceLink.PageNumber].Graphics.StrokeRectangle(sourceLink.Rect.left, sourceLink.Rect.top, sourceLink.Rect.width, sourceLink.Rect.height,
350+
Colour.FromRgba(0, 0, 0, 0), tag: sourceLinkTag);
351+
352+
pdfPages[targetLink.PageNumber].Graphics.StrokeRectangle(targetLink.Rect.left, 0, targetLink.Rect.width, targetLink.Rect.height,
353+
Colour.FromRgba(0, 0, 0, 0), tag: targetLinkTag);
354+
355+
links.Add(sourceLinkTag, $"#{targetLinkTag}");
356+
}
357+
}
358+
}
359+
360+
return links;
361+
}
362+
}
215363
}
216364
}

SupernoteSharp/Business/Parser.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public Notebook LoadNotebook(FileStream fileStream, Policy policy)
6363
{
6464
int titleAddress = Int32.Parse((string)notebook.Titles[i].Metadata["TITLEBITMAP"]);
6565
notebook.Titles[i].Content = GetContentAtAddress(fileStream, titleAddress);
66-
notebook.Titles[i].PageNumber = titlePageNumbers[i];
66+
notebook.Titles[i].PageNumber = titlePageNumbers[i] - 1; // title indexes are not 0 based
6767
}
6868

6969
// attach link data
@@ -72,7 +72,7 @@ public Notebook LoadNotebook(FileStream fileStream, Policy policy)
7272
{
7373
int linkAddress = Int32.Parse((string)notebook.Links[i].Metadata["LINKBITMAP"]);
7474
notebook.Links[i].Content = GetContentAtAddress(fileStream, linkAddress);
75-
notebook.Links[i].PageNumber = linkPageNumbers[i];
75+
notebook.Links[i].PageNumber = linkPageNumbers[i] - 1; // link indexes are not 0 based
7676
}
7777

7878
// attach page data

SupernoteSharp/Business/SupernoteXParser.cs.cs

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,8 @@ internal override Dictionary<string, object> ParseFooterBlock(FileStream fileStr
6161
{
6262
keywords.Add(ParseMetadataBlock(fileStream, keywordAddress));
6363
}
64-
footer[Constants.KEY_KEYWORDS] = keywords;
64+
if (keywords.Count > 0)
65+
footer[Constants.KEY_KEYWORDS] = keywords;
6566

6667
// parse titles
6768
List<int> titleAddresses = GetItemAddresses(footer, "TITLE_");
@@ -70,7 +71,8 @@ internal override Dictionary<string, object> ParseFooterBlock(FileStream fileStr
7071
{
7172
titles.Add(ParseMetadataBlock(fileStream, titleAddress));
7273
}
73-
footer[Constants.KEY_TITLES] = titles;
74+
if (titles.Count > 0)
75+
footer[Constants.KEY_TITLES] = titles;
7476

7577
// parse links
7678
List<int> linkAddresses = GetItemAddresses(footer, "LINK");
@@ -79,7 +81,8 @@ internal override Dictionary<string, object> ParseFooterBlock(FileStream fileStr
7981
{
8082
links.Add(ParseMetadataBlock(fileStream, linkAddress));
8183
}
82-
footer[Constants.KEY_LINKS] = links;
84+
if (links.Count > 0)
85+
footer[Constants.KEY_LINKS] = links;
8386

8487
return footer;
8588
}

SupernoteSharp/Common/Enums.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,8 @@
88
public enum Policy { Strict, Loose }
99

1010
public enum VisibilityOverlay : byte { Default, Visible, Invisible }
11+
12+
public enum LinkDirection { Out = 0, In = 1 }
13+
14+
public enum LinkType { Page = 0, File = 1, Web = 4 }
1115
}

SupernoteSharp/Entities/Link.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ public class Link
1111
public int Position { get; private set; }
1212
public int Type { get; private set; }
1313
public int InOut { get; private set; }
14-
public Tuple<int, int, int, int> Rect { get; private set; }
14+
public (int left, int top, int width, int height) Rect { get; private set; }
1515
public string Timestamp { get; private set; }
1616
public string FilePath { get; private set; }
1717
public string FileId { get; private set; }
@@ -32,7 +32,7 @@ public Link(Dictionary<string, object> metadata)
3232
int top = int.Parse(rectangle[1]);
3333
int width = int.Parse(rectangle[2]);
3434
int height = int.Parse(rectangle[3]);
35-
Rect = new Tuple<int, int, int, int>(left, top, left + width, top + height);
35+
Rect = (left, top, width, height);
3636

3737
Timestamp = (string)metadata["LINKTIMESTAMP"];
3838
FilePath = (string)metadata["LINKFILE"];

SupernoteSharp/SupernoteSharp.csproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
<ItemGroup>
1212
<PackageReference Include="SixLabors.ImageSharp" Version="3.0.1" />
13+
<PackageReference Include="VectSharp" Version="2.4.2" />
14+
<PackageReference Include="VectSharp.PDF" Version="2.6.1" />
1315
</ItemGroup>
1416

1517
</Project>

0 commit comments

Comments
 (0)