在 Android 上將簡報匯出為 XAML
概述
本文說明如何使用 Aspose.Slides for Android 透過 Java 將 PowerPoint 簡報匯出為 XAML。內容包括 XAML 的簡要介紹、展示如何使用預設設定將簡報儲存為 XAML,以及說明如何透過XamlOptions自訂匯出,包含匯出隱藏投影片。本文亦回答一些常見問題,涵蓋備援字型、XAML 堆疊相容性與隱藏投影片匯出行為。
關於 XAML
XAML 是一種基於 XML 的標記語言,用於描述 WPF(Windows Presentation Foundation)、UWP(Universal Windows Platform)以及 Xamarin.Forms 等框架中的使用者介面。
您可以在視覺化設計師中使用 XAML 檔案,或直接編寫與編輯標記。
使用預設選項將簡報匯出為 XAML
以下 Java 範例展示如何使用預設設定將簡報匯出為 XAML:
import com.aspose.slides.*;
Presentation presentation = new Presentation("pres.pptx");
try {
XamlOptions xamlOptions = new XamlOptions();
presentation.save(xamlOptions);
} finally {
presentation.dispose();
}
預設情況下,匯出的投影片會儲存在處理程序目前工作目錄下的 pres 子資料夾中。該資料夾會自動建立,所有必要的影像也會儲存在其中。
輸出資料夾名稱取自來源檔案名稱(不含副檔名)。例如 pres.pptx,輸出檔案會命名為 pres/Slide_1.xaml、pres/Slide_2.xaml,依此類推。即使您傳入簡報的絕對路徑,輸出資料夾仍會相對於目前工作目錄建立,而不是與輸入檔案同層。
在 Android 上,請使用應用程式可存取的輸入檔案。目前工作目錄可能無法寫入;請使用自訂的輸出 saver,將匯出保留在記憶體中或寫入應用程式儲存空間,如下所示。產生的 WPF XAML 旨在供相容的消費者使用,並非 Android 版面配置資源。
使用自訂選項將簡報匯出為 XAML
使用IXamlOptions介面來控制 Aspose.Slides 如何將簡報匯出為 XAML。
若要將輸出儲存到自訂位置,請實作IXamlOutputSaver並將您的實作實例傳遞給XamlOptions的setOutputSaver方法。
若要在 XAML 輸出中包含隱藏投影片,請以true呼叫setExportHiddenSlides,如下 Java 範例所示:
import com.aspose.slides.*;
Presentation presentation = new Presentation("pres.pptx");
try {
XamlOptions xamlOptions = new XamlOptions();
xamlOptions.setExportHiddenSlides(true);
presentation.save(xamlOptions);
} finally {
presentation.dispose();
}
擷取所有產生的 XAML 工件
XAML 匯出可能會為每個匯出的投影片產生一個 XAML 文件,外加獨立的影像與支援資源。將自訂的IXamlOutputSaver指派給XamlOptions.setOutputSaver以接收這些工件,而不是使用預設的檔案系統 saver。使用接受 XAML 選項的特定Presentation.save重載開始匯出。
了解回呼生命週期
匯出器會為每個產生的工件分別呼叫IXamlOutputSaver.save:
path用於識別工件,可能包含相對目錄。請保留此資訊,因為 XAML 可能使用相對路徑引用資源。data包含工件的位元組。影像與其他二進位資源不應被解碼為文字。- Saver 必須在返回之前保留或持久化資料。範例會將每個位元組陣列複製到應用程式自有的記憶體中。
- 僅在簡報儲存操作返回且所有回呼皆成功完成時,才視匯出為成功。不要忽略儲存錯誤或啟動未觀察的背景寫入。如果持久化在之後發生,僅在該步驟成功後才報告整體成功。
XamlOptions.setExportHiddenSlides 亦適用於自訂 saver。預設設定 false 會排除隱藏投影片的 XAML 文件。傳入 true 則會包含它們以及匯出所需的任何資源。資源數量取決於簡報本身;不要假設每張投影片僅有一次回呼或回呼順序固定。
匯出至記憶體並檢查工件
此完整範例載入 pres.pptx,將每個工件收集於Map<String, byte[]>中,並列印其名稱、類型與位元組計數。它會完整保留提供的名稱。若出現重複名稱,會將集合標記為無效,而非靜默覆寫工件。範例在使用結果前會檢查此情況。
import com.aspose.slides.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.nio.charset.StandardCharsets;
import java.util.Locale;
class MemoryXamlSaver implements IXamlOutputSaver {
final Map<String, byte[]> artifacts = new LinkedHashMap<>();
boolean valid = true;
@Override
public void save(String path, byte[] data) {
if (artifacts.containsKey(path)) {
valid = false;
System.err.println("Export rejected: duplicate artifact name: " + path);
return;
}
byte[] retainedData = data.clone();
artifacts.put(path, retainedData);
}
}
MemoryXamlSaver saver = new MemoryXamlSaver();
Presentation presentation = new Presentation("pres.pptx");
try {
XamlOptions options = new XamlOptions();
options.setOutputSaver(saver);
options.setExportHiddenSlides(true);
presentation.save(options);
} finally {
presentation.dispose();
}
if (!saver.valid) {
System.err.println("Export rejected: the artifact collection is invalid.");
return;
}
boolean inspectXamlText = false;
for (Map.Entry<String, byte[]> artifact : saver.artifacts.entrySet()) {
String name = artifact.getKey().toLowerCase(Locale.ROOT);
boolean isXaml = name.endsWith(".xaml");
boolean isImage = name.matches(".*\\.(png|jpg|jpeg|gif|bmp|tif|tiff|svg)$");
String kind = isXaml ? "slide XAML" : isImage ? "image" : "supporting resource";
System.out.println(artifact.getKey() + ": " + artifact.getValue().length + " bytes (" + kind + ")");
// 僅在需要文字檢查時才解碼 XAML。
if (isXaml && inspectXamlText) {
String markup = new String(artifact.getValue(), StandardCharsets.UTF_8);
System.out.println(markup);
}
}
擴充檢查對於檢查很有幫助;保留所有工件,包括不熟悉的資源類型。儲存或傳輸時請保持位元組不變。僅在需要文字處理的 XAML 時,才使用帶 UTF-8 的String constructor。
將收集的工件打包為 ZIP 壓縮檔
此獨立範例收集匯出結果,驗證名稱,並將原始位元組寫入 ZIP 壓縮檔。請將/path/to/app/files替換為 Android Context的getFilesDir方法回傳的路徑。唯一的壓縮檔名稱可區分同時的匯出工作。ZIP 條目使用正斜線並保留相對目錄。若名稱不安全或正規化後發生衝突,將在寫入前拒絕整個套件。
import com.aspose.slides.*;
import java.util.LinkedHashMap;
import java.util.Map;
import java.io.IOException;
import java.io.File;
import java.io.FileOutputStream;
import java.util.Set;
import java.util.TreeSet;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
class MemoryXamlSaver implements IXamlOutputSaver {
final Map<String, byte[]> artifacts = new LinkedHashMap<>();
boolean valid = true;
@Override
public void save(String path, byte[] data) {
if (artifacts.containsKey(path)) {
valid = false;
System.err.println("Export rejected: duplicate artifact name: " + path);
return;
}
byte[] retainedData = data.clone();
artifacts.put(path, retainedData);
}
}
MemoryXamlSaver saver = new MemoryXamlSaver();
Presentation presentation = new Presentation("pres.pptx");
try {
XamlOptions options = new XamlOptions();
options.setOutputSaver(saver);
options.setExportHiddenSlides(false);
presentation.save(options);
} finally {
presentation.dispose();
}
if (!saver.valid) {
System.err.println("Export rejected: the artifact collection is invalid.");
return;
}
Map<String, byte[]> entries = new LinkedHashMap<>();
Set<String> entryNames = new TreeSet<>(String.CASE_INSENSITIVE_ORDER);
for (Map.Entry<String, byte[]> artifact : saver.artifacts.entrySet()) {
String entryName = artifact.getKey().replace('\\', '/');
String[] segments = entryName.split("/", -1);
boolean unsafeName = entryName.startsWith("/") || entryName.contains(":");
for (String segment : segments) {
unsafeName |= segment.trim().isEmpty() || segment.equals(".") || segment.equals("..");
}
if (unsafeName || !entryNames.add(entryName)) {
System.err.println("Export rejected: unsafe or duplicate artifact name: " + artifact.getKey());
return;
}
entries.put(entryName, artifact.getValue());
}
File exportDirectory = new File("/path/to/app/files");
try {
File archiveFile = File.createTempFile("xaml-", ".zip", exportDirectory);
try (FileOutputStream archiveOutput = new FileOutputStream(archiveFile); ZipOutputStream archive = new ZipOutputStream(archiveOutput)) {
for (Map.Entry<String, byte[]> artifact : entries.entrySet()) {
ZipEntry entry = new ZipEntry(artifact.getKey());
archive.putNextEntry(entry);
archive.write(artifact.getValue());
archive.closeEntry();
}
}
// 在報告成功之前,透過關閉操作已完成 ZIP 目錄的最終化。
System.out.println("Saved " + entries.size() + " artifacts to " + archiveFile);
} catch (IOException exception) {
System.err.println("Archive persistence failed: " + exception.getMessage());
}
範例使用ZipOutputStream寫入單一本機壓縮檔;匯出器本身不會寫入分散的 XAML 或影像檔。若使用遠端儲存,請將寫入壓縮檔的階段改為上傳收集的位元組陣列。可使用匯出工作識別碼加上完整相對工件名稱作為 Blob 金鑰,或將工作識別碼、相對名稱與二進位資料存於資料庫欄位。僅在所有上傳完成或資料庫交易提交後才發布工作。若持久化失敗,請清理部分輸出。
對於大型簡報,自訂 saver 可以直接將每個工件持久化至應用程式儲存空間,以避免在記憶體中保留整個匯出的額外副本。從匯出器的角度來看,保持每個回呼為同步:僅在目標接受位元組後返回,並允許失敗傳遞給呼叫端。
保留資源名稱並驗證參考
- 當目的地需要時正規化路徑分隔符號,但保留相對目錄。除非所有產生的名稱已知唯一且資源參考仍有效,否則不要僅使用File.getName。
- 套用目的地特定的名稱驗證。寫入分散檔案時,拒絕根路徑與跳階段,使用File.getCanonicalPath解析目的地,並驗證其仍位於預期的匯出目錄之下,包含在包含性檢查中的目錄分隔符號。使用沒有符號連結的應用程式控制目錄,以免寫入被重新導向。
- 為每個匯出工作使用獨立的 saver 與儲存命名空間。於分隔符正規化後,並依目的地的大小寫敏感規則偵測衝突。
- 在發布之前,將每個 XAML 文件以 XML 解析,檢查其基於檔案的資源參考,例如影像的
Source或ImageSource屬性。將每個相對 URI 以所在 XAML 工件的目錄為基礎解析,正規化得到的儲存名稱,並確認對應的 map 鍵、ZIP 條目或儲存物件是否存在。將外部 URI 與 XAML 標記表達式與相對檔名分開處理。
例如,若pres/Slide_1.xaml參考images/image1.png,則儲存的資源必須以pres/images/image1.png的形式存在。僅保留image1.png會破壞此關係。對於物件儲存,請在工作前綴下保持相同的目錄結構,並讓這些資源 URL 可供 XAML 消費者存取。重新開啟完成的 ZIP,驗證條目名稱與資源位元組,並在目標 XAML 環境中載入代表性投影片,以確認影像正確解析。
常見問題
如果原始字型在機器上不可用,如何確保字型的可預測性?
在XamlOptions中呼叫setDefaultRegularFont — 當原始字型缺失時,匯出會使用此字型作為備援字型。但此並不保證產生的 XAML 會引用備援字型,或該字型在目標機器上可用。請確保 XAML 所參考的字型在顯示環境中可取得。
匯出的 XAML 僅供 WPF 使用,還是也能用於其他 XAML 堆疊?
Aspose.Slides 透過其公共 API 匯出 WPF XAML。與其他 XAML 堆疊(例如 UWP 與 Xamarin.Forms)的相容性未得到保證。請在目標環境中測試產生的標記。
是否支援隱藏投影片,且如何避免預設匯出它們?
預設情況下,隱藏投影片不會被包含。您可以透過XamlOptions中的setExportHiddenSlides控制此行為——若不需要匯出隱藏投影片,請保持其停用。