Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,10 @@ export const Menu: FC = () => {
key: 'exportCurrentPageAsSVG',
label: t({ id: 'export.currentPageAsSVG' }),
},
{
key: 'exportCurrentPageAsPNG',
label: t({ id: 'export.currentPageAsPNG' }),
},
],
},
{
Expand Down Expand Up @@ -101,6 +105,9 @@ export const Menu: FC = () => {
case 'exportCurrentPageAsSVG':
exportService.exportCurrentPageSVG(editor);
break;
case 'exportCurrentPageAsPNG':
exportService.exportCurrentPagePNG(editor);
break;
case 'keepToolSelectedAfterUse':
case 'invertZoomDirection':
case 'highlightLayersOnHover':
Expand Down
1 change: 1 addition & 0 deletions apps/suika/src/locale/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@
"import.originFile": "Import local file",
"export.originFile": "Save local copy",
"export.currentPageAsSVG": "Export current page as SVG",
"export.currentPageAsPNG": "Export current page as PNG",

"preference": "Preference",
"keepToolSelectedAfterUse": "Keep tool selected after use",
Expand Down
1 change: 1 addition & 0 deletions apps/suika/src/locale/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"import.originFile": "从本地导入",
"export.originFile": "导出到本地",
"export.currentPageAsSVG": "导出当前页面为 SVG",
"export.currentPageAsPNG": "导出当前页面为 PNG",

"preference": "偏好设置",
"keepToolSelectedAfterUse": "使用工具后保持选中状态",
Expand Down
2 changes: 1 addition & 1 deletion packages/core/src/clipboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class ClipboardManager {
if (graphs.length === 0) {
return;
}
const svgStr = toSVG(graphs);
const svgStr = toSVG(graphs).svg;
navigator.clipboard.writeText(svgStr).then(() => {
console.log('SVG copied');
});
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/graphics/frame/frame.ts
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,10 @@ export class SuikaFrame extends SuikaGraphics<FrameAttrs> {
}

override toSVGSegment(offset: IPoint) {
if (!this.isVisible()) {
return '';
}

let content = '';
if (!this.isGroup()) {
content += super.toSVGSegment(offset);
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/graphics/graphics/graphics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -728,6 +728,9 @@
}

toSVGSegment(offset: IPoint) {
if (!this.isVisible()) {
return '';
}
const tagHead = this.getSVGTagHead(offset);
if (!tagHead) {
console.warn(
Expand Down Expand Up @@ -816,7 +819,7 @@
return content;
}

protected getSVGTagHead(_offset?: IPoint) {

Check warning on line 822 in packages/core/src/graphics/graphics/graphics.ts

View workflow job for this annotation

GitHub Actions / eslint

'_offset' is defined but never used
return '';
}

Expand Down
33 changes: 29 additions & 4 deletions packages/core/src/service/export_service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { type SuikaEditor } from '../editor';
import { toSVG } from '../to_svg';
import { toPNGBlob, toSVG } from '../to_svg';

export const exportService = {
exportOriginFile: (editor: SuikaEditor, filename = 'design') => {
Expand All @@ -12,19 +12,44 @@ export const exportService = {

exportCurrentPageSVG: (editor: SuikaEditor) => {
const currentPage = editor.doc.getCurrentCanvas();
const graphicsItems = currentPage.getChildren();
const graphicsItems = currentPage
.getChildren()
.filter((item) => item.isVisible());

if (graphicsItems.length === 0) {
// TODO: if no graphics items, show error message
console.error('No graphics items to export');
return;
}

const svg = toSVG(graphicsItems);
const svg = toSVG(graphicsItems).svg;
const blob = new Blob([svg], {
type: 'image/svg+xml',
});
download(blob, 'design.svg');

const suffix = currentPage.attrs.objectName;
download(blob, `${suffix}.svg`);
},

exportCurrentPagePNG: async (editor: SuikaEditor) => {
const currentPage = editor.doc.getCurrentCanvas();
const graphicsItems = currentPage
.getChildren()
.filter((item) => item.isVisible());

if (graphicsItems.length === 0) {
// TODO: if no graphics items, show error message
console.error('No graphics items to export');
return;
}

try {
const blob = await toPNGBlob(graphicsItems);
const suffix = currentPage.attrs.objectName;
download(blob, `${suffix}.png`);
} catch (error) {
console.error('Failed to export PNG:', error);
}
},
};

Expand Down
61 changes: 60 additions & 1 deletion packages/core/src/to_svg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import { boxToRect, mergeBoxes } from '@suika/geo';
import { type SuikaGraphics } from './graphics';

export const toSVG = (graphicsArr: SuikaGraphics[]) => {
graphicsArr = graphicsArr.filter((item) => item.isVisible());

// FIXME: to sort
const mergedBbox = mergeBoxes(
graphicsArr.map((el) => el.getBboxWithStroke()),
Expand All @@ -21,5 +23,62 @@ export const toSVG = (graphicsArr: SuikaGraphics[]) => {
content += graphics.toSVGSegment(offset);
}

return svgHead + content + svgTail;
return {
width: mergedRect.width,
height: mergedRect.height,
svg: svgHead + content + svgTail,
};
};

export const toPNGBlob = async (
graphicsArr: SuikaGraphics[],
): Promise<Blob> => {
const svgData = toSVG(graphicsArr);
const { svg, width, height } = svgData;

const svgBlob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' });
const svgDataUrl = URL.createObjectURL(svgBlob);

// create image object and load SVG
const img = new Image();
img.crossOrigin = 'anonymous';

return new Promise((resolve, reject) => {
img.onload = () => {
try {
const canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
const ctx = canvas.getContext('2d');

if (!ctx) {
URL.revokeObjectURL(svgDataUrl);
reject(new Error('Failed to get canvas context'));
return;
}

ctx.drawImage(img, 0, 0);

// convert to blob
canvas.toBlob((blob) => {
URL.revokeObjectURL(svgDataUrl);
if (blob) {
resolve(blob);
} else {
reject(new Error('Failed to convert canvas to blob'));
}
}, 'image/png');
} catch (error) {
URL.revokeObjectURL(svgDataUrl);
reject(error);
}
};

img.onerror = () => {
URL.revokeObjectURL(svgDataUrl);
reject(new Error('Failed to load SVG'));
};

img.src = svgDataUrl;
});
};
Loading