외부 링크된 이미지와 함께 프레젠테이션을 HTML로 내보내기
개요
기본적으로 Aspose.Slides는 프레젠테이션을 독립형 HTML 파일로 내보냅니다. 이미지 및 기타 리소스는 일반적으로 Base64 데이터로 HTML에 직접 기록됩니다. 하나의 휴대용 파일이 필요할 때는 편리하지만, 웹사이트, CMS 또는 서버 측 변환 파이프라인에 항상 최적의 형식은 아닙니다.
다음과 같은 경우 외부 링크 리소스를 사용하십시오:
- HTML 문서 크기 감소
- 브라우저 또는 CDN에서 이미지, 글꼴, 오디오, 비디오를 별도로 캐시
- 내보낸 후 리소스를 검사, 교체, 압축 또는 후처리
- 웹 애플리케이션이 기대하는 구조에 더 가깝게 출력 유지
일반 HTML 변환 워크플로에 대해서는 Convert PowerPoint Presentations to HTML를 참조하십시오. 이 문서는 내보내기의 리소스 연결 부분에 초점을 맞춥니다.
링크된 리소스 내보내기 작동 방식
ILinkEmbedController에 대한 Java 프록시는 애플리케이션이 리소스 별로 데이터를 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 내보내기
다음 JavaScript 예제는 출력 디렉터리를 생성하고, HTML 파일을 그곳에 저장하며, assets 하위 디렉터리에 링크된 리소스를 보관합니다. 컨트롤러는 Aspose.Slides가 제공하거나 안전한 파일 확장자를 추론할 수 있을 때 일반 이미지, 글꼴, 오디오, 비디오 및 CSS 리소스를 링크합니다. 인식되지 않은 리소스는 계속 포함됩니다.
var aspose = aspose || {};
aspose.slides = require("aspose.slides.via.java");
const java = require("java");
const fs = require("fs");
const path = require("path");
class ExternalResourceController {
constructor(assetDirectory, assetUrlPrefix) {
if (assetDirectory == null || assetDirectory.trim().length === 0) {
throw new Error("The asset output directory must not be empty.");
}
this.assetDirectory = assetDirectory;
this.assetUrlPrefix = normalizeUrlPrefix(assetUrlPrefix);
this.fileNamesByResourceId = new Map();
}
createProxy() {
const linkEmbedControllerInterfaceName = "com.aspose.slides.ILinkEmbedController";
let controller = this;
return java.newProxy(linkEmbedControllerInterfaceName, {
getObjectStoringLocation: function(resourceId, entityData, semanticName, contentType, recommendedExtension) {
return controller.getObjectStoringLocation(
resourceId,
entityData,
semanticName,
contentType,
recommendedExtension);
},
getUrl: function(resourceId, referrer) {
return controller.getUrl(resourceId, referrer);
},
saveExternal: function(resourceId, entityData) {
controller.saveExternal(resourceId, entityData);
}
});
}
getObjectStoringLocation(resourceId, entityData, semanticName, contentType, recommendedExtension) {
let extension = resolveExtension(contentType, recommendedExtension);
if (extension == null) {
return aspose.slides.LinkEmbedDecision.Embed;
}
this.fileNamesByResourceId.set(resourceId, "resource-" + resourceId + extension);
return aspose.slides.LinkEmbedDecision.Link;
}
getUrl(resourceId, referrer) {
let fileName = this.fileNamesByResourceId.get(resourceId);
if (fileName == null) {
return null;
}
if (this.fileNamesByResourceId.has(referrer)) {
return fileName;
}
return this.assetUrlPrefix + fileName;
}
saveExternal(resourceId, entityData) {
let fileName = this.fileNamesByResourceId.get(resourceId);
if (fileName == null) {
throw new Error("Resource " + resourceId + " was not registered for external storage.");
}
if (entityData == null || entityData.length === 0) {
throw new Error("Resource " + resourceId + " contains no data and cannot be saved.");
}
fs.mkdirSync(this.assetDirectory, { recursive: true });
let filePath = path.join(this.assetDirectory, fileName);
let fileData = Buffer.from(entityData);
fs.writeFileSync(filePath, fileData);
}
}
function createExtensionsByContentType() {
let extensionsByContentType = new Map();
extensionsByContentType.set("image/jpeg", ".jpg");
extensionsByContentType.set("image/png", ".png");
extensionsByContentType.set("image/gif", ".gif");
extensionsByContentType.set("image/bmp", ".bmp");
extensionsByContentType.set("image/svg+xml", ".svg");
extensionsByContentType.set("image/tiff", ".tiff");
extensionsByContentType.set("image/x-emf", ".emf");
extensionsByContentType.set("image/x-wmf", ".wmf");
extensionsByContentType.set("font/woff", ".woff");
extensionsByContentType.set("font/woff2", ".woff2");
extensionsByContentType.set("font/ttf", ".ttf");
extensionsByContentType.set("application/font-woff", ".woff");
extensionsByContentType.set("application/vnd.ms-fontobject", ".eot");
extensionsByContentType.set("application/x-font-ttf", ".ttf");
extensionsByContentType.set("text/css", ".css");
extensionsByContentType.set("audio/mpeg", ".mp3");
extensionsByContentType.set("audio/mp4", ".m4a");
extensionsByContentType.set("audio/wav", ".wav");
extensionsByContentType.set("video/mp4", ".mp4");
extensionsByContentType.set("video/webm", ".webm");
return extensionsByContentType;
}
let extensionsByContentType = createExtensionsByContentType();
function resolveExtension(contentType, recommendedExtension) {
if (contentType != null && contentType.trim().length > 0) {
let mappedExtension = extensionsByContentType.get(contentType);
if (mappedExtension != null) {
return mappedExtension;
}
}
if (!isSupportedContentType(contentType)) {
return null;
}
return normalizeExtension(recommendedExtension);
}
function isSupportedContentType(contentType) {
if (contentType == null) {
return false;
}
let normalizedContentType = contentType.toLowerCase();
return normalizedContentType.startsWith("image/") ||
normalizedContentType.startsWith("font/") ||
normalizedContentType.startsWith("audio/") ||
normalizedContentType.startsWith("video/");
}
function normalizeExtension(extension) {
if (extension == null || extension.trim().length === 0) {
return null;
}
let extensionCharacters = extension.trim();
while (extensionCharacters.startsWith(".")) {
extensionCharacters = extensionCharacters.substring(1);
}
if (extensionCharacters.length === 0) {
return null;
}
for (let index = 0; index < extensionCharacters.length; index++) {
let character = extensionCharacters[index];
if (!/[A-Za-z0-9]/.test(character)) {
return null;
}
}
return "." + extensionCharacters.toLowerCase();
}
function normalizeUrlPrefix(urlPrefix) {
if (urlPrefix == null || urlPrefix.length === 0) {
return "";
}
let normalizedUrlPrefix = urlPrefix.replace(/\\/g, "/");
return normalizedUrlPrefix.endsWith("/")
? normalizedUrlPrefix
: normalizedUrlPrefix + "/";
}
let inputFilePath = "presentation.pptx";
let outputDirectory = "html-output";
let assetDirectoryName = "assets";
let assetDirectory = path.join(outputDirectory, assetDirectoryName);
fs.mkdirSync(outputDirectory, { recursive: true });
fs.mkdirSync(assetDirectory, { recursive: true });
let assetUrlPrefix = assetDirectoryName + "/";
let controllerWrapper = new ExternalResourceController(assetDirectory, assetUrlPrefix);
let controller = controllerWrapper.createProxy();
let svgOptions = new aspose.slides.SVGOptions(controller);
let slideImageFormat = aspose.slides.SlideImageFormat.svg(svgOptions);
let htmlOptions = new aspose.slides.HtmlOptions(controller);
htmlOptions.setHtmlFormatter(aspose.slides.HtmlFormatter.createDocumentFormatter("", false));
htmlOptions.setSlideImageFormat(slideImageFormat);
let presentation = new aspose.slides.Presentation(inputFilePath);
try {
let htmlFilePath = path.join(outputDirectory, "presentation.html");
presentation.save(htmlFilePath, aspose.slides.SaveFormat.Html, htmlOptions);
} finally {
if (presentation != null) {
presentation.dispose();
}
}
내보내기 후 출력 폴더 구조는 다음과 같습니다:
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이 기록한 파일의 최종 배포 위치와 일치해야 합니다. 서버 애플리케이션에서는 각 변환 작업마다 고유한 출력 디렉터리 또는 객체 저장소 접두사를 사용해 다른 내보내기의 파일이 덮어쓰이지 않도록 해야 합니다.
대신 포함해야 할 경우
단일 파일이어야 하는 경우(예: 이메일 첨부 파일, 오프라인 미리 보기, 별도 에셋 폴더 없이 이동되는 문서)에는 Base64로 포함된 HTML이 여전히 유용합니다. HTML이 웹 애플리케이션에 의해 제공되거나 CMS에 저장되고, 빌드 파이프라인에서 최적화되거나 브라우저에서 HTML과 독립적으로 캐시될 경우에는 링크된 리소스가 더 적합합니다.
FAQ
이미지만 외부화하고 다른 리소스는 포함할 수 있나요?
네. ILinkEmbedController.getObjectStoringLocation에서 별도 파일로 저장하고 싶은 콘텐츠 유형에 대해서만 LinkEmbedDecision.Link를 반환하고, 나머지는 LinkEmbedDecision.Embed를 반환하면 됩니다.
내보낸 이미지 확장자가 원본 프레젠테이션과 다른 이유는 무엇인가요?
Aspose.Slides는 HTML 내보내기 중에 크기나 브라우저 호환성을 개선하기 위해 래스터 이미지를 재인코딩할 수 있습니다. 예를 들어 원본 파일의 이미지가 렌더링 결과에 따라 JPEG 또는 PNG로 기록될 수 있습니다.
HTML 파일을 이동한 후에도 상대 URL이 작동하나요?
상대 URL은 동일한 상대 폴더 구조가 유지되는 경우에만 작동합니다. HTML이 assets/resource-1.png를 참조한다면 assets 폴더는 HTML 파일 옆에 그대로 있어야 합니다. 다른 URL 접두사를 생성하지 않는 한 말이죠.
서버 애플리케이션에서 동일한 출력 폴더를 재사용해야 하나요?
아니요. 각 변환 작업마다 고유한 출력 디렉터리 또는 저장 접두사를 사용하십시오. 이렇게 하면 파일 이름 충돌을 방지하고 한 내보내기가 다른 내보내기의 리소스를 덮어쓰는 상황을 피할 수 있습니다.