외부에 링크된 이미지로 프레젠테이션을 HTML로 내보내기
개요
기본적으로 Aspose.Slides는 프레젠테이션을 단일 HTML 파일로 내보냅니다. 이미지 및 기타 리소스는 일반적으로 Base64 데이터로 HTML에 직접 기록됩니다. 이는 하나의 휴대 가능한 파일이 필요할 때 편리하지만 웹 보기, CMS 또는 나중에 출력을 게시하는 서버 측 변환 파이프라인에 항상 최적의 형식은 아닙니다.
외부에 링크된 리소스를 사용하려는 경우:
- HTML 문서의 크기를 줄이기;
- 브라우저 또는 CDN에서 이미지, 폰트, 오디오 또는 비디오를 별도로 캐시하기;
- 내보낸 후 생성된 리소스를 검사, 교체, 압축 또는 후처리하기;
- 출력 구조를 웹 애플리케이션이 기대하는 형태에 가깝게 유지하기;
일반적인 HTML 변환 워크플로우에 대해서는 PowerPoint 프레젠테이션을 HTML로 변환를 참조하십시오. 이 문서는 내보내기의 리소스 연결 부분에 중점을 둡니다.
링크된 리소스 내보내기 작동 방식
ILinkEmbedController는 애플리케이션이 리소스별로 내보내기가 데이터를 HTML에 포함시킬지 외부에 저장하고 링크를 작성할지 결정하도록 해줍니다.
이 인터페이스에는 세 가지 메서드가 있습니다:
- ILinkEmbedController.getObjectStoringLocation은 리소스를 링크할지 포함시킬지 결정합니다.
- ILinkEmbedController.getUrl은 생성된 HTML 또는 다른 링크된 리소스에 기록될 URL을 반환합니다.
- ILinkEmbedController.saveExternal은 링크된 리소스 데이터를 디스크 또는 다른 저장 대상에 기록합니다.
파일 시스템 경로와 브라우저 URL은 별개의 개념입니다. 예를 들어, 아래 샘플은 리소스 파일을 애플리케이션의 파일 저장소인 html-output/assets에 저장하고, HTML에는 assets/resource-1.svg와 같은 상대 URL이 포함됩니다. 브라우저는 링크가 포함된 파일을 기준으로 이러한 URL을 해석합니다. 따라서 presentation.html에서 SVG 파일로의 링크는 assets/resource-1.svg를 사용하고, 해당 SVG 파일이 동일한 assets 폴더에 저장된 이미지로 연결할 때는 resource-4.jpg를 사용합니다.
링크된 리소스로 HTML 내보내기
다음 Android Java 예제는 출력 디렉터리를 생성하고 HTML 파일을 그곳에 저장하며, 링크된 리소스를 assets 하위 디렉터리에 저장합니다. applicationFilesDirectory로 context.getFilesDir()와 같은 앱 전용 디렉터리를 전달합니다. 이 코드는 java.nio.file API를 사용하지 않으므로 Android minSdk 19와 호환됩니다.
컨트롤러는 Aspose.Slides가 제공하거나 안전한 파일 확장자를 추론할 수 있는 경우 일반 이미지, 폰트, 오디오, 비디오 및 CSS 리소스를 링크합니다. 인식되지 않는 리소스는 포함된 상태로 남습니다.
import com.aspose.slides.HtmlFormatter;
import com.aspose.slides.HtmlOptions;
import com.aspose.slides.ILinkEmbedController;
import com.aspose.slides.LinkEmbedDecision;
import com.aspose.slides.Presentation;
import com.aspose.slides.SVGOptions;
import com.aspose.slides.SaveFormat;
import com.aspose.slides.SlideImageFormat;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
public class ExportToHtmlWithLinkedResources {
public static void exportPresentation(File applicationFilesDirectory) {
if (applicationFilesDirectory == null) {
throw new IllegalArgumentException("The application files directory must not be null.");
}
File inputFile = new File(applicationFilesDirectory, "presentation.pptx");
File outputDirectory = new File(applicationFilesDirectory, "html-output");
String assetDirectoryName = "assets";
File assetDirectory = new File(outputDirectory, assetDirectoryName);
createDirectory(outputDirectory, "HTML output");
createDirectory(assetDirectory, "asset output");
String assetUrlPrefix = assetDirectoryName + "/";
ExternalResourceController controller = new ExternalResourceController(assetDirectory, assetUrlPrefix);
SVGOptions svgOptions = new SVGOptions(controller);
SlideImageFormat slideImageFormat = SlideImageFormat.svg(svgOptions);
HtmlOptions htmlOptions = new HtmlOptions(controller);
htmlOptions.setHtmlFormatter(HtmlFormatter.createDocumentFormatter("", false));
htmlOptions.setSlideImageFormat(slideImageFormat);
Presentation presentation = new Presentation(inputFile.getAbsolutePath());
try {
File htmlFile = new File(outputDirectory, "presentation.html");
presentation.save(htmlFile.getAbsolutePath(), SaveFormat.Html, htmlOptions);
} finally {
presentation.dispose();
}
}
private static final class ExternalResourceController implements ILinkEmbedController {
private static final Map<String, String> EXTENSIONS_BY_CONTENT_TYPE = createExtensionsByContentType();
private final File assetDirectory;
private final String assetUrlPrefix;
private final Map<Integer, String> fileNamesByResourceId = new HashMap<Integer, String>();
private ExternalResourceController(File assetDirectory, String assetUrlPrefix) {
if (assetDirectory == null) {
throw new IllegalArgumentException("The asset output directory must not be null.");
}
this.assetDirectory = assetDirectory;
this.assetUrlPrefix = normalizeUrlPrefix(assetUrlPrefix);
}
@Override
public int getObjectStoringLocation(
int resourceId,
byte[] entityData,
String semanticName,
String contentType,
String recommendedExtension) {
String extension = resolveExtension(contentType, recommendedExtension);
if (extension == null) {
return LinkEmbedDecision.Embed;
}
fileNamesByResourceId.put(resourceId, "resource-" + resourceId + extension);
return LinkEmbedDecision.Link;
}
@Override
public String getUrl(int resourceId, int referrer) {
String fileName = fileNamesByResourceId.get(resourceId);
if (fileName == null) {
return null;
}
if (fileNamesByResourceId.containsKey(referrer)) {
return fileName;
}
return assetUrlPrefix + fileName;
}
@Override
public void saveExternal(int resourceId, byte[] entityData) {
String fileName = fileNamesByResourceId.get(resourceId);
if (fileName == null) {
throw new IllegalStateException(
"Resource " + resourceId + " was not registered for external storage.");
}
if (entityData == null || entityData.length == 0) {
throw new IllegalStateException(
"Resource " + resourceId + " contains no data and cannot be saved.");
}
createDirectory(assetDirectory, "asset output");
File outputFile = new File(assetDirectory, fileName);
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(outputFile);
outputStream.write(entityData);
} catch (IOException exception) {
throw new IllegalStateException(
"Failed to save external resource " + resourceId +
" to " + outputFile.getAbsolutePath() + ".",
exception);
} finally {
closeOutputStream(outputStream, outputFile);
}
}
private static Map<String, String> createExtensionsByContentType() {
Map<String, String> extensionsByContentType = new HashMap<String, String>();
extensionsByContentType.put("image/jpeg", ".jpg");
extensionsByContentType.put("image/png", ".png");
extensionsByContentType.put("image/gif", ".gif");
extensionsByContentType.put("image/bmp", ".bmp");
extensionsByContentType.put("image/svg+xml", ".svg");
extensionsByContentType.put("image/tiff", ".tiff");
extensionsByContentType.put("image/x-emf", ".emf");
extensionsByContentType.put("image/x-wmf", ".wmf");
extensionsByContentType.put("font/woff", ".woff");
extensionsByContentType.put("font/woff2", ".woff2");
extensionsByContentType.put("font/ttf", ".ttf");
extensionsByContentType.put("application/font-woff", ".woff");
extensionsByContentType.put("application/vnd.ms-fontobject", ".eot");
extensionsByContentType.put("application/x-font-ttf", ".ttf");
extensionsByContentType.put("text/css", ".css");
extensionsByContentType.put("audio/mpeg", ".mp3");
extensionsByContentType.put("audio/mp4", ".m4a");
extensionsByContentType.put("audio/wav", ".wav");
extensionsByContentType.put("video/mp4", ".mp4");
extensionsByContentType.put("video/webm", ".webm");
return extensionsByContentType;
}
private static String resolveExtension(String contentType, String recommendedExtension) {
if (contentType != null && !contentType.trim().equals("")) {
String normalizedContentType = contentType.toLowerCase(Locale.US);
String mappedExtension = EXTENSIONS_BY_CONTENT_TYPE.get(normalizedContentType);
if (mappedExtension != null) {
return mappedExtension;
}
}
if (!isSupportedContentType(contentType)) {
return null;
}
return normalizeExtension(recommendedExtension);
}
private static boolean isSupportedContentType(String contentType) {
return contentType != null &&
(contentType.regionMatches(true, 0, "image/", 0, "image/".length()) ||
contentType.regionMatches(true, 0, "font/", 0, "font/".length()) ||
contentType.regionMatches(true, 0, "audio/", 0, "audio/".length()) ||
contentType.regionMatches(true, 0, "video/", 0, "video/".length()));
}
private static String normalizeExtension(String extension) {
if (extension == null || extension.trim().equals("")) {
return null;
}
String extensionCharacters = extension.trim();
while (extensionCharacters.startsWith(".")) {
extensionCharacters = extensionCharacters.substring(1);
}
if (extensionCharacters.equals("")) {
return null;
}
int characterCount = extensionCharacters.length();
for (int index = 0; index < characterCount; index++) {
char character = extensionCharacters.charAt(index);
if (!Character.isLetterOrDigit(character)) {
return null;
}
}
return "." + extensionCharacters.toLowerCase(Locale.US);
}
private static String normalizeUrlPrefix(String urlPrefix) {
if (urlPrefix == null || urlPrefix.equals("")) {
return "";
}
String normalizedUrlPrefix = urlPrefix.replace('\\', '/');
return normalizedUrlPrefix.endsWith("/")
? normalizedUrlPrefix
: normalizedUrlPrefix + "/";
}
}
private static void createDirectory(File directory, String description) {
if (directory.exists()) {
if (!directory.isDirectory()) {
throw new IllegalStateException(
"The " + description + " path exists but is not a directory: " +
directory.getAbsolutePath());
}
return;
}
if (!directory.mkdirs()) {
throw new IllegalStateException(
"Failed to create the " + description + " directory: " +
directory.getAbsolutePath());
}
}
private static void closeOutputStream(FileOutputStream outputStream, File outputFile) {
if (outputStream == null) {
return;
}
try {
outputStream.close();
} catch (IOException exception) {
throw new IllegalStateException(
"Failed to close the external resource file: " +
outputFile.getAbsolutePath(),
exception);
}
}
}
내보낸 후 출력 폴더는 다음과 같은 구조를 가집니다:
html-output/
presentation.html
assets/
resource-1.svg
resource-2.svg
resource-3.svg
resource-4.jpg
resource-5.png
정확한 파일은 프레젠테이션 내용 및 내보내기 옵션에 따라 다릅니다. 예를 들어 래스터 이미지는 일반적으로 JPEG 또는 PNG 형식으로 내보내집니다. Aspose.Slides는 더 작거나 적합한 파일을 생성할 경우 원본 프레젠테이션에서 사용된 것과 다른 이미지 코덱을 선택할 수 있습니다. 투명성을 포함한 이미지는 PNG로 내보내집니다.
배포를 위한 URL 선택
샘플은 상대 URL 접두사 assets/를 사용합니다. presentation.html을 html-output/presentation.html에서 열면 브라우저는 html-output/assets/resource-1.svg를 로드합니다.
링크된 리소스가 다른 링크된 리소스를 참조할 때, 샘플은 ILinkEmbedController.getUrl의 referrer 매개변수를 사용하고 파일 이름만 반환합니다. 예를 들어 resource-1.svg와 resource-4.jpg가 모두 assets 폴더에 있는 경우, SVG 파일은 assets/resource-4.jpg가 아니라 resource-4.jpg를 참조해야 합니다.
파일이 다른 위치에 배포될 때는 다른 URL 접두사를 사용하십시오:
- HTML 파일과 같은 위치에 에셋 디렉터리가 있을 경우
assets/를 사용합니다. - 에셋 디렉터리가 HTML 파일 위 한 단계에 있을 경우
../assets/를 사용합니다. - 파일이 CDN이나 정적 파일 서버에 업로드될 경우
https://cdn.example.com/presentations/job-123/assets/를 사용합니다.
ILinkEmbedController.getUrl이 반환하는 URL은 ILinkEmbedController.saveExternal가 파일을 기록한 최종 배포 위치와 일치해야 합니다. Android 애플리케이션에서는 게시 워크플로에 따라 앱 전용 저장소, 캐시 디렉터리 또는 Storage Access Framework를 통해 얻은 디렉터리를 사용하십시오. 서버 애플리케이션에서는 각 변환 작업마다 고유한 출력 디렉터리 또는 객체 저장소 접두사를 사용하여 다른 내보내기의 파일이 덮어쓰이는 것을 방지합니다.
대신 포함해야 할 경우
Base64로 포함된 HTML은 이메일 첨부 파일, 오프라인 미리 보기 또는 지원 에셋 폴더 없이 이동되는 문서와 같이 출력이 단일 파일이어야 할 때 여전히 유용합니다. HTML이 웹 애플리케이션을 통해 제공되거나 CMS에 저장되고, 빌드 파이프라인에 의해 최적화되거나 브라우저가 HTML과 별도로 캐시할 경우에는 링크된 리소스가 더 적합합니다.
FAQ
이미지만 외부화하고 다른 리소스는 포함된 상태로 유지할 수 있나요?
예. ILinkEmbedController.getObjectStoringLocation에서 별도의 파일로 저장하고자 하는 콘텐츠 유형에 대해서만 LinkEmbedDecision의 Link를 반환하고, 그 외에는 Embed를 반환하면 됩니다.
왜 내보낸 이미지 확장자가 원본 프레젠테이션과 다릅니까?
Aspose.Slides는 크기 감소 또는 브라우저 호환성을 위해 HTML 내보내기 중에 래스터 이미지를 다시 인코딩할 수 있습니다. 예를 들어 원본 파일의 이미지는 렌더링 결과에 따라 JPEG 또는 PNG로 기록될 수 있습니다.
HTML 파일을 이동한 후에도 상대 URL이 작동합니까?
상대 URL은 동일한 상대 폴더 구조가 유지될 때만 작동합니다. HTML이 assets/resource-1.png를 참조한다면, assets 폴더는 다른 URL 접두사를 생성하지 않는 한 HTML 파일 옆에 있어야 합니다.
Android에서 리소스를 공용 외부 저장소에 쓸 수 있나요?
예, 애플리케이션이 대상 Android 버전에 맞는 유효한 저장 위치와 권한 모델을 가지고 있다면 가능합니다. 앱에서만 사용하는 생성된 HTML의 경우 앱 전용 파일이나 캐시 디렉터리가 보통 더 간단합니다. 사용자에게 보이는 출력의 경우 사용자가 선택한 위치나 앱에 맞는 다른 저장 방식을 사용하십시오.
서버 애플리케이션이 동일한 출력 폴더를 재사용해야 하나요?
아니요. 각 변환 작업마다 고유한 출력 디렉터리나 저장 접두사를 사용하십시오. 이렇게 하면 파일명 충돌을 방지하고 한 내보내기가 다른 내보내기의 리소스를 덮어쓰는 것을 막을 수 있습니다.