647 lines
24 KiB
C#
647 lines
24 KiB
C#
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Xml.Linq;
|
||
|
||
|
||
namespace PropresentorScript
|
||
{
|
||
#nullable enable
|
||
using System;
|
||
using System.Collections.Generic;
|
||
using System.IO;
|
||
using System.Linq;
|
||
using System.Text;
|
||
using System.Xml.Linq;
|
||
|
||
class Program
|
||
{
|
||
static void Main(string[] args)
|
||
{
|
||
// 用法:dotnet run "path/to/song.txt"
|
||
string inputText;
|
||
if (args.Length > 0 && File.Exists(args[0]))
|
||
{
|
||
inputText = File.ReadAllText(args[0]);
|
||
}
|
||
else
|
||
{
|
||
// 沒給檔案就用內建範例(可自行刪除)
|
||
inputText = @"向我神
|
||
To Our God
|
||
|
||
[Verse 1]
|
||
從塵土灰燼 祢愛贖回我
|
||
Up from the ashes Your love has brought us
|
||
脫離黑暗中 進入光明
|
||
Out of the darkness into the light;
|
||
挪去我憂傷
|
||
Lifting our sorrows
|
||
承受我重擔 醫治我心
|
||
Bearing our burdens healing our hearts;
|
||
|
||
|
||
[Chorus]
|
||
向我神 我揚聲歌唱
|
||
To our God we lift up one voice
|
||
向我神 我高舉雙手
|
||
To our God we lift up one song;
|
||
向我神 同心來宣揚
|
||
To our God we lift up one voice
|
||
高唱 哈利路亞
|
||
Singing Hallelujah;";
|
||
}
|
||
|
||
Song song = ParseSong(inputText);
|
||
|
||
XDocument pro6 = BuildPro6(song, 1920, 1080);
|
||
string safeTitle = MakeSafeFileName(song.TitleZh ?? song.Title);
|
||
string outputPath = Path.Combine(Directory.GetCurrentDirectory(), safeTitle + ".pro6");
|
||
pro6.Save(outputPath);
|
||
Console.WriteLine("Wrote: " + outputPath);
|
||
}
|
||
|
||
// ---------------- Models ----------------
|
||
|
||
class Section
|
||
{
|
||
public string Name { get; set; } = "";
|
||
// 每個元素為一張「原始」投影片的多行文字(以 \n 分隔);後續會再自動分頁
|
||
public List<string> Slides { get; set; } = new List<string>();
|
||
}
|
||
|
||
class Song
|
||
{
|
||
public string Title { get; set; } = "Untitled"; // 後備
|
||
public string? TitleZh { get; set; } // 中文歌名(若有)
|
||
public string? TitleEn { get; set; } // 英文歌名(若有)
|
||
public List<Section> Sections { get; set; } = new List<Section>();
|
||
|
||
public string FooterText(string sectionName)
|
||
{
|
||
string baseTitle = !string.IsNullOrWhiteSpace(TitleEn) && !string.IsNullOrWhiteSpace(TitleZh)
|
||
? (TitleZh + " / " + TitleEn)
|
||
: (TitleZh ?? Title);
|
||
return baseTitle + " - " + sectionName;
|
||
}
|
||
}
|
||
|
||
// ---------------- Parsing ----------------
|
||
|
||
// 規則:
|
||
// - 第一個非空行 = 標題1(可能是中或英)
|
||
// - 第二個非空行若不是 [Header],則當作標題2
|
||
// - [xxx] 開頭代表段落標頭;其後文字累積,遇到 ; 或 ; 就分成新投影片
|
||
static Song ParseSong(string raw)
|
||
{
|
||
string[] lines = raw.Replace("\r\n", "\n").Replace('\r', '\n').Split('\n');
|
||
int i = 0;
|
||
|
||
// 標題1
|
||
while (i < lines.Length && string.IsNullOrWhiteSpace(lines[i])) i++;
|
||
string title1 = (i < lines.Length) ? lines[i].Trim() : "Untitled";
|
||
i++;
|
||
|
||
// 標題2(若下一個非空行不是 header)
|
||
while (i < lines.Length && string.IsNullOrWhiteSpace(lines[i])) i++;
|
||
string? title2 = null;
|
||
if (i < lines.Length && !IsHeader(lines[i].Trim(), out _))
|
||
{
|
||
title2 = lines[i].Trim();
|
||
i++;
|
||
}
|
||
|
||
Song song = new Song();
|
||
if (ContainsCjk(title1))
|
||
{
|
||
song.TitleZh = title1;
|
||
song.Title = title1;
|
||
if (!string.IsNullOrWhiteSpace(title2)) song.TitleEn = title2;
|
||
}
|
||
else
|
||
{
|
||
song.Title = title1;
|
||
if (!string.IsNullOrWhiteSpace(title2))
|
||
{
|
||
if (ContainsCjk(title2)) song.TitleZh = title2;
|
||
else song.TitleEn = title2;
|
||
}
|
||
}
|
||
|
||
List<Section> sections = new List<Section>();
|
||
Section? current = null;
|
||
List<string> slideBuffer = new List<string>();
|
||
|
||
void FlushSlide()
|
||
{
|
||
if (current == null) return;
|
||
string joined = string.Join("\n", slideBuffer).Trim();
|
||
if (!string.IsNullOrWhiteSpace(joined))
|
||
current.Slides.Add(joined);
|
||
slideBuffer.Clear();
|
||
}
|
||
|
||
void FlushSection()
|
||
{
|
||
if (current == null) return;
|
||
if (slideBuffer.Count > 0) FlushSlide();
|
||
sections.Add(current);
|
||
current = null;
|
||
}
|
||
|
||
// 跳過 header 前的空白
|
||
while (i < lines.Length && string.IsNullOrWhiteSpace(lines[i])) i++;
|
||
|
||
for (; i < lines.Length; i++)
|
||
{
|
||
string rawLine = lines[i] ?? "";
|
||
string line = rawLine.TrimEnd();
|
||
|
||
string header;
|
||
if (IsHeader(line, out header))
|
||
{
|
||
FlushSection();
|
||
current = new Section { Name = header };
|
||
slideBuffer.Clear();
|
||
continue;
|
||
}
|
||
|
||
if (current == null) continue; // 忽略 header 之前的散行
|
||
|
||
// 支援多個分號;每遇到 ; 或 ; 分頁
|
||
string remaining = line;
|
||
while (true)
|
||
{
|
||
int idx = IndexOfBreak(remaining); // ; 或 ;
|
||
if (idx == -1)
|
||
{
|
||
if (!string.IsNullOrWhiteSpace(remaining))
|
||
slideBuffer.Add(remaining);
|
||
break;
|
||
}
|
||
else
|
||
{
|
||
string before = remaining.Substring(0, idx);
|
||
if (!string.IsNullOrWhiteSpace(before))
|
||
slideBuffer.Add(before);
|
||
|
||
// 分頁
|
||
FlushSlide();
|
||
|
||
// 繼續處理分號之後的內容
|
||
remaining = remaining.Substring(idx + 1);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 收尾
|
||
FlushSection();
|
||
|
||
song.Sections = sections;
|
||
return song;
|
||
}
|
||
|
||
static bool IsHeader(string line, out string name)
|
||
{
|
||
name = "";
|
||
if (string.IsNullOrWhiteSpace(line)) return false;
|
||
line = line.Trim();
|
||
if (line.Length >= 3 && line.StartsWith("[") && line.EndsWith("]"))
|
||
{
|
||
name = line.Substring(1, line.Length - 2).Trim();
|
||
return name.Length > 0;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static int IndexOfBreak(string s)
|
||
{
|
||
if (string.IsNullOrEmpty(s)) return -1;
|
||
int a = s.IndexOf(';'); // 半形
|
||
int b = s.IndexOf(';'); // 全形
|
||
if (a == -1) return b;
|
||
if (b == -1) return a;
|
||
return Math.Min(a, b);
|
||
}
|
||
|
||
static bool ContainsCjk(string s)
|
||
{
|
||
foreach (char ch in s)
|
||
{
|
||
// 基本 CJK 區段(足夠處理歌詞)
|
||
if (ch >= 0x4E00 && ch <= 0x9FFF) return true;
|
||
}
|
||
return false;
|
||
}
|
||
|
||
static string MakeSafeFileName(string s)
|
||
{
|
||
char[] invalid = Path.GetInvalidFileNameChars();
|
||
char[] arr = s.Select(c => invalid.Contains(c) ? '_' : c).ToArray();
|
||
string cleaned = new string(arr);
|
||
return string.IsNullOrWhiteSpace(cleaned) ? "Presentation" : cleaned;
|
||
}
|
||
|
||
// ---------------- Pro6 XML building ----------------
|
||
|
||
static XDocument BuildPro6(Song song, int width, int height)
|
||
{
|
||
XElement root = new XElement("RVPresentationDocument",
|
||
new XAttribute("height", height),
|
||
new XAttribute("width", width),
|
||
new XAttribute("docType", "0"),
|
||
new XAttribute("versionNumber", "600"),
|
||
new XAttribute("usedCount", "0"),
|
||
new XAttribute("backgroundColor", "0 0 0 1"),
|
||
new XAttribute("drawingBackgroundColor", "false"),
|
||
new XAttribute("CCLIDisplay", "false"),
|
||
new XAttribute("lastDateUsed", ""),
|
||
new XAttribute("selectedArrangementID", ""),
|
||
new XAttribute("category", "Presentation"),
|
||
new XAttribute("resourcesDirectory", ""),
|
||
new XAttribute("notes", ""),
|
||
new XAttribute("CCLISongTitle", song.TitleZh ?? song.Title),
|
||
new XAttribute("chordChartPath", ""),
|
||
new XAttribute("os", "1"),
|
||
new XAttribute("buildNumber", "6016")
|
||
);
|
||
|
||
// Minimal timeline
|
||
root.Add(new XElement("RVTimeline",
|
||
new XAttribute("timeOffset", "0"),
|
||
new XAttribute("duration", "0"),
|
||
new XAttribute("selectedMediaTrackIndex", "-1"),
|
||
new XAttribute("loop", "false"),
|
||
new XAttribute("rvXMLIvarName", "timeline"),
|
||
new XElement("array", new XAttribute("rvXMLIvarName", "timeCues")),
|
||
new XElement("array", new XAttribute("rvXMLIvarName", "mediaTracks"))
|
||
));
|
||
|
||
XElement groupsArray = new XElement("array", new XAttribute("rvXMLIvarName", "groups"));
|
||
root.Add(groupsArray);
|
||
|
||
// Intro group at top
|
||
groupsArray.Add(BuildIntroGroup(song, width, height));
|
||
|
||
// Section groups
|
||
foreach (Section sec in song.Sections)
|
||
{
|
||
XElement group = new XElement("RVSlideGrouping",
|
||
new XAttribute("name", sec.Name),
|
||
new XAttribute("color", "1 1 1 0"),
|
||
new XAttribute("uuid", Guid.NewGuid().ToString())
|
||
);
|
||
|
||
XElement slidesArray = new XElement("array", new XAttribute("rvXMLIvarName", "slides"));
|
||
group.Add(slidesArray);
|
||
|
||
foreach (string slideText in sec.Slides)
|
||
{
|
||
// ⬇️ 自動分頁:可能回傳 1..N 張投影片
|
||
foreach (XElement pageSlide in BuildSlidesAutoPaginate(song, sec.Name, slideText, width, height))
|
||
{
|
||
slidesArray.Add(pageSlide);
|
||
}
|
||
}
|
||
|
||
groupsArray.Add(group);
|
||
}
|
||
|
||
// Arrangements (empty)
|
||
root.Add(new XElement("array", new XAttribute("rvXMLIvarName", "arrangements")));
|
||
|
||
return new XDocument(new XDeclaration("1.0", "utf-8", null), root);
|
||
}
|
||
|
||
// ---------------- Slides & Pagination ----------------
|
||
|
||
static XElement BuildIntroGroup(Song song, int width, int height)
|
||
{
|
||
XElement group = new XElement("RVSlideGrouping",
|
||
new XAttribute("name", "Intro"),
|
||
new XAttribute("color", "1 1 1 0"),
|
||
new XAttribute("uuid", Guid.NewGuid().ToString())
|
||
);
|
||
|
||
XElement slidesArray = new XElement("array", new XAttribute("rvXMLIvarName", "slides"));
|
||
group.Add(slidesArray);
|
||
|
||
slidesArray.Add(BuildIntroSlide(song, width, height));
|
||
return group;
|
||
}
|
||
|
||
static XElement BuildIntroSlide(Song song, int docWidth, int docHeight)
|
||
{
|
||
XElement slide = NewEmptySlide();
|
||
XElement elements = new XElement("array", new XAttribute("rvXMLIvarName", "displayElements"));
|
||
|
||
string titleZh = song.TitleZh ?? song.Title;
|
||
string? titleEn = song.TitleEn;
|
||
|
||
int marginX = 80;
|
||
int width = docWidth - marginX * 2;
|
||
int gap = 20;
|
||
|
||
// 150pt / 105pt → RTF \fs (half-points)
|
||
int fsCn = 300;
|
||
int fsEn = 210;
|
||
|
||
int cnHeight = 260;
|
||
int enHeight = 180;
|
||
|
||
if (!string.IsNullOrWhiteSpace(titleEn))
|
||
{
|
||
int totalH = cnHeight + gap + enHeight;
|
||
int startY = (docHeight - totalH) / 2;
|
||
|
||
string cnRect = "{" + marginX + " " + startY + " 0 " + width + " " + cnHeight + "}";
|
||
elements.Add(BuildTextElement(titleZh, cnRect, fsCn, TextAlign.Center));
|
||
|
||
string enRect = "{" + marginX + " " + (startY + cnHeight + gap) + " 0 " + width + " " + enHeight + "}";
|
||
elements.Add(BuildTextElement(titleEn, enRect, fsEn, TextAlign.Center));
|
||
}
|
||
else
|
||
{
|
||
int startY = (docHeight - cnHeight) / 2;
|
||
string rect = "{" + marginX + " " + startY + " 0 " + width + " " + cnHeight + "}";
|
||
elements.Add(BuildTextElement(titleZh, rect, fsCn, TextAlign.Center));
|
||
}
|
||
|
||
slide.Add(elements);
|
||
return slide;
|
||
}
|
||
|
||
// 自動分頁:把一段 slideText 依版面自動切成多張投影片
|
||
static IEnumerable<XElement> BuildSlidesAutoPaginate(Song song, string sectionName, string slideText, int docWidth, int docHeight)
|
||
{
|
||
// 版面參數(與單頁版一致)
|
||
int marginX = 80;
|
||
int marginTop = 120;
|
||
int marginBottom = 120;
|
||
int innerGap = 12; // 同組中英行距
|
||
int width = docWidth - marginX * 2;
|
||
|
||
// 字級(half-points):100pt=200、90pt=180
|
||
int fsCn = 200;
|
||
int fsEn = 180;
|
||
|
||
// 高度估值(像素)
|
||
int cnHeight = 190;
|
||
int enHeight = 160;
|
||
|
||
// 一組(中+英)所需高度(含一點額外間距)
|
||
int pairSlot = cnHeight + innerGap + enHeight + 20;
|
||
|
||
// 可用高度
|
||
int usableHeight = docHeight - marginTop - marginBottom;
|
||
|
||
// 先做中英配對
|
||
List<string> rawLines = (slideText ?? "")
|
||
.Replace("\r\n", "\n").Replace("\r", "\n")
|
||
.Split('\n')
|
||
.Select(s => s.TrimEnd())
|
||
.Where(s => !string.IsNullOrWhiteSpace(s))
|
||
.ToList();
|
||
|
||
List<Tuple<string?, string?>> pairs = new List<Tuple<string?, string?>>();
|
||
for (int i = 0; i < rawLines.Count;)
|
||
{
|
||
string line = rawLines[i];
|
||
if (ContainsCjk(line))
|
||
{
|
||
string? en = null;
|
||
if (i + 1 < rawLines.Count && !ContainsCjk(rawLines[i + 1]))
|
||
{
|
||
en = rawLines[i + 1];
|
||
i += 2;
|
||
}
|
||
else
|
||
{
|
||
i += 1;
|
||
}
|
||
pairs.Add(new Tuple<string?, string?>(line, en));
|
||
}
|
||
else
|
||
{
|
||
pairs.Add(new Tuple<string?, string?>(null, line));
|
||
i += 1;
|
||
}
|
||
}
|
||
|
||
// 開始分頁
|
||
int idx = 0;
|
||
while (idx < pairs.Count)
|
||
{
|
||
XElement slide = NewEmptySlide();
|
||
XElement elementsArray = new XElement("array", new XAttribute("rvXMLIvarName", "displayElements"));
|
||
int y = marginTop;
|
||
|
||
while (idx < pairs.Count)
|
||
{
|
||
string? cn = pairs[idx].Item1;
|
||
string? en = pairs[idx].Item2;
|
||
bool isPair = (cn != null && en != null);
|
||
|
||
int needed = isPair ? pairSlot : (ContainsCjk(cn ?? en ?? "") ? (cnHeight + 20) : (enHeight + 20));
|
||
|
||
// 如果放不下,先結束這一頁
|
||
if (y + needed > marginTop + usableHeight)
|
||
{
|
||
// 若這頁還沒放任何元素,強制塞一個以避免死循環
|
||
if (y == marginTop)
|
||
{
|
||
if (isPair)
|
||
{
|
||
string cnRect0 = "{" + marginX + " " + y + " 0 " + width + " " + cnHeight + "}";
|
||
elementsArray.Add(BuildTextElement(cn!, cnRect0, fsCn, TextAlign.Left));
|
||
|
||
string enRect0 = "{" + marginX + " " + (y + cnHeight + innerGap) + " 0 " + width + " " + enHeight + "}";
|
||
elementsArray.Add(BuildTextElement(en!, enRect0, fsEn, TextAlign.Left));
|
||
|
||
y += needed;
|
||
}
|
||
else
|
||
{
|
||
bool isCn = ContainsCjk(cn ?? en ?? "");
|
||
int fs = isCn ? fsCn : fsEn;
|
||
int boxH = isCn ? cnHeight : enHeight;
|
||
string rect0 = "{" + marginX + " " + y + " 0 " + width + " " + boxH + "}";
|
||
elementsArray.Add(BuildTextElement(cn ?? en ?? "", rect0, fs, TextAlign.Left));
|
||
y += (boxH + 20);
|
||
}
|
||
idx++; // 放了一個,移到下一筆
|
||
}
|
||
break; // 結束這頁
|
||
}
|
||
|
||
// 放進這頁
|
||
if (isPair)
|
||
{
|
||
string cnRect = "{" + marginX + " " + y + " 0 " + width + " " + cnHeight + "}";
|
||
elementsArray.Add(BuildTextElement(cn!, cnRect, fsCn, TextAlign.Left));
|
||
|
||
string enRect = "{" + marginX + " " + (y + cnHeight + innerGap) + " 0 " + width + " " + enHeight + "}";
|
||
elementsArray.Add(BuildTextElement(en!, enRect, fsEn, TextAlign.Left));
|
||
|
||
y += pairSlot;
|
||
}
|
||
else
|
||
{
|
||
bool isCn = ContainsCjk(cn ?? en ?? "");
|
||
int fs = isCn ? fsCn : fsEn;
|
||
int boxH = isCn ? cnHeight : enHeight;
|
||
|
||
string rect = "{" + marginX + " " + y + " 0 " + width + " " + boxH + "}";
|
||
elementsArray.Add(BuildTextElement(cn ?? en ?? "", rect, fs, TextAlign.Left));
|
||
|
||
y += (boxH + 20);
|
||
}
|
||
|
||
idx++;
|
||
}
|
||
|
||
// 加入右下角頁尾
|
||
string footerText = song.FooterText(sectionName);
|
||
elementsArray.Add(BuildTextElement(
|
||
footerText,
|
||
rect: FooterRect(docWidth, docHeight),
|
||
fontSizeHalfPoints: 28, // ≈14pt
|
||
align: TextAlign.Right
|
||
));
|
||
|
||
slide.Add(elementsArray);
|
||
yield return slide;
|
||
}
|
||
}
|
||
|
||
static XElement NewEmptySlide()
|
||
{
|
||
return new XElement("RVDisplaySlide",
|
||
new XAttribute("backgroundColor", "1 1 1 0"),
|
||
new XAttribute("highlightColor", ""),
|
||
new XAttribute("drawingBackgroundColor", "false"),
|
||
new XAttribute("enabled", "true"),
|
||
new XAttribute("hotKey", ""),
|
||
new XAttribute("label", ""),
|
||
new XAttribute("notes", ""),
|
||
new XAttribute("UUID", Guid.NewGuid().ToString()),
|
||
new XAttribute("chordChartPath", ""),
|
||
new XElement("array", new XAttribute("rvXMLIvarName", "cues"))
|
||
);
|
||
}
|
||
|
||
enum TextAlign { Left, Center, Right }
|
||
|
||
static string FooterRect(int docWidth, int docHeight)
|
||
{
|
||
int pad = 30;
|
||
int footerWidth = 900; // 雙語標題較長,留寬一點
|
||
int footerHeight = 50;
|
||
int x = docWidth - pad - footerWidth;
|
||
int y = docHeight - pad - footerHeight;
|
||
return "{" + x + " " + y + " 0 " + footerWidth + " " + footerHeight + "}";
|
||
}
|
||
|
||
// 建立一個 RVTextElement(RTF 內用白色、Unicode \uNNNN、可選對齊)
|
||
static XElement BuildTextElement(string text, string rect, int fontSizeHalfPoints, TextAlign align)
|
||
{
|
||
string rtf = BuildRtf(text, fontSizeHalfPoints, align);
|
||
string rtfB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(rtf));
|
||
string plainB64 = Convert.ToBase64String(Encoding.UTF8.GetBytes(text ?? ""));
|
||
|
||
return new XElement("RVTextElement",
|
||
new XAttribute("displayName", "Text"),
|
||
new XAttribute("UUID", Guid.NewGuid().ToString()),
|
||
new XAttribute("typeID", "0"),
|
||
new XAttribute("displayDelay", "0"),
|
||
new XAttribute("locked", "false"),
|
||
new XAttribute("persistent", "0"),
|
||
new XAttribute("fromTemplate", "false"),
|
||
new XAttribute("opacity", "1"),
|
||
new XAttribute("source", ""),
|
||
new XAttribute("bezelRadius", "0"),
|
||
new XAttribute("rotation", "0"),
|
||
new XAttribute("drawingFill", "false"),
|
||
new XAttribute("drawingShadow", "false"),
|
||
new XAttribute("drawingStroke", "false"),
|
||
new XAttribute("fillColor", "1 1 1 1"),
|
||
new XAttribute("adjustsHeightToFit", "false"),
|
||
new XAttribute("verticalAlignment", "0"),
|
||
new XAttribute("revealType", "0"),
|
||
|
||
new XElement("RVRect3D",
|
||
new XAttribute("rvXMLIvarName", "position"),
|
||
rect
|
||
),
|
||
|
||
new XElement("dictionary", new XAttribute("rvXMLIvarName", "stroke"),
|
||
new XElement("NSColor",
|
||
new XAttribute("rvXMLIvarName", "RVShapeElementStrokeColorKey"),
|
||
"1 1 1 1"
|
||
),
|
||
new XElement("NSNumber",
|
||
new XAttribute("rvXMLIvarName", "RVShapeElementStrokeWidthKey"),
|
||
new XAttribute("serialization", "double"),
|
||
"0"
|
||
)
|
||
),
|
||
|
||
new XElement("NSString", new XAttribute("rvXMLIvarName", "PlainText"), plainB64),
|
||
new XElement("NSString", new XAttribute("rvXMLIvarName", "RTFData"), rtfB64)
|
||
);
|
||
}
|
||
|
||
// 產生白字 + Unicode 的 RTF(全 ASCII,非 ASCII 用 \uNNNN)
|
||
static string BuildRtf(string text, int fontSizeHalfPoints, TextAlign align)
|
||
{
|
||
// 將換行先標準化,之後轉成 \line
|
||
string normalized = (text ?? "").Replace("\r\n", "\n").Replace("\r", "\n");
|
||
string content = ToRtfUnicode(normalized).Replace("\n", @"\line ");
|
||
|
||
string alignCode =
|
||
align == TextAlign.Right ? @"\qr " :
|
||
(align == TextAlign.Center ? @"\qc " : @"\ql ");
|
||
|
||
// 注意:\fs 是 half-points;\uc0 表示 \u 後不帶替代字元
|
||
// 字型表:f0=Arial(備用西文字型)、f1=Microsoft JhengHei(CJK 友善)
|
||
return @"{\rtf1\ansi\deff0"
|
||
+ @"{\fonttbl{\f0 Arial;}{\f1 Microsoft JhengHei;}}"
|
||
+ @"{\colortbl ;\red255\green255\blue255;}"
|
||
+ @"\uc0\f1\fs" + fontSizeHalfPoints + " " + alignCode + @"\cf1 "
|
||
+ content
|
||
+ "}";
|
||
}
|
||
|
||
// 把所有非 ASCII 字元轉成 RTF Unicode:\uNNNN(以 16 位有號整數輸出),並處理 RTF 轉義
|
||
static string ToRtfUnicode(string s)
|
||
{
|
||
StringBuilder sb = new StringBuilder(s.Length * 6);
|
||
foreach (char ch in s)
|
||
{
|
||
switch (ch)
|
||
{
|
||
case '\\': sb.Append(@"\\"); break;
|
||
case '{': sb.Append(@"\{"); break;
|
||
case '}': sb.Append(@"\}"); break;
|
||
case '\n': sb.Append('\n'); break; // 之後替換成 \line
|
||
default:
|
||
if (ch <= 0x7F)
|
||
{
|
||
sb.Append(ch);
|
||
}
|
||
else
|
||
{
|
||
// \uNNNN 使用 16 位帶正負號的數值
|
||
sb.Append(@"\u").Append(unchecked((short)ch)).Append(' ');
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
return sb.ToString();
|
||
}
|
||
}
|
||
|
||
} |