ส่งออกงานนำเสนอเป็น HTML พร้อมรูปภาพที่เชื่อมโยงภายนอก

ภาพรวม

โดยค่าเริ่มต้น Aspose.Slides จะส่งออกงานนำเสนอเป็นไฟล์ HTML ที่บรรจุทั้งหมด ภาพและทรัพยากรอื่น ๆ จะถูกเขียนลงใน HTML โดยตรงทั่วไปเป็นข้อมูล Base64 สิ่งนี้สะดวกเมื่อคุณต้องการไฟล์พกพาเดียว แต่ไม่จำเป็นต้องเป็นรูปแบบที่ดีที่สุดสำหรับเว็บไซต์, CMS หรือไพล์ไลน์การแปลงฝั่งเซิร์ฟเวอร์

ใช้ทรัพยากรที่เชื่อมโยงภายนอกเมื่อต้องการ:

  • ลดขนาดของเอกสาร HTML;
  • แคชภาพ, ฟอนต์, เสียงหรือวิดีโอแยกกันในเบราว์เซอร์หรือ CDN;
  • ตรวจสอบ, แทนที่, บีบอัด หรือประมวลผลต่อเนื่องทรัพยากรที่สร้างหลังการส่งออก;
  • ทำให้โครงสร้างผลลัพธ์ใกล้เคียงกับที่แอปพลิเคชันเว็บคาดหวังมากขึ้น.

สำหรับกระบวนการแปลง HTML ทั่วไป, ดูที่ แปลงงานนำเสนอ PowerPoint เป็น HTML. บทความนี้เน้นส่วนการเชื่อมโยงทรัพยากรของการส่งออก.

วิธีการทำงานของการส่งออกทรัพยากรที่เชื่อมโยง

พร็อกซี Java สำหรับ ILinkEmbedController ให้แอปพลิเคชันของคุณกำหนด, ทีละทรัพยากร, ว่าเครื่องมือส่งออกจะฝังข้อมูลลงใน HTML หรือบันทึกเป็นไฟล์ภายนอกและเขียนลิงก์

คอนโทรลเลอร์มีสามเมธอด:

  • ILinkEmbedController.getObjectStoringLocation กำหนดว่าทรัพยากรควรเชื่อมหรือฝัง;
  • ILinkEmbedController.getUrl คืนค่า URL ที่จะเขียนลงใน HTML ที่สร้างหรือทรัพยากรที่เชื่อมโยงอื่น;
  • ILinkEmbedController.saveExternal เขียนข้อมูลของทรัพยากรที่เชื่อมโยงลงดิสก์หรือเป้าหมายการจัดเก็บอื่น

เส้นทางระบบไฟล์และ URL ของเบราว์เซอร์เป็นเรื่องที่แยกจากกัน ตัวอย่างเช่น ตัวอย่างด้านล่างเขียนไฟล์ทรัพยากรไปยัง html-output/assets บนดิสก์, ในขณะที่ HTML มี URL แบบสัมพันธ์เช่น assets/resource-1.svg. เบราว์เซอร์จะตีความ URL เหล่านี้สัมพันธ์กับไฟล์ที่มีลิงก์ ดังนั้นลิงก์จาก presentation.html ไปยังไฟล์ SVG จะใช้ assets/resource-1.svg, ในขณะที่ลิงก์จากไฟล์ SVG นั้นไปยังรูปภาพที่บันทึกในโฟลเดอร์ assets เดียวกันจะใช้ resource-4.jpg.

ส่งออก HTML พร้อมทรัพยากรที่เชื่อมโยง

ตัวอย่าง JavaScript ด้านล่างสร้างไดเรกทอรีผลลัพธ์, บันทึกไฟล์ HTML ไว้ที่นั่น, และเก็บทรัพยากรที่เชื่อมโยงในโฟลเดอร์ย่อย assets. คอนโทรลเลอร์จะเชื่อมโยงรูปภาพ, ฟอนต์, เสียง, วิดีโอ และ CSS ทั่วไปเมื่อ Aspose.Slides มีหรือสามารถคาดเดานามสกุลไฟล์ที่ปลอดภัยได้. ทรัพยากรที่ไม่รู้จักจะยังคงถูกฝังไว้.

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.

