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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,30 @@ Table SHOULD be edited by advanced GUI applications, BUT converted to any other
- If you still need 0.0.9, look for an archived asset on [Releases](https://github.com/fanfeilong/exceltk/releases) (do not rely on README version matrices)

# Convert Excel to Json
chagne the `-t` option to `json`
change the `-t` option to `json`
- `exceltk.exe -t json -xls example.xls `
- JSON is emitted as an ExcelTk `TableDocument` (`marker=EXCELTK1`) so it can be imported back

# Convert Excel to TeX
change the `-t` option to `tex`
- `exceltk.exe -t tex -xls example.xls`
- using `-st n` option to split table into multitable
- using `-sn` option to adjust number, for example, `1234656` will be split into `1 2 3 4 5 6`, it the table width is too large, this is useful

# Format plugins (export / import)
Output formats are plugins (`md`, `json`, `tex`, `img`). Each plugin can **export** Excel/CSV and **import** back to `.xlsx`.

- Export: `exceltk -t <plugin> -xls file.xlsx [-sheet name] [-out prefix]`
- Import: `exceltk -t <plugin> -import file.ext [-out out.xlsx]`

Precise payloads use marker `EXCELTK1` (Markdown HTML comment, TeX `%` comment, JSON field, or PNG private chunk `tkXl`).

# Marked image plugin (`-t img`)
- Export renders a PNG preview and embeds the table payload in a private PNG chunk
- Import only accepts ExcelTk-marked PNGs; unmarked images are rejected
- `exceltk -t img -xls example.xlsx`
- `exceltk -t img -import exampleSheet1.png -out restored.xlsx`

# Download

Prebuilt binaries are published through **GitHub Releases** (CI builds self-contained packages for each RID on version tags `v*`).
Expand Down Expand Up @@ -105,6 +120,8 @@ CI: GitHub Actions (`.github/workflows/ci.yml`) builds on .NET 10 and runs `src/

Architecture note: binary/OpenXML reading is organized as **streaming packages** (emit a complete local format unit as soon as it is recognized) so a renderer can progress while data is still arriving — see [`docs/streaming-packages.md`](docs/streaming-packages.md).

Output formats are **plugins** with export/import — see [`docs/format-plugins.md`](docs/format-plugins.md).

## Build on MacOS
```bash
dotnet publish -r osx-x64 src/Exceltk/Exceltk.csproj -c Release
Expand Down
13 changes: 13 additions & 0 deletions README.zhcn.txt
Original file line number Diff line number Diff line change
Expand Up @@ -87,11 +87,24 @@ exceltk -t md -a r -xls example.xlsx

# 转换到Json
- `exceltk.exe -t json -xls example.xls `
- JSON 使用带 `EXCELTK1` 标记的 TableDocument,可再导入为 xlsx

# 转换到TeX
- `exceltk.exe -t tex -xls example.xls`
- 使用 `-st n` 拆分表格
- 使用 `-sn` 把数字拆分,例如`1234656` 会被拆成`1 2 3 4 5 6`, 如果表太大时有用

# 格式插件(导出 / 导入)
输出格式已插件化:`md` / `json` / `tex` / `img`,均支持 **export** 与 **import**(导回 `.xlsx`)。

- 导出:`exceltk -t <plugin> -xls file.xlsx [-sheet name] [-out prefix]`
- 导入:`exceltk -t <plugin> -import file.ext [-out out.xlsx]`

# 带标记的图片插件(`-t img`)
- 导出 PNG 预览,并在私有 chunk `tkXl` 中写入精确表格数据
- 导入只接受含 ExcelTk 标记的 PNG;无标记图片暂不支持
- `exceltk -t img -xls example.xlsx`
- `exceltk -t img -import exampleSheet1.png -out restored.xlsx`
# 在 Linux 上构建与使用

需要安装 [.NET 10 SDK](https://learn.microsoft.com/dotnet/core/install/linux)(LTS)。仓库根目录 `global.json` 会约束使用 .NET 10 SDK。
Expand Down
34 changes: 34 additions & 0 deletions docs/format-plugins.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Format plugins

ExcelTk conversion outputs are **format plugins**. Each plugin implements:

- **Export**: `DataSet` → one or more artifacts (text or binary)
- **Import**: artifact stream → `DataSet` → written as `.xlsx`

Built-in plugins:

| `-t` | Extension | Notes |
| --- | --- | --- |
| `md` | `.md` | Pipe / MultiMarkdown tables; embeds `<!-- EXCELTK1 … -->` for precise import |
| `json` | `.json` | `TableDocument` JSON with `marker=EXCELTK1` |
| `tex` | `.tex` | TeX `tabular`; embeds `% EXCELTK1 …` |
| `img` | `.png` | Preview image + private PNG chunk `tkXl`; **unmarked PNGs are rejected** |

## CLI

```bash
# export
exceltk -t md -xls book.xlsx
exceltk -t img -xls book.xlsx -sheet Sheet1

# import back to Excel
exceltk -t md -import bookSheet1.md -out restored.xlsx
exceltk -t img -import bookSheet1.png -out restored.xlsx
```

## Extending

1. Implement `Exceltk.Format.IFormatPlugin`
2. Register in `FormatRegistry.BuiltInPlugins()` (or call `FormatRegistry.Register` at startup)

Precise round-trip data uses `TableDocument` (`marker=EXCELTK1`). The image plugin stores `TableDocument.ToMarkedBytes()` inside the `tkXl` chunk so visual pixels are not the source of truth.
84 changes: 46 additions & 38 deletions src/Exceltk/Extension/Extension.cs
Original file line number Diff line number Diff line change
@@ -1,67 +1,75 @@
using System;
using System.Text;
using System.Collections.Generic;
using System.IO;
using System.Text;

#if !OS_WINDOWS
using System.Xml;
using System.IO;
#endif

using Exceltk.Format;
using Exceltk.Reader;

namespace Exceltk {
public class SimpleTable{
public string Name{get;set;}
public string Value{get;set;}
public class SimpleTable {
public string Name { get; set; }
public string Value { get; set; }
}

public static class Extension {
public static IEnumerable<SimpleTable> ToSimpleTable(this string xls,string ext){
if(ext=="md"){
return xls.ToMd();
}else if(ext=="json"){
return xls.ToJson();
} else if (ext == "tex") {
return xls.ToTex();
} else {
return null;
public static IEnumerable<SimpleTable> ToSimpleTable(this string xls, string ext) {
IFormatPlugin plugin = FormatRegistry.GetRequired(ext);
DataSet dataSet = WorkbookLoader.Load(xls);
foreach (FormatArtifact artifact in plugin.Export(dataSet, null)) {
if (artifact.IsBinary) {
continue;
}
yield return new SimpleTable {
Name = artifact.Name,
Value = artifact.TextContent
};
}
}

public static SimpleTable ToSimpleTable(this string xls,string sheet,string ext){
if(ext=="md"){
return xls.ToMd(sheet);
}else if(ext=="json"){
return xls.ToJson(sheet);
} else if (ext == "tex") {
return xls.ToTex(sheet);
} else {
return null;
public static SimpleTable ToSimpleTable(this string xls, string sheet, string ext) {
IFormatPlugin plugin = FormatRegistry.GetRequired(ext);
DataSet dataSet = WorkbookLoader.Load(xls);
foreach (FormatArtifact artifact in plugin.Export(dataSet, sheet)) {
if (artifact.IsBinary) {
continue;
}
return new SimpleTable {
Name = artifact.Name,
Value = artifact.TextContent
};
}
throw new ArgumentException("Sheet not found: " + sheet);
}

#if !OS_WINDOWS
public static void Close(this XmlReader xmlReader) {
xmlReader.Dispose();
}
public static void Close(this Stream stream){
public static void Close(this Stream stream) {
stream.Dispose();
}
public static void Close(this MemoryStream stream){
public static void Close(this MemoryStream stream) {
stream.Dispose();
}
public static void Close(this BinaryReader stream){
public static void Close(this BinaryReader stream) {
stream.Dispose();
}
#endif

public static string GetUserName(){
public static string GetUserName() {
#if OS_WINDOWS
return Environment.UserName;
#else
return "";
#endif
}

public static Encoding DefaultEncoding(){
public static Encoding DefaultEncoding() {
#if OS_WINDOWS
return Encoding.Default;
#else
Expand All @@ -70,22 +78,22 @@ public static Encoding DefaultEncoding(){
}

public static bool IsHexDigit(this char digit) {
return (('0'<=digit&&digit<='9')||
('a'<=digit&&digit<='f')||
('A'<=digit&&digit<='F'));
return (('0' <= digit && digit <= '9') ||
('a' <= digit && digit <= 'f') ||
('A' <= digit && digit <= 'F'));
}
public static int FromHex(this char digit) {
if ('0'<=digit&&digit<='9') {
return (int)(digit-'0');
if ('0' <= digit && digit <= '9') {
return (int)(digit - '0');
}

if ('a'<=digit&&digit<='f')
return (int)(digit-'a'+10);
if ('a' <= digit && digit <= 'f')
return (int)(digit - 'a' + 10);

if ('A'<=digit&&digit<='F')
return (int)(digit-'A'+10);
if ('A' <= digit && digit <= 'F')
return (int)(digit - 'A' + 10);

throw new ArgumentException("digit");
}
}
}
}
41 changes: 41 additions & 0 deletions src/Exceltk/Format/FormatArtifact.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
using System;

namespace Exceltk.Format {
/// <summary>
/// One exported sheet/file produced by a format plugin.
/// </summary>
public sealed class FormatArtifact {
public string Name { get; set; }
public string Extension { get; set; }
public bool IsBinary { get; set; }
public string TextContent { get; set; }
public byte[] BinaryContent { get; set; }

public static FormatArtifact Text(string name, string extension, string content) {
return new FormatArtifact {
Name = name ?? "",
Extension = NormalizeExtension(extension),
IsBinary = false,
TextContent = content ?? ""
};
}

public static FormatArtifact Binary(string name, string extension, byte[] content) {
return new FormatArtifact {
Name = name ?? "",
Extension = NormalizeExtension(extension),
IsBinary = true,
BinaryContent = content ?? Array.Empty<byte>()
};
}

private static string NormalizeExtension(string extension) {
if (string.IsNullOrEmpty(extension)) {
return "";
}
return extension.StartsWith(".", StringComparison.Ordinal)
? extension.Substring(1)
: extension;
}
}
}
71 changes: 71 additions & 0 deletions src/Exceltk/Format/FormatRegistry.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Collections.Generic;
using System.Linq;

using Exceltk.Format.Plugins;

namespace Exceltk.Format {
/// <summary>
/// Discovers and resolves format plugins by CLI target name.
/// </summary>
public static class FormatRegistry {
private static readonly object Gate = new object();
private static Dictionary<string, IFormatPlugin> _plugins;

public static IEnumerable<IFormatPlugin> All {
get {
EnsureRegistered();
return _plugins.Values.OrderBy(p => p.Name, StringComparer.Ordinal);
}
}

public static bool TryGet(string name, out IFormatPlugin plugin) {
EnsureRegistered();
if (string.IsNullOrEmpty(name)) {
plugin = null;
return false;
}
return _plugins.TryGetValue(name.Trim().ToLowerInvariant(), out plugin);
}

public static IFormatPlugin GetRequired(string name) {
if (!TryGet(name, out IFormatPlugin plugin)) {
throw new ArgumentException("Unknown format plugin: " + name);
}
return plugin;
}

public static void Register(IFormatPlugin plugin) {
if (plugin == null) {
throw new ArgumentNullException(nameof(plugin));
}
EnsureRegistered();
lock (Gate) {
_plugins[plugin.Name.ToLowerInvariant()] = plugin;
}
}

private static void EnsureRegistered() {
if (_plugins != null) {
return;
}
lock (Gate) {
if (_plugins != null) {
return;
}
var map = new Dictionary<string, IFormatPlugin>(StringComparer.OrdinalIgnoreCase);
foreach (IFormatPlugin plugin in BuiltInPlugins()) {
map[plugin.Name.ToLowerInvariant()] = plugin;
}
_plugins = map;
}
}

private static IEnumerable<IFormatPlugin> BuiltInPlugins() {
yield return new MarkdownFormatPlugin();
yield return new JsonFormatPlugin();
yield return new TexFormatPlugin();
yield return new MarkedImageFormatPlugin();
}
}
}
27 changes: 27 additions & 0 deletions src/Exceltk/Format/IFormatPlugin.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
using System.Collections.Generic;
using System.IO;

using Exceltk.Reader;

namespace Exceltk.Format {
/// <summary>
/// Pluggable conversion format. Every format supports export (Excel → format)
/// and import (format → Excel workbook DataSet).
/// </summary>
public interface IFormatPlugin {
/// <summary>CLI target id, e.g. md / json / tex / img.</summary>
string Name { get; }

/// <summary>Default file extension without dot.</summary>
string FileExtension { get; }

/// <summary>Human-readable summary for help text.</summary>
string Description { get; }

bool SupportsExport { get; }
bool SupportsImport { get; }

IEnumerable<FormatArtifact> Export(DataSet dataSet, string sheetFilter);
DataSet Import(Stream input, string sourcePath);
}
}
Loading
Loading