|
|
|
|
@ -498,17 +498,40 @@ function buildEmptyPara(): string {
|
|
|
|
|
function buildPicParagraph(binDataId: number, widthHwp: number, heightHwp: number): string {
|
|
|
|
|
const pId = nextId();
|
|
|
|
|
const picId = nextId();
|
|
|
|
|
const fileId = `image${binDataId}`;
|
|
|
|
|
const halfW = Math.round(widthHwp / 2);
|
|
|
|
|
const halfH = Math.round(heightHwp / 2);
|
|
|
|
|
return (
|
|
|
|
|
`<hp:p id="${pId}" paraPrIDRef="0" styleIDRef="0" pageBreak="0" columnBreak="0" merged="0">` +
|
|
|
|
|
'<hp:run charPrIDRef="0">' +
|
|
|
|
|
`<hp:pic id="${picId}" zOrder="0" numberingType="FIGURE" textWrap="FLOAT" ` +
|
|
|
|
|
`textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" pageBreak="CELL">` +
|
|
|
|
|
`<hp:sz width="${widthHwp}" height="${heightHwp}" widthRelTo="ABSOLUTE" heightRelTo="ABSOLUTE" protect="0"/>` +
|
|
|
|
|
`<hp:pic id="${picId}" zOrder="0" numberingType="PICTURE" textWrap="TOP_AND_BOTTOM" ` +
|
|
|
|
|
`textFlow="BOTH_SIDES" lock="0" dropcapstyle="None" href="" groupLevel="0" instid="${picId}" reverse="0">` +
|
|
|
|
|
'<hp:offset x="0" y="0"/>' +
|
|
|
|
|
`<hp:orgSz width="${widthHwp}" height="${heightHwp}"/>` +
|
|
|
|
|
`<hp:curSz width="${widthHwp}" height="${heightHwp}"/>` +
|
|
|
|
|
'<hp:flip horizontal="0" vertical="0"/>' +
|
|
|
|
|
`<hp:rotationInfo angle="0" centerX="${halfW}" centerY="${halfH}" rotateimage="1"/>` +
|
|
|
|
|
'<hp:renderingInfo>' +
|
|
|
|
|
'<hc:transMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>' +
|
|
|
|
|
'<hc:scaMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>' +
|
|
|
|
|
'<hc:rotMatrix e1="1" e2="0" e3="0" e4="0" e5="1" e6="0"/>' +
|
|
|
|
|
'</hp:renderingInfo>' +
|
|
|
|
|
'<hp:imgRect>' +
|
|
|
|
|
`<hc:pt0 x="0" y="0"/>` +
|
|
|
|
|
`<hc:pt1 x="${widthHwp}" y="0"/>` +
|
|
|
|
|
`<hc:pt2 x="${widthHwp}" y="${heightHwp}"/>` +
|
|
|
|
|
`<hc:pt3 x="0" y="${heightHwp}"/>` +
|
|
|
|
|
'</hp:imgRect>' +
|
|
|
|
|
`<hp:imgClip left="0" right="${widthHwp}" top="0" bottom="${heightHwp}"/>` +
|
|
|
|
|
'<hp:inMargin left="0" right="0" top="0" bottom="0"/>' +
|
|
|
|
|
`<hp:imgDim dimwidth="${widthHwp}" dimheight="${heightHwp}"/>` +
|
|
|
|
|
`<hc:img binaryItemIDRef="${fileId}" bright="0" contrast="0" effect="REAL_PIC" alpha="0"/>` +
|
|
|
|
|
'<hp:effects/>' +
|
|
|
|
|
`<hp:sz width="${widthHwp}" widthRelTo="ABSOLUTE" height="${heightHwp}" heightRelTo="ABSOLUTE" protect="0"/>` +
|
|
|
|
|
'<hp:pos treatAsChar="1" affectLSpacing="0" flowWithText="1" allowOverlap="0" ' +
|
|
|
|
|
'holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="COLUMN" ' +
|
|
|
|
|
'holdAnchorAndSO="0" vertRelTo="PARA" horzRelTo="PARA" ' +
|
|
|
|
|
'vertAlign="TOP" horzAlign="LEFT" vertOffset="0" horzOffset="0"/>' +
|
|
|
|
|
'<hp:outMargin left="0" right="0" top="0" bottom="0"/>' +
|
|
|
|
|
`<hp:img binDataIDRef="${binDataId}" effect="REAL_PIC" alpha="0"/>` +
|
|
|
|
|
'</hp:pic>' +
|
|
|
|
|
'</hp:run>' +
|
|
|
|
|
'</hp:p>'
|
|
|
|
|
@ -879,8 +902,6 @@ export async function exportAsHWPX(
|
|
|
|
|
// 이미지 처리
|
|
|
|
|
let imageBinIds: { step3?: number; step6?: number; sensitiveMap?: number; sensitivityMap?: number } | undefined;
|
|
|
|
|
let extraManifestItems = '';
|
|
|
|
|
let binDataListXml = '';
|
|
|
|
|
let binCount = 0;
|
|
|
|
|
|
|
|
|
|
const processImage = (src: string, binId: number, fileId: string) => {
|
|
|
|
|
// 실제 이미지 포맷 감지 (JPEG vs PNG)
|
|
|
|
|
@ -890,13 +911,10 @@ export async function exportAsHWPX(
|
|
|
|
|
const filePath = `BinData/image${binId}.${ext}`;
|
|
|
|
|
|
|
|
|
|
const base64 = src.replace(/^data:image\/\w+;base64,/, '');
|
|
|
|
|
// HWPX 스펙: 이미지는 ZIP 루트의 BinData/에 저장 (Contents/ 아래 아님)
|
|
|
|
|
zip.file(filePath, base64, { base64: true });
|
|
|
|
|
extraManifestItems += `<opf:item id="${fileId}" href="${filePath}" media-type="${mediaType}"/>`;
|
|
|
|
|
// inMemory="NO": 데이터는 ZIP 내 파일로 저장, 요소 내용은 파일 경로
|
|
|
|
|
binDataListXml +=
|
|
|
|
|
`<hh:binData id="${binId}" isSameDocData="0" compress="NO" inMemory="NO" ` +
|
|
|
|
|
`doNotCompressFile="0" blockDecompress="0" limitWidth="0" limitHeight="0">${filePath}</hh:binData>`;
|
|
|
|
|
binCount++;
|
|
|
|
|
// isEmbeded="1": 한글 HWPX 표준 속성, hc:img binaryItemIDRef가 이 id로 참조
|
|
|
|
|
extraManifestItems += `<opf:item id="${fileId}" href="${filePath}" media-type="${mediaType}" isEmbeded="1"/>`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (images?.step3 || images?.step6) {
|
|
|
|
|
@ -921,16 +939,8 @@ export async function exportAsHWPX(
|
|
|
|
|
processImage(images.sensitivityMap, 4, 'image4');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// header.xml: binDataList를 hh:refList 뒤에 삽입 (HWPML 스펙 준수)
|
|
|
|
|
let headerXml = HEADER_XML;
|
|
|
|
|
if (binCount > 0) {
|
|
|
|
|
const binDataList =
|
|
|
|
|
`<hh:binDataList itemCnt="${binCount}">` +
|
|
|
|
|
binDataListXml +
|
|
|
|
|
'</hh:binDataList>';
|
|
|
|
|
// refList 닫힘 태그 직후에 삽입 (binDataList는 head의 직접 자식)
|
|
|
|
|
headerXml = HEADER_XML.replace('</hh:refList>', '</hh:refList>' + binDataList);
|
|
|
|
|
}
|
|
|
|
|
// header.xml: hh:binDataList 불필요 — hc:img binaryItemIDRef → content.hpf manifest 방식 사용
|
|
|
|
|
const headerXml = HEADER_XML;
|
|
|
|
|
|
|
|
|
|
// Contents
|
|
|
|
|
zip.file('Contents/content.hpf', buildContentHpf(extraManifestItems));
|
|
|
|
|
|