เมื่อทรัพยากรที่เชื่อมโยงหนึ่งอ้างอิงถึงอีกทรัพยากรที่เชื่อมโยง, ตัวอย่างใช้พารามิเตอร์ referrer ใน ILinkEmbedController.getUrl และคืนค่าเฉพาะชื่อไฟล์. ตัวอย่างเช่น หาก resource-1.svg และ resource-4.jpg อยู่ในโฟลเดอร์ assets ทั้งสอง, ไฟล์ SVG ควรอ้างถึง resource-4.jpg ไม่ใช่ assets/resource-4.jpg.

ใช้คำนำหน้า URL ที่แตกต่างเมื่อไฟล์ถูกปรับใช้ในที่อื่น:

  • ใช้ assets/ เมื่อไดเรกทอรีทรัพยากรอยู่ถัดจากไฟล์ HTML;
  • ใช้ ../assets/ เมื่อไดเรกทอรีทรัพยากรอยู่ระดับหนึ่งเหนือไฟล์ HTML;
  • ใช้ https://cdn.example.com/presentations/job-123/assets/ เมื่อไฟล์ถูกอัปโหลดไปยัง CDN หรือเซิร์ฟเวอร์ไฟล์สถิตย์.

URL ที่คืนโดย ILinkEmbedController.getUrl ต้องตรงกับตำแหน่งที่ปรับใช้จริงของไฟล์ที่เขียนโดย ILinkEmbedController.saveExternal. ในแอปพลิเคชันเซิร์ฟเวอร์, ใช้ไดเรกทอรีผลลัพธ์หรือคำนำหน้าการจัดเก็บวัตถุที่ไม่ซ้ำกันสำหรับแต่ละงานแปลงเพื่อหลีกเลี่ยงการเขียนทับไฟล์จากการส่งออกอื่น.

เมื่อควรฝังแทนที่จะเชื่อมโยง

HTML ที่ฝัง Base64 ยังคงมีประโยชน์เมื่อผลลัพธ์ต้องเป็นไฟล์เดียว, เช่น การแนบอีเมล, ตัวอย่างออฟไลน์, หรือเอกสารที่ต้องย้ายโดยไม่มีโฟลเดอร์ทรัพยากรสนับสนุน. ทรัพยากรที่เชื่อมโยงเหมาะสมกว่าเมื่อ HTML จะถูกให้บริการโดยแอปพลิเคชันเว็บ, เก็บใน CMS, ปรับโดยไพล์ไลน์การสร้าง, หรือแคชโดยเบราว์เซอร์แยกจาก HTML.

คำถามที่พบบ่อย

ฉันสามารถแยกภาพออกเป็นไฟล์ภายนอกและให้ทรัพยากรอื่น ๆ ยังคงฝังอยู่ได้หรือไม่?

ได้. ใน ILinkEmbedController.getObjectStoringLocation, คืนค่า LinkEmbedDecision.Link เฉพาะสำหรับประเภทเนื้อหาที่ต้องการบันทึกเป็นไฟล์แยก, และคืนค่า LinkEmbedDecision.Embed สำหรับส่วนอื่นทั้งหมด.

ทำไมนามสกุลของภาพที่ส่งออกจึงแตกต่างจากงานนำเสนอต้นฉบับ?

Aspose.Slides อาจทำการเข้ารหัสใหม่ของรูปภาพราสเตอร์ระหว่างการส่งออกเป็น HTML เพื่อปรับขนาดหรือความเข้ากันได้กับเบราว์เซอร์. ตัวอย่างเช่น ภาพจากไฟล์ต้นฉบับอาจถูกเขียนเป็น JPEG หรือ PNG ขึ้นอยู่กับผลลัพธ์ที่เรนเดอร์ได้.

URL แบบสัมพันธ์ทำงานได้หรือไม่หลังจากที่ย้ายไฟล์ HTML?

URL แบบสัมพันธ์ทำงานได้เฉพาะเมื่อโครงสร้างโฟลเดอร์สัมพันธ์เดียวกันยังคงอยู่. หาก HTML อ้างถึง assets/resource-1.png, โฟลเดอร์ assets ต้องอยู่ถัดจากไฟล์ HTML เว้นแต่คุณจะสร้างคำนำหน้า URL ที่แตกต่าง.

แอปพลิเคชันเซิร์ฟเวอร์ควรใช้โฟลเดอร์ผลลัพธ์เดียวกันหรือไม่?

ไม่. ใช้ไดเรกทอรีผลลัพธ์หรือคำนำหน้าการจัดเก็บที่ไม่ซ้ำกันสำหรับแต่ละงานแปลง. วิธีนี้จะหลีกเลี่ยงการชนของชื่อไฟล์และป้องกันการเขียนทับทรัพยากรที่สร้างโดยการส่งออกอื่น.