diff --git a/ConvertOneNote2MarkDown-v2.Tests.ps1 b/ConvertOneNote2MarkDown-v2.Tests.ps1
index 72d1ae7..806daff 100644
--- a/ConvertOneNote2MarkDown-v2.Tests.ps1
+++ b/ConvertOneNote2MarkDown-v2.Tests.ps1
@@ -58,6 +58,7 @@ $usedocx = '1'
# $keepdocx = 1 # Deliberately omit a configuration option from
$prefixFolders = 1
$medialocation = 1
+$mergeInkStrokes = 1
$conversion = 'markdown-simple_tables-multiline_tables-grid_tables+pipe_tables' # Default
$headerTimestampEnabled = 1
$keepspaces = 1
@@ -85,6 +86,7 @@ $newlineCharacter = 1
$fakeUserInputs['prefixFolders']['value'] = ''
$fakeUserInputs['mdFileNameAndFolderNameMaxLength']['value'] = ''
$fakeUserInputs['medialocation']['value'] = ''
+ $fakeUserInputs['mergeInkStrokes']['value'] = ''
$fakeUserInputs['conversion']['value'] = ''
$fakeUserInputs['headerTimestampEnabled']['value'] = ''
$fakeUserInputs['keepspaces']['value'] = ''
@@ -732,6 +734,16 @@ AAAAAElFTkSuQmCC
+
+
+
+ ACoDC0gQRICArARFNUY1EQAAgD8fCREAAAAAAADwPwoLAg1SsxnAED4DQiU=
+
+
+
+
+ ACoDC0gQRICArARFNUY1EQAAgD8fCREAAAAAAADwPwoLAg41wQiQEEAUQBQ=
+
@@ -1191,6 +1203,49 @@ Describe 'New-SectionGroupConversionConfig' -Tag 'Unit' {
}
}
+ It "Should honor config mergeInkStrokes" {
+ $params['Config']['mergeInkStrokes']['value'] = 1
+
+ $result = @( New-SectionGroupConversionConfig @params 6>$null )
+
+ # 15 pages from 'test' notebook, 15 pages from 'test2' notebook
+ $result.Count | Should -Be 48
+
+ foreach ($pageCfg in $result) {
+ $pageCfg['inkDrawings'].Count | Should -Be 0
+ $pageCfg['images'].Count | Should -Be 0
+ }
+
+ $params['Config']['mergeInkStrokes']['value'] = 2
+
+ $result = @( New-SectionGroupConversionConfig @params 6>$null )
+
+ # 15 pages from 'test' notebook, 15 pages from 'test2' notebook
+ $result.Count | Should -Be 48
+
+ foreach ($pageCfg in $result) {
+ $pageCfg['inkDrawings'].Count | Should -Be 2
+ $pageCfg['inkDrawings'][0]['x'] | Should -Be 74.25
+ $pageCfg['inkDrawings'][0]['y'] | Should -Be 449.25
+ $pageCfg['inkDrawings'][1]['x'] | Should -Be 96.75
+ $pageCfg['inkDrawings'][1]['y'] | Should -Be 464.25
+ $pageCfg['inkDrawings'][0]['width'] | Should -Be 15
+ $pageCfg['inkDrawings'][0]['height'] | Should -Be 30
+ $pageCfg['inkDrawings'][0]['data'] | Should -Be 'ACoDC0gQRICArARFNUY1EQAAgD8fCREAAAAAAADwPwoLAg1SsxnAED4DQiU='
+ $pageCfg['inkDrawings'][1]['width'] | Should -Be 22.5
+ $pageCfg['inkDrawings'][1]['height'] | Should -Be 0
+ $pageCfg['inkDrawings'][1]['data'] | Should -Be 'ACoDC0gQRICArARFNUY1EQAAgD8fCREAAAAAAADwPwoLAg41wQiQEEAUQBQ='
+ # The page's thumbnail images, which are in paragraphs with subparagraphs. So their positions cannot be determined
+ $pageCfg['images'].Count | Should -Be 2
+ $pageCfg['images'][0]['width'] | Should -Be 19.5
+ $pageCfg['images'][0]['height'] | Should -Be 25.875
+ $pageCfg['images'][0]['x'] | Should -Be $null
+ $pageCfg['images'][1]['width'] | Should -Be 24
+ $pageCfg['images'][1]['height'] | Should -Be 27.75
+ $pageCfg['images'][1]['x'] | Should -Be $null
+ }
+ }
+
It "Should honor config keepspaces" {
$params['Config']['keepspaces']['value'] = 1
@@ -1528,6 +1583,399 @@ foo`r`nbar`r`nbaz
}
+Describe 'Get-DocxImages' -Tag 'Unit' {
+
+ Context 'Behavior' {
+
+ It "Gets the images of a .docx file in document order, with their file names and display sizes in points" {
+ $documentXml = @'
+
+
+
+
+ foo
+
+
+
+'@
+ $relationshipsXml = @'
+
+
+
+
+
+
+'@
+ $docx = Join-Path $TestDrive 'test.docx'
+ Add-Type -AssemblyName System.IO.Compression, System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::Open($docx, 'Create')
+ try {
+ foreach ($entry in @( @('word/document.xml', $documentXml), @('word/_rels/document.xml.rels', $relationshipsXml) )) {
+ $writer = New-Object System.IO.StreamWriter($zip.CreateEntry($entry[0]).Open())
+ try {
+ $writer.Write($entry[1])
+ }finally {
+ $writer.Dispose()
+ }
+ }
+ }finally {
+ $zip.Dispose()
+ }
+
+ $result = @( Get-DocxImages -Path $docx )
+
+ $result.Count | Should -Be 2
+ $result[0]['fileName'] | Should -Be 'image1.png'
+ $result[0]['width'] | Should -Be 75
+ $result[0]['height'] | Should -Be 100
+ $result[1]['fileName'] | Should -Be 'image2.png'
+ $result[1]['width'] | Should -Be 10
+ $result[1]['height'] | Should -Be 20
+ }
+
+ }
+
+}
+
+Describe 'Get-OneNotePageImages' -Tag 'Unit' {
+
+ Context 'Behavior' {
+
+ It "Gets the images of a page from top to bottom, with their sizes and positions" {
+ $pageXml = @'
+
+
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+
+
+ bar
+
+
+'@ -as [xml]
+
+ $result = @( Get-OneNotePageImages -PageXml $pageXml )
+
+ $result.Count | Should -Be 2
+ $result[0]['x'] | Should -Be 50
+ $result[0]['y'] | Should -Be 100
+ $result[0]['width'] | Should -Be 20
+ $result[0]['height'] | Should -Be 10
+ $result[1]['x'] | Should -Be 36
+ $result[1]['y'] | Should -Be 300
+ $result[1]['width'] | Should -Be 150
+ $result[1]['height'] | Should -Be 100
+ }
+
+ It "Determines the positions of images in an outline from the heights of the preceding or following images and empty paragraphs" -Skip:($env:OS -notmatch 'windows') {
+ Add-Type -AssemblyName PresentationCore, WindowsBase
+ $lineHeight = 12 * (New-Object System.Windows.Media.FontFamily('Arial')).LineSpacing
+ $textHeight = 20 # Unknown to Get-OneNotePageImages
+ $outlineHeight = 100 + $lineHeight + 50 + $textHeight + 40 + $lineHeight
+ $pageXml = @"
+
+
+
+
+
+
+
+
+ foo
+
+
+
+
+
+ bar
+
+
+
+
+
+ baz
+
+
+
+
+
+
+
+"@ -as [xml]
+
+ $result = @( Get-OneNotePageImages -PageXml $pageXml )
+
+ $result.Count | Should -Be 3
+ $result[0]['x'] | Should -Be 36
+ $result[0]['y'] | Should -Be 86.4
+ $result[1]['x'] | Should -Be 186 # Centered
+ [Math]::Round($result[1]['y'], 4) | Should -Be ([Math]::Round(86.4 + 100 + $lineHeight, 4)) # Preceded by an image and an empty paragraph
+ $result[2]['x'] | Should -Be 536 # Right-aligned
+ [Math]::Round($result[2]['y'], 4) | Should -Be ([Math]::Round(86.4 + $outlineHeight - $lineHeight - 40, 4)) # Preceded by text of unknown height, but followed by an empty paragraph
+ }
+
+ It "Does not determine the positions of images in an outline whose layout cannot be determined" {
+ $pageXml = @'
+
+
+
+
+
+
+
+ foo
+
+
+ bar
+
+
+
+
+
+
+
+
+
+
+
+ baz
+
+
+
+
+
+
+
+
+
+'@ -as [xml]
+
+ $result = @( Get-OneNotePageImages -PageXml $pageXml )
+
+ # The heights of the 1st outline's images do not add up to the outline's height. The 2nd outline's image is nested, and surrounded by text of unknown height
+ $result.Count | Should -Be 3
+ foreach ($imageCfg in $result) {
+ $imageCfg['x'] | Should -Be $null
+ $imageCfg['y'] | Should -Be $null
+ }
+ $result[0]['height'] | Should -Be 100
+ $result[1]['height'] | Should -Be 50
+ $result[2]['height'] | Should -Be 100
+ }
+
+ }
+
+}
+
+Describe 'New-InkDrawingImage' -Tag 'Unit' {
+
+ Context 'Behavior' {
+
+ It "Renders the strokes of ink drawing(s) into a single .png image, and returns its size in points" -Skip:($env:OS -notmatch 'windows') {
+ $destination = Join-Path $TestDrive 'ink.png'
+ $inkDrawings = @(
+ [ordered]@{ width = 15; height = 30; data = 'ACoDC0gQRICArARFNUY1EQAAgD8fCREAAAAAAADwPwoLAg1SsxnAED4DQiU=' }
+ [ordered]@{ width = 22.5; height = 0; data = 'ACoDC0gQRICArARFNUY1EQAAgD8fCREAAAAAAADwPwoLAg41wQiQEEAUQBQ=' }
+ )
+
+ $result = New-InkDrawingImage -InkDrawing $inkDrawings -Destination $destination
+
+ # Both strokes, plus padding
+ [Math]::Round($result['width'], 1) | Should -Be 49.5
+ [Math]::Round($result['height'], 1) | Should -Be 34.5
+ $bytes = [System.IO.File]::ReadAllBytes($destination)
+ $bytes[1..3] | Should -Be @( 0x50, 0x4E, 0x47 ) # 'PNG'
+ }
+
+ It "Renders the strokes of ink drawing(s) on an image in the image's resolution, extending the image if strokes exceed it, and returns its size in points" -Skip:($env:OS -notmatch 'windows') {
+ Add-Type -AssemblyName PresentationCore, WindowsBase
+ function Get-PixelSize ($Path) {
+ $frame = [System.Windows.Media.Imaging.BitmapFrame]::Create( (New-Object System.IO.MemoryStream(,[System.IO.File]::ReadAllBytes($Path))), [System.Windows.Media.Imaging.BitmapCreateOptions]::None, [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad )
+ "$( $frame.PixelWidth )x$( $frame.PixelHeight )"
+ }
+ $inkDrawings = @(
+ [ordered]@{ width = 15; height = 30; data = 'ACoDC0gQRICArARFNUY1EQAAgD8fCREAAAAAAADwPwoLAg1SsxnAED4DQiU=' } # Bounds: 99,599 22x42 pixels
+ [ordered]@{ width = 22.5; height = 0; data = 'ACoDC0gQRICArARFNUY1EQAAgD8fCREAAAAAAADwPwoLAg41wQiQEEAUQBQ=' } # Bounds: 129,619 32x2 pixels
+ )
+ $background = Join-Path $TestDrive 'background.png'
+ $bitmap = New-Object System.Windows.Media.Imaging.RenderTargetBitmap(400, 200, 96, 96, [System.Windows.Media.PixelFormats]::Pbgra32)
+ $encoder = New-Object System.Windows.Media.Imaging.PngBitmapEncoder
+ $encoder.Frames.Add( [System.Windows.Media.Imaging.BitmapFrame]::Create($bitmap) )
+ $fileStream = [System.IO.File]::Create($background)
+ try {
+ $encoder.Save($fileStream)
+ }finally {
+ $fileStream.Dispose()
+ }
+
+ # Strokes within the image. The image is 200x100 pixels on the page, i.e. 2 image pixels per pixel
+ $destination = Join-Path $TestDrive 'image1.png'
+ Copy-Item $background $destination
+ $result = New-InkDrawingImage -InkDrawing $inkDrawings -Destination $destination -BackgroundImagePath $destination -BackgroundImageConfig @{ x = 60; y = 420; width = 150; height = 75 }
+
+ $result['width'] | Should -Be 150
+ $result['height'] | Should -Be 75
+ Get-PixelSize $destination | Should -Be '400x200'
+
+ # A stroke exceeds the image to the left by 21 pixels
+ $destination = Join-Path $TestDrive 'image2.png'
+ Copy-Item $background $destination
+ $result = New-InkDrawingImage -InkDrawing $inkDrawings -Destination $destination -BackgroundImagePath $destination -BackgroundImageConfig @{ x = 90; y = 420; width = 150; height = 75 }
+
+ [Math]::Round($result['width'], 2) | Should -Be 165.75
+ $result['height'] | Should -Be 75
+ Get-PixelSize $destination | Should -Be '442x200'
+ }
+
+ }
+
+}
+
+Describe 'Merge-InkDrawingStrokes' -Tag 'Unit' {
+
+ Context 'Behavior' {
+
+ BeforeEach {
+ function Get-Content { # Create this function for the sake of a "bug" in pester where calls to the Get-Content mock spits out a non-terminating pester error: A parameter cannot be found that matches parameter name 'Raw'
+ param (
+ [Parameter()]
+ [string]
+ $Path
+ ,
+ [Parameter()]
+ [string]
+ $LiteralPath
+ ,
+ [Parameter()]
+ [switch]
+ $Raw
+ )
+ ''
+ }
+ Mock Get-Content {
+ @'
+{width="1.0417in" height="1.3889in"}
+
+{width="0.3229in" height="0.5313in"}{width="0.4271in" height="0.1146in"}
+
+foo 
+'@
+ }
+ function Set-ContentNoBom {
+ param (
+ [Parameter()]
+ [string]
+ $LiteralPath
+ ,
+ [Parameter()]
+ [string]
+ $Value
+ )
+ }
+ Mock Set-ContentNoBom {}
+ Mock Remove-Item {}
+ Mock New-InkDrawingImage {
+ @{
+ width = 49.5
+ height = 34.5
+ }
+ }
+ Mock Get-DocxImages {
+ [ordered]@{ fileName = 'image1.png'; width = 75; height = 100 } # Not a stroke
+ [ordered]@{ fileName = 'image2.png'; width = 23.25; height = 38.25 } # 1st stroke
+ [ordered]@{ fileName = 'image3.png'; width = 30.75; height = 8.25 } # 2nd stroke
+ [ordered]@{ fileName = 'image4.png'; width = 23.25; height = 38.25 } # 3rd stroke
+ }
+ $pageCfg = [ordered]@{
+ docxExportFilePath = 'c:\temp\notes\docx\page.docx'
+ filePath = 'c:\temp\notes\page.md'
+ mediaPathPandoc = 'c:/temp/tmp/media'
+ inkDrawings = @(
+ [ordered]@{ width = 15; height = 30; data = 'foo' }
+ [ordered]@{ width = 22.5; height = 0; data = 'bar' }
+ [ordered]@{ width = 15; height = 30; data = 'baz' }
+ )
+ }
+ }
+
+ It "Merges adjacent stroke images into a single image, and keeps other images" {
+ $expectedContent = @'
+{width="1.0417in" height="1.3889in"}
+
+{width="0.6875in" height="0.4792in"}
+
+foo 
+'@
+
+ Merge-InkDrawingStrokes -PageConfig $pageCfg
+
+ Assert-MockCalled -CommandName New-InkDrawingImage -Times 2 -Scope It
+ Assert-MockCalled -CommandName New-InkDrawingImage -ParameterFilter { $InkDrawing.Count -eq 2 -and $InkDrawing[0]['data'] -eq 'foo' -and $InkDrawing[1]['data'] -eq 'bar' -and $Destination -match 'image2\.png$' } -Times 1 -Scope It
+ Assert-MockCalled -CommandName New-InkDrawingImage -ParameterFilter { $InkDrawing.Count -eq 1 -and $InkDrawing[0]['data'] -eq 'baz' -and $Destination -match 'image4\.png$' } -Times 1 -Scope It
+ Assert-MockCalled -CommandName Remove-Item -ParameterFilter { $LiteralPath -match 'image3\.png$' } -Times 1 -Scope It
+ Assert-MockCalled -CommandName Set-ContentNoBom -ParameterFilter { $Value -eq $expectedContent } -Times 1 -Scope It
+ }
+
+ It "Draws the strokes drawn on an image on that image, and merges the other strokes into a single image" {
+ Mock New-InkDrawingImage -ParameterFilter { $BackgroundImagePath } {
+ @{
+ width = 80 # Strokes exceed the image
+ height = 100
+ }
+ }
+ $pageCfg['images'] = @(
+ [ordered]@{ x = 36; y = 100; width = 75; height = 100 }
+ )
+ $pageCfg['inkDrawings'] = @(
+ [ordered]@{ x = 40; y = 110; width = 15; height = 30; data = 'foo' } # On the image
+ [ordered]@{ x = 300; y = 300; width = 22.5; height = 0; data = 'bar' }
+ [ordered]@{ x = 50; y = 150; width = 15; height = 30; data = 'baz' } # On the image
+ )
+ $expectedContent = @'
+{width="1.1111in" height="1.3889in"}
+
+{width="0.6875in" height="0.4792in"}
+
+foo
+'@ + ' '
+
+ Merge-InkDrawingStrokes -PageConfig $pageCfg
+
+ Assert-MockCalled -CommandName New-InkDrawingImage -Times 2 -Scope It
+ Assert-MockCalled -CommandName New-InkDrawingImage -ParameterFilter { $InkDrawing.Count -eq 2 -and $InkDrawing[0]['data'] -eq 'foo' -and $InkDrawing[1]['data'] -eq 'baz' -and $Destination -match 'image1\.png$' -and $BackgroundImagePath -match 'image1\.png$' -and $BackgroundImageConfig['x'] -eq 36 } -Times 1 -Scope It
+ Assert-MockCalled -CommandName New-InkDrawingImage -ParameterFilter { $InkDrawing.Count -eq 1 -and $InkDrawing[0]['data'] -eq 'bar' -and $Destination -match 'image3\.png$' -and !$BackgroundImagePath } -Times 1 -Scope It
+ Assert-MockCalled -CommandName Remove-Item -Times 2 -Scope It
+ Assert-MockCalled -CommandName Remove-Item -ParameterFilter { $LiteralPath -match 'image2\.png$' } -Times 1 -Scope It
+ Assert-MockCalled -CommandName Remove-Item -ParameterFilter { $LiteralPath -match 'image4\.png$' } -Times 1 -Scope It
+ Assert-MockCalled -CommandName Set-ContentNoBom -ParameterFilter { $Value -eq $expectedContent } -Times 1 -Scope It
+ }
+
+ It "Does nothing if no stroke images are identified" {
+ Mock Get-DocxImages {
+ [ordered]@{ fileName = 'image1.png'; width = 75; height = 100 }
+ }
+
+ Merge-InkDrawingStrokes -PageConfig $pageCfg
+
+ Assert-MockCalled -CommandName New-InkDrawingImage -Times 0 -Scope It
+ Assert-MockCalled -CommandName Set-ContentNoBom -Times 0 -Scope It
+ }
+
+ }
+
+}
+
Describe 'Convert-OneNotePage' -Tag 'Unit' {
function New-FakeSectionGroupConversionConfig {
function Get-OneNotePageContent {}
@@ -1762,6 +2210,29 @@ Describe 'Convert-OneNotePage' -Tag 'Unit' {
Assert-MockCalled -CommandName Move-Item -Times 1 -Scope It
}
+ It "Merges ink drawing strokes into single images" {
+ Mock Merge-InkDrawingStrokes {}
+
+ Convert-OneNotePage @params 6>$null
+
+ Assert-MockCalled -CommandName Merge-InkDrawingStrokes -Times 0 -Scope It
+
+ $params['ConversionConfig']['inkDrawings'] = @( [ordered]@{ width = 15; height = 30; data = 'foo' } )
+
+ Convert-OneNotePage @params 6>$null
+
+ Assert-MockCalled -CommandName Merge-InkDrawingStrokes -Times 1 -Scope It
+ }
+
+ It "Does not halt conversion if merging ink drawing strokes fails" {
+ Mock Merge-InkDrawingStrokes { throw 'foo' }
+ $params['ConversionConfig']['inkDrawings'] = @( [ordered]@{ width = 15; height = 30; data = 'foo' } )
+
+ $err = Convert-OneNotePage @params 6>$null 2>&1
+
+ $err.Exception.Message | Select-Object -First 1 | Should -match 'Failed to merge ink drawing strokes'
+ }
+
It "Does not halt conversion if renaming image(s) fails" {
Mock Move-Item { throw }
diff --git a/ConvertOneNote2MarkDown-v2.ps1 b/ConvertOneNote2MarkDown-v2.ps1
index 3668cba..74e3daf 100644
--- a/ConvertOneNote2MarkDown-v2.ps1
+++ b/ConvertOneNote2MarkDown-v2.ps1
@@ -117,6 +117,16 @@ Default: 32
Whether to store media in single or multiple folders
1: Images stored in single 'media' folder at Notebook-level - Default
2: Separate 'media' folder for each folder in the hierarchy
+'@
+ default = 1
+ value = 1
+ validateRange = 1,2
+ }
+ mergeInkStrokes = @{
+ description = @'
+Whether to merge the strokes of pen/hand written drawings (InkDrawing) into a single image that preserves the position of each stroke. Strokes drawn on an image are drawn on that image. Requires Windows
+1: Don't merge. Each stroke is converted to a separate image - Default
+2: Merge strokes into a single image
'@
default = 1
value = 1
@@ -628,6 +638,411 @@ Function Publish-OneNotePage {
$OneNoteConnection.Publish($PageId, $Destination, $PublishFormat, "")
}
+Function Get-DocxImages {
+ [CmdletBinding()]
+ param (
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [string]
+ $Path
+ )
+
+ # Get the images of a .docx file in document order, with their file names and display sizes in points
+ Add-Type -AssemblyName System.IO.Compression, System.IO.Compression.FileSystem
+ $zip = [System.IO.Compression.ZipFile]::OpenRead($Path)
+ try {
+ $entries = @{}
+ foreach ($name in 'word/document.xml', 'word/_rels/document.xml.rels') {
+ $reader = New-Object System.IO.StreamReader($zip.GetEntry($name).Open())
+ try {
+ $entries[$name] = [xml]$reader.ReadToEnd()
+ }finally {
+ $reader.Dispose()
+ }
+ }
+ }finally {
+ $zip.Dispose()
+ }
+
+ $relationshipsNs = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
+ $targets = @{}
+ foreach ($r in $entries['word/_rels/document.xml.rels'].DocumentElement.SelectNodes('*')) {
+ $targets[$r.GetAttribute('Id')] = $r.GetAttribute('Target')
+ }
+ $document = $entries['word/document.xml']
+ $ns = New-Object Xml.XmlNamespaceManager $document.NameTable
+ $ns.AddNamespace('wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing')
+ $ns.AddNamespace('a', 'http://schemas.openxmlformats.org/drawingml/2006/main')
+ foreach ($drawing in $document.SelectNodes('//wp:inline | //wp:anchor', $ns)) {
+ $extent = $drawing.SelectSingleNode('wp:extent', $ns)
+ $blip = $drawing.SelectSingleNode('.//a:blip', $ns)
+ if ($extent -and $blip -and $targets[$blip.GetAttribute('embed', $relationshipsNs)]) {
+ [ordered]@{
+ fileName = Split-Path $targets[$blip.GetAttribute('embed', $relationshipsNs)] -Leaf
+ width = [double]$extent.GetAttribute('cx') / 12700 # EMUs to points
+ height = [double]$extent.GetAttribute('cy') / 12700 # EMUs to points
+ }
+ }
+ }
+}
+
+Function Get-OneNotePageImages {
+ [CmdletBinding()]
+ param (
+ # Page content
+ [Parameter(Mandatory)]
+ [xml]
+ $PageXml
+ )
+
+ # Get the images of a page in the same order as in the page's .docx export (i.e. outlines and images from top to bottom), with their sizes in points.
+ # Also get their positions on the page in points, where they can be determined. Images in outlines have no position, so it is determined from the outline's position and the heights of the paragraphs preceding or following the image, if they are known (i.e. images and empty paragraphs). Otherwise, the position is $null
+ $ns = New-Object Xml.XmlNamespaceManager $PageXml.NameTable
+ $ns.AddNamespace("one", $PageXml.DocumentElement.NamespaceURI)
+ $quickStyles = @{}
+ foreach ($q in $PageXml.SelectNodes('/one:Page/one:QuickStyleDef', $ns)) {
+ $quickStyles[$q.index] = $q
+ }
+ $lineSpacings = @{}
+ $objects = @( $PageXml.SelectNodes('/one:Page/one:Outline[one:Position] | /one:Page/one:Image[one:Position and one:Size]', $ns) | Sort-Object { [double]$_.Position.y } )
+ foreach ($o in $objects) {
+ if ($o.LocalName -eq 'Image') {
+ [ordered]@{
+ x = [double]$o.Position.x
+ y = [double]$o.Position.y
+ width = [double]$o.Size.width
+ height = [double]$o.Size.height
+ }
+ continue
+ }
+
+ # Determine the heights of the outline's paragraphs. -1 if unknown
+ $paragraphs = @( $o.SelectNodes('one:OEChildren/one:OE', $ns) )
+ $heights = New-Object double[] $paragraphs.Count
+ for ($i = 0; $i -lt $paragraphs.Count; $i++) {
+ $p = $paragraphs[$i]
+ $children = @( $p.SelectNodes('*') )
+ $heights[$i] = -1
+ if ($children.Count -ne 1 -or $p.HasAttribute('style') -or $p.HasAttribute('spaceBefore') -or $p.HasAttribute('spaceAfter') -or $p.HasAttribute('spaceBetween')) {
+ continue
+ }
+ if ($children[0].LocalName -eq 'Image' -and $children[0].SelectSingleNode('one:Size', $ns)) {
+ $heights[$i] = [double]$children[0].Size.height
+ }elseif ($children[0].LocalName -eq 'T' -and $children[0].InnerText.Trim() -eq '' -and $quickStyles.ContainsKey($p.GetAttribute('quickStyleIndex'))) {
+ # Empty paragraph, i.e. a single line in the paragraph's font
+ $q = $quickStyles[$p.GetAttribute('quickStyleIndex')]
+ if (!$lineSpacings.ContainsKey($q.font)) {
+ $lineSpacings[$q.font] = try {
+ Add-Type -AssemblyName PresentationCore, WindowsBase -ErrorAction Stop # Not available on non-Windows
+ $glyphTypeface = $null
+ if ((New-Object System.Windows.Media.Typeface($q.font) -ErrorAction Stop).TryGetGlyphTypeface([ref]$glyphTypeface)) {
+ (New-Object System.Windows.Media.FontFamily($q.font) -ErrorAction Stop).LineSpacing
+ }else {
+ -1 # Font is not installed
+ }
+ }catch {
+ -1
+ }
+ }
+ if ($lineSpacings[$q.font] -gt 0) {
+ $heights[$i] = [double]$q.fontSize * $lineSpacings[$q.font] + [double]$q.spaceBefore + [double]$q.spaceAfter
+ }
+ }
+ }
+
+ # Determine the top of the paragraphs, from the top of the outline downwards, and from the bottom of the outline upwards
+ $tops = @{}
+ $top = [double]$o.Position.y
+ for ($i = 0; $i -lt $paragraphs.Count; $i++) {
+ $tops[$i] = $top
+ if ($heights[$i] -lt 0) {
+ break
+ }
+ $top += $heights[$i]
+ }
+ $size = $o.SelectSingleNode('one:Size', $ns)
+ if ($size) {
+ $bottom = [double]$o.Position.y + [double]$size.height
+ for ($i = $paragraphs.Count - 1; $i -ge 0; $i--) {
+ if ($heights[$i] -lt 0) {
+ break
+ }
+ $bottom -= $heights[$i]
+ $tops[$i] = $bottom
+ }
+ # Validate: The known heights cannot exceed the outline's height, and must add up to the outline's height if all of them are known
+ $knownHeights = ( $heights | Where-Object { $_ -ge 0 } | Measure-Object -Sum ).Sum
+ if ($knownHeights -gt [double]$size.height + 1 -or (@( $heights | Where-Object { $_ -lt 0 } ).Count -eq 0 -and [Math]::Abs($knownHeights - [double]$size.height) -gt 1)) {
+ "Could not determine the layout of an outline. Expected height: $( $size.height ), calculated height: $knownHeights" | Write-Verbose
+ $tops = @{}
+ }
+ }
+
+ # Get the images in document order. Only images in the outline's top level paragraphs have a determinable position
+ foreach ($image in $o.SelectNodes('.//one:Image[one:Size]', $ns)) {
+ $imageCfg = [ordered]@{
+ x = $null
+ y = $null
+ width = [double]$image.Size.width
+ height = [double]$image.Size.height
+ }
+ $i = [array]::IndexOf($paragraphs, $image.ParentNode)
+ if ($i -ge 0 -and $tops.ContainsKey($i) -and $size) {
+ $imageCfg['x'] = switch ($image.ParentNode.GetAttribute('alignment')) {
+ 'center' { [double]$o.Position.x + ([double]$size.width - $imageCfg['width']) / 2 }
+ 'right' { [double]$o.Position.x + [double]$size.width - $imageCfg['width'] }
+ default { [double]$o.Position.x }
+ }
+ $imageCfg['y'] = $tops[$i]
+ }
+ $imageCfg
+ }
+ }
+}
+
+Function New-InkDrawingImage {
+ [CmdletBinding()]
+ param (
+ # Ink drawing configuration objects
+ [Parameter(Mandatory)]
+ [object[]]
+ $InkDrawing
+ ,
+ [Parameter(Mandatory)]
+ [ValidateNotNullOrEmpty()]
+ [string]
+ $Destination
+ ,
+ # A .png or .jpg image that was drawn on. The strokes are rendered on top of it
+ [Parameter()]
+ [string]
+ $BackgroundImagePath
+ ,
+ # Position and size of the image that was drawn on, in points
+ [Parameter()]
+ [object]
+ $BackgroundImageConfig
+ )
+
+ # Render the strokes of the ink drawing(s) into a single image, preserving the position of each stroke. Returns the size of the image in points
+ Add-Type -AssemblyName PresentationCore, WindowsBase
+ $strokes = New-Object System.Windows.Ink.StrokeCollection
+ foreach ($i in $InkDrawing) {
+ $stream = New-Object System.IO.MemoryStream(,[Convert]::FromBase64String($i['data']))
+ $strokes.Add( (New-Object System.Windows.Ink.StrokeCollection -ArgumentList (,$stream)) ) # Stroke coordinates are relative to the page, in device-independent pixels (1/96 inch)
+ }
+ if ($strokes.Count -eq 0) {
+ throw "No strokes found in ink drawing(s)"
+ }
+ $bounds = $strokes.GetBounds()
+ if ($BackgroundImagePath) {
+ # Keep the resolution of the image. Extend the image if any stroke exceeds it
+ $background = [System.Windows.Media.Imaging.BitmapFrame]::Create( (New-Object System.IO.MemoryStream(,[System.IO.File]::ReadAllBytes($BackgroundImagePath))), [System.Windows.Media.Imaging.BitmapCreateOptions]::None, [System.Windows.Media.Imaging.BitmapCacheOption]::OnLoad )
+ $backgroundRect = New-Object System.Windows.Rect(($BackgroundImageConfig['x'] / 0.75), ($BackgroundImageConfig['y'] / 0.75), ($BackgroundImageConfig['width'] / 0.75), ($BackgroundImageConfig['height'] / 0.75)) # Points to pixels
+ $scaleX = $background.PixelWidth / $backgroundRect.Width
+ $scaleY = $background.PixelHeight / $backgroundRect.Height
+ $bounds = [System.Windows.Rect]::Union($bounds, $backgroundRect)
+ # Align the extension to whole pixels of the image, so that the image is not resampled
+ $left = $backgroundRect.Left - [Math]::Ceiling(($backgroundRect.Left - $bounds.Left) * $scaleX) / $scaleX
+ $top = $backgroundRect.Top - [Math]::Ceiling(($backgroundRect.Top - $bounds.Top) * $scaleY) / $scaleY
+ $right = $backgroundRect.Right + [Math]::Ceiling(($bounds.Right - $backgroundRect.Right) * $scaleX) / $scaleX
+ $bottom = $backgroundRect.Bottom + [Math]::Ceiling(($bounds.Bottom - $backgroundRect.Bottom) * $scaleY) / $scaleY
+ $bounds = New-Object System.Windows.Rect($left, $top, ($right - $left), ($bottom - $top))
+ }else {
+ $padding = 2
+ $bounds = New-Object System.Windows.Rect(($bounds.X - $padding), ($bounds.Y - $padding), ($bounds.Width + 2 * $padding), ($bounds.Height + 2 * $padding))
+ $scaleX = $scaleY = 2 # Render at 192 DPI so that the image stays sharp
+ }
+ $visual = New-Object System.Windows.Media.DrawingVisual
+ $context = $visual.RenderOpen()
+ $context.PushTransform( (New-Object System.Windows.Media.MatrixTransform($scaleX, 0, 0, $scaleY, (-$bounds.X * $scaleX), (-$bounds.Y * $scaleY))) )
+ $context.DrawRectangle([System.Windows.Media.Brushes]::White, $null, $bounds)
+ if ($BackgroundImagePath) {
+ $context.DrawImage($background, $backgroundRect)
+ }
+ $strokes.Draw($context)
+ $context.Pop()
+ $context.Close()
+ $bitmap = New-Object System.Windows.Media.Imaging.RenderTargetBitmap([int][Math]::Round($bounds.Width * $scaleX), [int][Math]::Round($bounds.Height * $scaleY), 96, 96, [System.Windows.Media.PixelFormats]::Pbgra32)
+ $bitmap.Render($visual)
+ $encoder = if ($Destination -match '\.jpe?g$') {
+ New-Object System.Windows.Media.Imaging.JpegBitmapEncoder -Property @{ QualityLevel = 95 }
+ }else {
+ New-Object System.Windows.Media.Imaging.PngBitmapEncoder
+ }
+ $encoder.Frames.Add( [System.Windows.Media.Imaging.BitmapFrame]::Create($bitmap) )
+ $fileStream = [System.IO.File]::Create($Destination)
+ try {
+ $encoder.Save($fileStream)
+ }finally {
+ $fileStream.Dispose()
+ }
+
+ @{
+ width = $bounds.Width * 0.75 # Pixels to points
+ height = $bounds.Height * 0.75 # Pixels to points
+ }
+}
+
+Function Merge-InkDrawingStrokes {
+ [CmdletBinding()]
+ param (
+ # Page conversion configuration object
+ [Parameter(Mandatory)]
+ [object]
+ $PageConfig
+ )
+
+ $pageCfg = $PageConfig
+ $docxImages = @( Get-DocxImages -Path $pageCfg['docxExportFilePath'] )
+
+ # Identify the stroke images of the .docx. Each ink drawing is exported as a separate image, in the same order as in the page, sized as the ink drawing plus a padding of about 8.25 points
+ $inkDrawingImages = @{}
+ $i = 0
+ foreach ($image in $docxImages) {
+ if ($i -lt $pageCfg['inkDrawings'].Count) {
+ $inkDrawingCfg = $pageCfg['inkDrawings'][$i]
+ if ([Math]::Abs($image['width'] - $inkDrawingCfg['width'] - 8.25) -le 1.5 -and [Math]::Abs($image['height'] - $inkDrawingCfg['height'] - 8.25) -le 1.5) {
+ $inkDrawingImages[$image['fileName']] = $inkDrawingCfg
+ $i++
+ }
+ }
+ }
+ "Identified $( $inkDrawingImages.Count ) of $( $pageCfg['inkDrawings'].Count ) ink drawing stroke images" | Write-Verbose
+ if ($inkDrawingImages.Count -eq 0) {
+ return
+ }
+
+ # Identify the other images of the .docx. They are exported in the same order as in the page, with the same size
+ $images = @{}
+ $pageImages = @( $pageCfg['images'] | Where-Object { $null -ne $_ } )
+ $i = 0
+ foreach ($image in $docxImages) {
+ if ($i -lt $pageImages.Count -and !$inkDrawingImages.ContainsKey($image['fileName'])) {
+ $imageCfg = $pageImages[$i]
+ if ([Math]::Abs($image['width'] - $imageCfg['width']) -le 1.5 -and [Math]::Abs($image['height'] - $imageCfg['height']) -le 1.5) {
+ $images[$image['fileName']] = $imageCfg
+ $i++
+ }
+ }
+ }
+
+ $content = Get-Content -LiteralPath $pageCfg['filePath'] -Raw -ErrorAction Stop # Use -LiteralPath so that characters like '(', ')', '[', ']', '`', "'", '"' are supported. Or else we will get an error "Cannot find path 'xxx' because it does not exist"
+ if (!$content) {
+ return
+ }
+ $imageRegex = "!\[(?:\\.|[^\\\]])*\]\($( [regex]::Escape($pageCfg['mediaPathPandoc']) )/([^)\s]+)\)(\{[^}\r\n]*\})?"
+ $references = @( [regex]::Matches($content, $imageRegex) )
+ $referencedFileNames = @{}
+ foreach ($r in $references) {
+ $referencedFileNames[$r.Groups[1].Value] = $true
+ }
+
+ # Assign each stroke drawn on an image to that image, i.e. if the stroke's center is within an image with a known position. Only .png and .jpg images that are referenced in the markdown are supported
+ $drawnOnImages = @{}
+ $strokesOnImages = @{}
+ foreach ($image in $docxImages) {
+ if (!$inkDrawingImages.ContainsKey($image['fileName'])) {
+ continue
+ }
+ $inkDrawingCfg = $inkDrawingImages[$image['fileName']]
+ $centerX = $inkDrawingCfg['x'] + $inkDrawingCfg['width'] / 2
+ $centerY = $inkDrawingCfg['y'] + $inkDrawingCfg['height'] / 2
+ foreach ($imageFileName in $images.Keys) {
+ $imageCfg = $images[$imageFileName]
+ if ($null -ne $imageCfg['x'] -and $null -ne $imageCfg['y'] -and $imageFileName -match '\.(png|jpe?g)$' -and $referencedFileNames.ContainsKey($imageFileName) -and
+ $centerX -ge $imageCfg['x'] -and $centerX -le $imageCfg['x'] + $imageCfg['width'] -and $centerY -ge $imageCfg['y'] -and $centerY -le $imageCfg['y'] + $imageCfg['height']) {
+ if (!$drawnOnImages.ContainsKey($imageFileName)) {
+ $drawnOnImages[$imageFileName] = [System.Collections.ArrayList]@()
+ }
+ $drawnOnImages[$imageFileName].Add($image['fileName']) > $null
+ $strokesOnImages[$image['fileName']] = $true
+ break
+ }
+ }
+ }
+
+ # Render the strokes on the images they were drawn on. If that fails, the strokes are merged into a separate image instead
+ $drawnOnImageSizes = @{}
+ foreach ($imageFileName in $drawnOnImages.Keys) {
+ $imagePath = [io.path]::combine( $pageCfg['mediaPathPandoc'], $imageFileName )
+ try {
+ "Drawing $( $drawnOnImages[$imageFileName].Count ) strokes on image $imagePath" | Write-Verbose
+ $drawnOnImageSizes[$imageFileName] = New-InkDrawingImage -InkDrawing @( $drawnOnImages[$imageFileName] | ForEach-Object { $inkDrawingImages[$_] } ) -Destination $imagePath -BackgroundImagePath $imagePath -BackgroundImageConfig $images[$imageFileName]
+ }catch {
+ Write-Error "Failed to draw strokes on image $imagePath. Exception: $( $_.Exception.Message )" -ErrorAction Continue
+ foreach ($f in $drawnOnImages[$imageFileName]) {
+ $strokesOnImages.Remove($f)
+ }
+ }
+ }
+
+ # Group adjacent stroke image references, e.g. ''
+ $groups = [System.Collections.ArrayList]@()
+ $edits = [System.Collections.ArrayList]@()
+ $group = $null
+ foreach ($r in $references) {
+ $fileName = $r.Groups[1].Value
+ if ($inkDrawingImages.ContainsKey($fileName)) {
+ if (!$group -or $content.Substring($group['end'], $r.Index - $group['end']) -notmatch '^\s*$') {
+ $group = @{
+ start = $r.Index
+ fileNames = [System.Collections.ArrayList]@()
+ hasAttributes = $false
+ }
+ $groups.Add($group) > $null
+ }
+ $group['end'] = $r.Index + $r.Length
+ if (!$strokesOnImages.ContainsKey($fileName)) {
+ $group['fileNames'].Add($fileName) > $null
+ $group['hasAttributes'] = $group['hasAttributes'] -or $r.Groups[2].Success
+ }
+ }else {
+ $group = $null
+ if ($drawnOnImageSizes.ContainsKey($fileName) -and $r.Groups[2].Success) {
+ # Update the size of an image that was drawn on, if strokes exceed the image
+ $size = $drawnOnImageSizes[$fileName]
+ if ([Math]::Abs($size['width'] - $images[$fileName]['width']) -gt 0.5 -or [Math]::Abs($size['height'] - $images[$fileName]['height']) -gt 0.5) {
+ $edits.Add(@{
+ start = $r.Groups[2].Index
+ end = $r.Groups[2].Index + $r.Groups[2].Length
+ replacement = "{width=`"$( [Math]::Round($size['width'] / 72, 4) )in`" height=`"$( [Math]::Round($size['height'] / 72, 4) )in`"}" # Points to inches
+ }) > $null
+ }
+ }
+ }
+ }
+
+ # Replace each group with a single image of the strokes that were not drawn on an image, or remove the group if there are none
+ $fileNamesToRemove = @( $strokesOnImages.Keys )
+ foreach ($group in $groups) {
+ $replacement = ''
+ if ($group['fileNames'].Count -gt 0) {
+ $fileName = $group['fileNames'][0]
+ $size = New-InkDrawingImage -InkDrawing @( $group['fileNames'] | ForEach-Object { $inkDrawingImages[$_] } ) -Destination ([io.path]::combine( $pageCfg['mediaPathPandoc'], $fileName ))
+ $fileNamesToRemove += @( $group['fileNames'] | Select-Object -Skip 1 )
+ $replacement = "/$fileName)"
+ if ($group['hasAttributes']) {
+ $replacement += "{width=`"$( [Math]::Round($size['width'] / 72, 4) )in`" height=`"$( [Math]::Round($size['height'] / 72, 4) )in`"}" # Points to inches
+ }
+ }
+ $edits.Add(@{
+ start = $group['start']
+ end = $group['end']
+ replacement = $replacement
+ }) > $null
+ }
+
+ # Apply the edits. Begin from the last edit, so that the positions of preceding edits stay intact
+ foreach ($e in @( $edits | Sort-Object { $_.start } -Descending )) { # Use dot notation, since index notation fails in Sort-Object in Powershell 6 and 7
+ $content = $content.Substring(0, $e['start']) + $e['replacement'] + $content.Substring($e['end'])
+ }
+ foreach ($f in $fileNamesToRemove) {
+ Remove-Item -LiteralPath ([io.path]::combine( $pageCfg['mediaPathPandoc'], $f )) -Force -ErrorAction SilentlyContinue
+ }
+ Set-ContentNoBom -LiteralPath $pageCfg['filePath'] -Value $content -ErrorAction Stop # Use -LiteralPath so that characters like '(', ')', '[', ']', '`', "'", '"' are supported. Or else we will get an error "Cannot find path 'xxx' because it does not exist"
+}
+
Function New-SectionGroupConversionConfig {
[CmdletBinding()]
param (
@@ -868,13 +1283,12 @@ Function New-SectionGroupConversionConfig {
}else {
[io.path]::combine( $cfg['notesDocxDirectory'], "$( $pageCfg['pathFromRootCompat'] ).docx" )
}
+ $pagexml = Get-OneNotePageContent -OneNoteConnection $OneNoteConnection -PageId $pageCfg['object'].ID
+ $ns = new-object Xml.XmlNamespaceManager $pagexml.NameTable
+ $ns.AddNamespace("one", $pagexml.DocumentElement.NamespaceURI)
$pageCfg['insertedAttachments'] = @(
& {
- $pagexml = Get-OneNotePageContent -OneNoteConnection $OneNoteConnection -PageId $pageCfg['object'].ID
-
# Search recursively for all attachment(s). This includes attachments nested in tables etc.
- $ns = new-object Xml.XmlNamespaceManager $pagexml.NameTable
- $ns.AddNamespace("one", $pagexml.DocumentElement.NamespaceURI)
$insertedFiles = $pagexml.SelectNodes("//one:InsertedFile", $ns)
foreach ($i in $insertedFiles) {
$attachmentCfg = [ordered]@{}
@@ -888,6 +1302,27 @@ Function New-SectionGroupConversionConfig {
}
}
)
+ $pageCfg['inkDrawings'] = @(
+ if ($config['mergeInkStrokes']['value'] -eq 2) {
+ # Pen/hand written drawings at the page level. Each drawing is a single stroke, exported as a separate image in the .docx in this order
+ foreach ($i in $pagexml.SelectNodes("/one:Page/one:InkDrawing[one:Position and one:Size and one:Data]", $ns)) {
+ $inkDrawingCfg = [ordered]@{}
+ $inkDrawingCfg['x'] = [double]$i.Position.x # Points
+ $inkDrawingCfg['y'] = [double]$i.Position.y # Points
+ $inkDrawingCfg['width'] = [double]$i.Size.width # Points
+ $inkDrawingCfg['height'] = [double]$i.Size.height # Points
+ $inkDrawingCfg['data'] = $i.Data # Base64-encoded Ink Serialized Format (ISF)
+
+ $inkDrawingCfg
+ }
+ }
+ )
+ $pageCfg['images'] = @(
+ if ($pageCfg['inkDrawings'].Count -gt 0) {
+ # Images that may have been drawn on
+ Get-OneNotePageImages -PageXml $pagexml
+ }
+ )
$pageCfg['mutations'] = @(
# Markdown mutations. Each search and replace is done against a string containing the entire markdown content
@@ -1209,6 +1644,18 @@ Function Convert-OneNotePage {
}
}
+ # Merge the strokes of pen/hand written drawings into single images
+ if ($pageCfg['inkDrawings'].Count -gt 0) {
+ try {
+ "Merging the strokes of $( $pageCfg['inkDrawings'].Count ) ink drawing(s) into single images" | Write-Verbose
+ if (!$config['dryRun']['value']) {
+ Merge-InkDrawingStrokes -PageConfig $pageCfg
+ }
+ }catch {
+ Write-Error "Failed to merge ink drawing strokes of $( $pageCfg['filePathNormal'] ). Exception: $( $_.Exception.Message )" -ErrorAction Continue
+ }
+ }
+
# Cleanup Word files
if ($config['keepdocx']['value'] -eq 1) {
try {
diff --git a/README.md b/README.md
index c23006f..72ee5d7 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,7 @@ The powershell script `ConvertOneNote2MarkDown-v2.ps1` will utilize the OneNote
* [Download Onetastic here](https://getonetastic.com/download) and, once installed, use New `Macro > File > Import` to install the attached .xml macro file within Onetastic
1. Password protected sections should be unlocked before continuing, the Object Model does not have access to them if you don't
1. You should start by "flattening" all `InkDrawing` (i.e. pen/hand written elements) in your onennote pages. Because OneNote does not have this function you will have to take screenshots of your pages with pen/hand written notes and paste the resulting image and then remove the scriblings. If you are a heavy pen user this is a very cumbersome.
+ * Alternatively, use the config option `$mergeInkStrokes = 2` to merge the strokes of pen/hand written drawings at the page level into a single image that preserves the position of each stroke. Otherwise, each stroke is converted to a separate image. Strokes drawn on an image (e.g. a pasted slide) are drawn on that image, unless the image is between lines of text in the same note container, in which case its position on the page cannot be determined. Pen/hand writing within text is not merged. Requires Windows.
* Alternatively, if you are converting a notebook only for reading sake, and want to preserve all notes layout, instead of flattening all `InkDrawing` manually, you may prefer to export a `.pdf` which preserves the full apperance and layout of the original note (including `InkDrawing`). Simply use the config option `$exportPdf = 2` to export a `.pdf` alongisde the `.md` file.
1. While running the conversion OneNote will be unusable and it is recommended to walk away and have some coffee as the Object Model might be interrupted if you do anything else.
1. Linked file object in `.md` files are clickable in VSCode, but do not open in their associated program, you will have to open the files directly from the file system.
diff --git a/config.example.ps1 b/config.example.ps1
index 6077bdf..dfcaefb 100644
--- a/config.example.ps1
+++ b/config.example.ps1
@@ -49,6 +49,11 @@ $mdFileNameAndFolderNameMaxLength = 32
# 2: Separate 'media' folder for each folder in the hierarchy
$medialocation = 1
+# Whether to merge the strokes of pen/hand written drawings (InkDrawing) into a single image that preserves the position of each stroke. Strokes drawn on an image are drawn on that image. Requires Windows
+# 1: Don't merge. Each stroke is converted to a separate image - Default
+# 2: Merge strokes into a single image
+$mergeInkStrokes = 1
+
# Specify Pandoc output format and optional extension(s) in the format: <+extension><-extension>
# Use this to customize the markdown flavor
# Extension(s) must be supported by your pandoc version. See: https://pandoc.org/MANUAL.html#options