Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,6 @@ packaging/debian/DEBIAN/* text eol=lf
# Preserve upstream-distributed vendor runtime contents verbatim.
src/assets/mathlive/vendor/mathlive.min.mjs -whitespace
src/assets/mathlive/vendor/mathlive.LICENSE.txt -whitespace
src/assets/MathJax/** -text -whitespace
tools/mathjax/liteDOM.js -text -whitespace
tools/mathjax/office.props -text
2 changes: 2 additions & 0 deletions benchmarks/mathcraft_ocr/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# MathCraft OCR Benchmark

All commands below run from the repository root. Benchmark data defaults to the sibling `../MathCraftBenchData` directory; use `-DataRoot` (PowerShell) or `--data-root` where supported to override it, and adjust explicit input/output paths accordingly. CDM requires its dependencies in the selected Python environment and Ghostscript/ImageMagick on `PATH`.

This benchmark suite runs MathCraft OCR only. It does not install, import, or execute third-party OCR models.

## Scope
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/mathcraft_ocr/REPRODUCE_MATHWRITING.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Reproduce MathWriting Test

All commands below run from the repository root. Benchmark data defaults to the sibling `../MathCraftBenchData` directory; use `-DataRoot` (PowerShell) or `--data-root` where supported to override it, and adjust explicit input/output paths accordingly. CDM requires its dependencies in the selected Python environment and Ghostscript/ImageMagick on `PATH`.

MathWriting is used as an independent handwritten formula benchmark. The
protocol is fixed to the public test split, human samples only, offline raster
images, and the dataset-provided normalized LaTeX labels as ground truth.
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/mathcraft_ocr/REPRODUCE_OPENSTAX.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Reproducing OpenStax Mixed-Page Results

All commands below run from the repository root. Benchmark data defaults to the sibling `../MathCraftBenchData` directory; use `-DataRoot` (PowerShell) or `--data-root` where supported to override it, and adjust explicit input/output paths accordingly. CDM requires its dependencies in the selected Python environment and Ghostscript/ImageMagick on `PATH`.

OpenStax pages are used as a public mixed mathematical document benchmark for
page-level OCR behavior, layout reconstruction, runtime, and qualitative
evidence. They do not provide formula-level ground truth labels.
Expand Down
2 changes: 2 additions & 0 deletions benchmarks/mathcraft_ocr/REPRODUCE_UNIMER.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Reproducing UniMER-Test Results

All commands below run from the repository root. Benchmark data defaults to the sibling `../MathCraftBenchData` directory; use `-DataRoot` (PowerShell) or `--data-root` where supported to override it, and adjust explicit input/output paths accordingly. CDM requires its dependencies in the selected Python environment and Ghostscript/ImageMagick on `PATH`.

This document records the MathCraft OCR UniMER-Test experiment used for the
paper tables. Large datasets and run outputs are kept outside the repository.

Expand Down
2 changes: 2 additions & 0 deletions docs/mathcraft_ocr_literature_data_inventory.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# MathCraft OCR Literature and Public Data Inventory

All commands below run from the repository root. Benchmark data defaults to the sibling `../MathCraftBenchData` directory; use `-DataRoot` (PowerShell) or `--data-root` where supported to override it, and adjust explicit input/output paths accordingly. CDM requires its dependencies in the selected Python environment and Ghostscript/ImageMagick on `PATH`.

Reviewed: 2026-08-25

This inventory records the paper evidence that is already reproducible, the
Expand Down
138 changes: 45 additions & 93 deletions docs/office_plugin_formula_workflows.md

Large diffs are not rendered by default.

553 changes: 553 additions & 0 deletions docs/office_plugin_typography_refactor.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions office_plugin/LaTeXSnipper.OfficePlugin.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
<Project Path="hosts/WordAddIn/LaTeXSnipper.OfficePlugin.WordAddIn.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/LaTeXSnipper.OfficePlugin.PowerPointE2E/LaTeXSnipper.OfficePlugin.PowerPointE2E.csproj" />
<Project Path="tests/LaTeXSnipper.OfficePlugin.Typography.Tests/LaTeXSnipper.OfficePlugin.Typography.Tests.csproj" />
<Project Path="tests/LaTeXSnipper.OfficePlugin.MetadataSafety.Tests/LaTeXSnipper.OfficePlugin.MetadataSafety.Tests.csproj" />
<Project Path="tests/LaTeXSnipper.OfficePlugin.WordParsingE2E/LaTeXSnipper.OfficePlugin.WordParsingE2E.csproj" />
</Folder>
Expand Down
2 changes: 2 additions & 0 deletions office_plugin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ Office 2016 is not officially supported (requires manual .NET 4.8 and WebView2 i

## Project Layout

Desktop and Office now share pinned MathJax 4.1.3 with independent resource profiles and asynchronous conversion. See [runtime maintenance and verification](../tools/mathjax/README.md). The [typography, editor, and preset implementation plan](../docs/office_plugin_typography_refactor.md) describes the next phase; the formula schema and typography settings have not changed in this runtime update.

| Path | Role |
|---|---|
| `src/LaTeXSnipper.OfficePlugin.Abstractions` | Stable contracts shared by hosts, renderer, editor, and automation client |
Expand Down
28 changes: 25 additions & 3 deletions office_plugin/hosts/OleFormulaObjectNative/src/Presentation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ double ExtractJsonNumber(const std::wstring& json, const std::wstring& propertyN

wchar_t* end = nullptr;
double value = wcstod(text.c_str(), &end);
return end == text.c_str() ? 0 : value;
return end == text.c_str() || *end != L'\0' || !std::isfinite(value) ? 0 : value;
}

int DecodeBase64Char(wchar_t ch)
Expand Down Expand Up @@ -210,13 +210,35 @@ void ApplyPayloadSize(const std::wstring& payloadJson, FormulaPresentation* pres

}

static bool HasSupportedTypography(const std::wstring& payloadJson)
{
const std::wstring symbol = ExtractJsonString(payloadJson, L"symbolFontId");
const std::wstring style = ExtractJsonString(payloadJson, L"defaultMathStyle");
const std::wstring styles[] = {L"Automatic", L"Upright", L"Bold", L"Italic", L"BoldItalic",
L"SansSerif", L"SansSerifBold", L"SansSerifItalic", L"SansSerifBoldItalic", L"Monospace",
L"Calligraphic", L"Script", L"Fraktur", L"BoldFraktur", L"Blackboard"};
bool supportedStyle = false;
for (const auto& candidate : styles) supportedStyle = supportedStyle || style == candidate;
const std::wstring color = ExtractJsonString(payloadJson, L"color");
if (color.size() != 7 || color[0] != L'#') return false;
for (size_t index = 1; index < color.size(); ++index)
if (!((color[index] >= L'0' && color[index] <= L'9') || (color[index] >= L'A' && color[index] <= L'F')))
return false;
return ExtractJsonString(payloadJson, L"typographyVersion") == L"1"
&& (symbol == L"mathjax-tex" || symbol == L"mathjax-stix2") && supportedStyle
&& payloadJson.find(L"\"numberFontFamily\"") != std::wstring::npos
&& !ExtractJsonString(payloadJson, L"cjkFontFamily").empty()
&& ExtractJsonNumber(payloadJson, L"fontSizePoints") >= 1
&& ExtractJsonNumber(payloadJson, L"fontSizePoints") <= 1638;
}

static bool HasRequiredFormulaPayloadFields(const std::wstring& payloadJson)
{
std::wstring presentationPayload = ExtractJsonString(payloadJson, L"presentationPayloadBase64");
return !ExtractJsonString(payloadJson, L"latex").empty()
&& !ExtractJsonString(payloadJson, L"displayMode").empty()
&& !ExtractJsonString(payloadJson, L"numberingMode").empty()
&& !ExtractJsonString(payloadJson, L"fontScale").empty()
&& HasSupportedTypography(payloadJson)
&& !ExtractJsonString(payloadJson, L"renderEngine").empty()
&& !ExtractJsonString(payloadJson, L"rendererVersion").empty()
&& ExtractJsonNumber(payloadJson, L"widthPoints") > 0
Expand All @@ -233,7 +255,7 @@ bool IsSupportedFormulaPayload(const std::wstring& payloadJson)
return false;
}

return ExtractJsonString(payloadJson, L"schemaVersion") == L"2"
return ExtractJsonString(payloadJson, L"schemaVersion") == L"3"
&& payloadJson.find(L"\"documentId\"") == std::wstring::npos
&& payloadJson.find(L"\"equationId\"") == std::wstring::npos;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ public sealed class DynamicPowerPointApplicationAdapter : IPowerPointApplication
private const int MsoTrue = -1;
private const float DefaultLeftPoints = 72f;
private const float DefaultTopPoints = 96f;
private const float FormulaSizeTolerancePoints = 0.1f;
private const string OleFormulaProgId = "LaTeXSnipper.Formula";

private readonly dynamic _application;
Expand All @@ -26,6 +25,19 @@ public DynamicPowerPointApplicationAdapter(object application)
_application = application ?? throw new ArgumentNullException(nameof(application));
}

public double GetCurrentFontSizePoints()
{
try
{
double points = Convert.ToDouble(_application.ActiveWindow.Selection.TextRange.Font.Size);
return FormulaFontSize.IsValid(points) ? points : 0;
}
catch (Exception error) when (error is COMException || error is Microsoft.CSharp.RuntimeBinder.RuntimeBinderException)
{
return 0;
}
}

public Task ActivateForEditingAsync(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
Expand Down Expand Up @@ -261,13 +273,13 @@ public Task<PowerPointFormulaEditTarget> LoadSelectedFormulaAsync(CancellationTo
presentation));
}

public Task<IReadOnlyList<PowerPointFormulaEntry>> LoadSelectedFormulaEntriesAsync(CancellationToken cancellationToken)
public Task<IReadOnlyList<PowerPointFormulaEntry>> LoadFormulaEntriesAsync(bool all, CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var entries = new List<PowerPointFormulaEntry>();
dynamic presentation = _application.ActivePresentation;
string documentId = PowerPointDocumentIdentityStore.GetOrCreate(presentation);
IReadOnlyList<object> shapes = GetSelectedFormulaShapes();
IReadOnlyList<object> shapes = all ? GetAllFormulaShapes(cancellationToken) : GetSelectedFormulaShapes();
EnsureUniqueShapeIdentities(shapes);
foreach (object item in shapes)
{
Expand Down Expand Up @@ -307,43 +319,22 @@ public bool ContainsFormula(string equationId)
return false;
}

public Task<int> ResetCustomFormulaSizesAsync(CancellationToken cancellationToken)
private IReadOnlyList<object> GetAllFormulaShapes(CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
int resetCount = 0;
dynamic presentation = _application.ActivePresentation;
int slideCount = Convert.ToInt32(presentation.Slides.Count);
for (int slideIndex = 1; slideIndex <= slideCount; slideIndex++)
var formulas = new List<object>();
dynamic slides = _application.ActivePresentation.Slides;
for (int slideIndex = 1; slideIndex <= Convert.ToInt32(slides.Count); slideIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
dynamic shapes = presentation.Slides.Item(slideIndex).Shapes;
int shapeCount = Convert.ToInt32(shapes.Count);
for (int shapeIndex = 1; shapeIndex <= shapeCount; shapeIndex++)
dynamic shapes = slides.Item(slideIndex).Shapes;
for (int shapeIndex = 1; shapeIndex <= Convert.ToInt32(shapes.Count); shapeIndex++)
{
cancellationToken.ThrowIfCancellationRequested();
dynamic shape = shapes.Item(shapeIndex);
if (string.IsNullOrWhiteSpace(ReadTag(shape, PowerPointFormulaMetadataStore.EquationIdTag)))
{
continue;
}

float naturalWidth = ReadRequiredFloatTag(shape, PowerPointFormulaMetadataStore.NaturalWidthPointsTag);
float naturalHeight = ReadRequiredFloatTag(shape, PowerPointFormulaMetadataStore.NaturalHeightPointsTag);
float currentWidth = Convert.ToSingle(shape.Width, System.Globalization.CultureInfo.InvariantCulture);
float currentHeight = Convert.ToSingle(shape.Height, System.Globalization.CultureInfo.InvariantCulture);
if (Math.Abs(currentWidth - naturalWidth) <= FormulaSizeTolerancePoints
&& Math.Abs(currentHeight - naturalHeight) <= FormulaSizeTolerancePoints)
{
continue;
}

shape.Width = naturalWidth;
shape.Height = naturalHeight;
resetCount++;
if (!string.IsNullOrWhiteSpace(ReadTag(shape, PowerPointFormulaMetadataStore.EquationIdTag)))
formulas.Add(shape);
}
}

return Task.FromResult(resetCount);
return formulas;
}

public Task DeleteFormulaByIdAsync(string equationId, CancellationToken cancellationToken)
Expand Down Expand Up @@ -506,7 +497,7 @@ private static FormulaMetadata WithNewIdentity(FormulaMetadata metadata, string
metadata.NumberText,
metadata.RenderEngine,
metadata.SchemaVersion,
metadata.FontScale);
metadata.Typography);
}

private static PowerPointFormulaEntry CreateEntry(dynamic shape, int slideIndex, string documentId)
Expand Down
Loading