Preserve QPowerPoint image hyperlinks in PPTX

This commit is contained in:
Курнат Андрей
2026-06-02 06:17:58 +03:00
parent 0a675236b3
commit e5727b4484
6 changed files with 165 additions and 8 deletions
+24 -6
View File
@@ -74,7 +74,7 @@ export async function importPptxFile(file: File): Promise<ImportedPowerPointDeck
const slideRelationshipsXml = await readOptionalParsedXml(zip, parser, relationshipsPath);
const hyperlinkTargets = relationshipTargetsById(slideRelationshipsXml, hyperlinkRelationshipType, path);
const slideTextBlocks = mergeOrderedSlideTextBlocks(extractSlideTextBlocks(parsedSlide, hyperlinkTargets), extractOrderedTextBodyTexts(orderedSlide));
const images = await extractSlideImages(zip, parser, parsedSlide, path, index);
const images = await extractSlideImages(zip, parser, parsedSlide, path, index, slideRelationshipsXml);
const notesPath = await slideNotesPath(zip, parser, path);
const notesXml = notesPath ? await readOptionalZipText(zip, notesPath) : await readOptionalZipText(zip, `ppt/notesSlides/notesSlide${index}.xml`);
const notes = notesXml
@@ -157,13 +157,15 @@ export async function exportPptxBlob(deck: ExportablePowerPointDeck): Promise<Bl
return;
}
const imageHyperlink = normalizedExternalHyperlinkTarget(image.hyperlink);
slide.addImage({
data,
x: image.x,
y: image.y,
w: image.width,
h: image.height,
altText: image.alt || `Изображение ${index + 1}`
altText: image.alt || `Изображение ${index + 1}`,
...(imageHyperlink ? { hyperlink: { url: imageHyperlink } } : {})
});
});
slide.addText(sourceSlide.title || "Без заголовка", {
@@ -426,14 +428,16 @@ async function extractSlideImages(
parser: { parse: (xml: string) => unknown },
slideXml: unknown,
slidePath: string,
slideIndex: number
slideIndex: number,
relationshipsXml?: unknown
): Promise<SlideImage[]> {
const relationshipsPath = `${partDirectory(slidePath)}/_rels/${partFileName(slidePath)}.rels`;
const relationshipsXml = await readOptionalParsedXml(zip, parser, relationshipsPath);
const imageTargets = relationshipTargetsById(relationshipsXml, imageRelationshipType, slidePath);
const parsedRelationshipsXml = relationshipsXml ?? (await readOptionalParsedXml(zip, parser, relationshipsPath));
const imageTargets = relationshipTargetsById(parsedRelationshipsXml, imageRelationshipType, slidePath);
const hyperlinkTargets = relationshipTargetsById(parsedRelationshipsXml, hyperlinkRelationshipType, slidePath);
const pictures = recordsByKey(slideXml, "pic");
const images = await Promise.all(
pictures.map(async (picture, imageIndex) => slideImageFromPicture(zip, picture, imageTargets, slideIndex, imageIndex))
pictures.map(async (picture, imageIndex) => slideImageFromPicture(zip, picture, imageTargets, hyperlinkTargets, slideIndex, imageIndex))
);
return images.filter((image): image is SlideImage => Boolean(image));
@@ -443,6 +447,7 @@ async function slideImageFromPicture(
zip: ZipArchive,
picture: Record<string, unknown>,
imageTargets: Record<string, string>,
hyperlinkTargets: Record<string, string>,
slideIndex: number,
imageIndex: number
): Promise<SlideImage | null> {
@@ -453,14 +458,27 @@ async function slideImageFromPicture(
return null;
}
const hyperlink = pictureHyperlink(picture, hyperlinkTargets);
return {
id: `pptx-slide-${slideIndex}-image-${imageIndex + 1}`,
src,
alt: pictureAltText(picture) || `Изображение ${imageIndex + 1}`,
...(hyperlink ? { hyperlink } : {}),
...pictureBounds(picture)
};
}
function pictureHyperlink(picture: Record<string, unknown>, hyperlinkTargets: Record<string, string>) {
return (
recordsByKey(picture, "hlinkClick")
.map((record) => {
const id = relationshipId(record);
return id ? normalizedExternalHyperlinkTarget(hyperlinkTargets[id]) : "";
})
.find(Boolean) ?? ""
);
}
function pictureRelationshipId(picture: Record<string, unknown>) {
const blip = firstRecordByKey(picture, "blip");
return stringValue(