UPDATE: Download to PNG the visible part of the selected area

This downloads only the visible part of the selected area.
To download the full selected area we would need to work a bit more
This commit is contained in:
Nicolás Hatcher
2025-02-23 14:17:06 +01:00
committed by Nicolás Hatcher Andrés
parent ce7318840d
commit eecf6f3c3b
5 changed files with 558 additions and 468 deletions

View File

@@ -20,6 +20,7 @@ import {
Grid2X2, Grid2X2,
Grid2x2Check, Grid2x2Check,
Grid2x2X, Grid2x2X,
ImageDown,
Italic, Italic,
PaintBucket, PaintBucket,
PaintRoller, PaintRoller,
@@ -70,6 +71,7 @@ type ToolbarProperties = {
onBorderChanged: (border: BorderOptions) => void; onBorderChanged: (border: BorderOptions) => void;
onClearFormatting: () => void; onClearFormatting: () => void;
onIncreaseFontSize: (delta: number) => void; onIncreaseFontSize: (delta: number) => void;
onDownloadPNG: () => void;
fillColor: string; fillColor: string;
fontColor: string; fontColor: string;
bold: boolean; bold: boolean;
@@ -399,6 +401,17 @@ function Toolbar(properties: ToolbarProperties) {
> >
<RemoveFormatting /> <RemoveFormatting />
</StyledButton> </StyledButton>
<StyledButton
type="button"
$pressed={false}
disabled={!canEdit}
onClick={() => {
properties.onDownloadPNG();
}}
title={t("toolbar.selected_png")}
>
<ImageDown />
</StyledButton>
<ColorPicker <ColorPicker
color={properties.fontColor} color={properties.fontColor}

View File

@@ -15,6 +15,8 @@ import {
LAST_COLUMN, LAST_COLUMN,
ROW_HEIGH_SCALE, ROW_HEIGH_SCALE,
} from "../WorksheetCanvas/constants"; } from "../WorksheetCanvas/constants";
import type WorksheetCanvas from "../WorksheetCanvas/worksheetCanvas";
import { devicePixelRatio } from "../WorksheetCanvas/worksheetCanvas";
import { import {
CLIPBOARD_ID_SESSION_STORAGE_KEY, CLIPBOARD_ID_SESSION_STORAGE_KEY,
getNewClipboardId, getNewClipboardId,
@@ -30,6 +32,9 @@ import useKeyboardNavigation from "./useKeyboardNavigation";
const Workbook = (props: { model: Model; workbookState: WorkbookState }) => { const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
const { model, workbookState } = props; const { model, workbookState } = props;
const rootRef = useRef<HTMLDivElement | null>(null); const rootRef = useRef<HTMLDivElement | null>(null);
const worksheetRef = useRef<{
getCanvas: () => WorksheetCanvas | null;
}>(null);
// Calling `setRedrawId((id) => id + 1);` forces a redraw // Calling `setRedrawId((id) => id + 1);` forces a redraw
// This is needed because `model` or `workbookState` can change without React being aware of it // This is needed because `model` or `workbookState` can change without React being aware of it
@@ -548,6 +553,59 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
onIncreaseFontSize={(delta: number) => { onIncreaseFontSize={(delta: number) => {
onIncreaseFontSize(delta); onIncreaseFontSize(delta);
}} }}
onDownloadPNG={() => {
// creates a new canvas element in the visible part of the the selected area
const worksheetCanvas = worksheetRef.current?.getCanvas();
if (!worksheetCanvas) {
return;
}
const {
range: [rowStart, columnStart, rowEnd, columnEnd],
} = model.getSelectedView();
const { topLeftCell, bottomRightCell } =
worksheetCanvas.getVisibleCells();
const firstRow = Math.max(rowStart, topLeftCell.row);
const firstColumn = Math.max(columnStart, topLeftCell.column);
const lastRow = Math.min(rowEnd, bottomRightCell.row);
const lastColumn = Math.min(columnEnd, bottomRightCell.column);
let [x, y] = worksheetCanvas.getCoordinatesByCell(
firstRow,
firstColumn,
);
const [x1, y1] = worksheetCanvas.getCoordinatesByCell(
lastRow + 1,
lastColumn + 1,
);
const width = (x1 - x) * devicePixelRatio;
const height = (y1 - y) * devicePixelRatio;
x *= devicePixelRatio;
y *= devicePixelRatio;
const capturedCanvas = document.createElement("canvas");
capturedCanvas.width = width;
capturedCanvas.height = height;
const ctx = capturedCanvas.getContext("2d");
if (!ctx) {
return;
}
ctx.drawImage(
worksheetCanvas.canvas,
x,
y,
width,
height,
0,
0,
width,
height,
);
const downloadLink = document.createElement("a");
downloadLink.href = capturedCanvas.toDataURL("image/png");
downloadLink.download = "ironcalc.png";
downloadLink.click();
}}
onBorderChanged={(border: BorderOptions): void => { onBorderChanged={(border: BorderOptions): void => {
const { const {
sheet, sheet,
@@ -640,6 +698,7 @@ const Workbook = (props: { model: Model; workbookState: WorkbookState }) => {
refresh={(): void => { refresh={(): void => {
setRedrawId((id) => id + 1); setRedrawId((id) => id + 1);
}} }}
ref={worksheetRef}
/> />
<SheetTabBar <SheetTabBar

View File

@@ -1,6 +1,13 @@
import { type Model, columnNameFromNumber } from "@ironcalc/wasm"; import { type Model, columnNameFromNumber } from "@ironcalc/wasm";
import { styled } from "@mui/material/styles"; import { styled } from "@mui/material/styles";
import { useEffect, useLayoutEffect, useRef, useState } from "react"; import {
forwardRef,
useEffect,
useImperativeHandle,
useLayoutEffect,
useRef,
useState,
} from "react";
import Editor from "../Editor/Editor"; import Editor from "../Editor/Editor";
import { import {
COLUMN_WIDTH_SCALE, COLUMN_WIDTH_SCALE,
@@ -34,11 +41,15 @@ function useWindowSize() {
return size; return size;
} }
function Worksheet(props: { const Worksheet = forwardRef(
(
props: {
model: Model; model: Model;
workbookState: WorkbookState; workbookState: WorkbookState;
refresh: () => void; refresh: () => void;
}) { },
ref,
) => {
const canvasElement = useRef<HTMLCanvasElement>(null); const canvasElement = useRef<HTMLCanvasElement>(null);
const worksheetElement = useRef<HTMLDivElement>(null); const worksheetElement = useRef<HTMLDivElement>(null);
@@ -62,6 +73,10 @@ function Worksheet(props: {
const { model, workbookState, refresh } = props; const { model, workbookState, refresh } = props;
const [clientWidth, clientHeight] = useWindowSize(); const [clientWidth, clientHeight] = useWindowSize();
useImperativeHandle(ref, () => ({
getCanvas: () => worksheetCanvas.current,
}));
useEffect(() => { useEffect(() => {
const canvasRef = canvasElement.current; const canvasRef = canvasElement.current;
const columnGuideRef = columnResizeGuide.current; const columnGuideRef = columnResizeGuide.current;
@@ -409,7 +424,8 @@ function Worksheet(props: {
const text = model.getCellContent(sheet, row, column); const text = model.getCellContent(sheet, row, column);
const editorWidth = const editorWidth =
model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE; model.getColumnWidth(sheet, column) * COLUMN_WIDTH_SCALE;
const editorHeight = model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE; const editorHeight =
model.getRowHeight(sheet, row) * ROW_HEIGH_SCALE;
workbookState.setEditingCell({ workbookState.setEditingCell({
sheet, sheet,
row, row,
@@ -514,7 +530,8 @@ function Worksheet(props: {
/> />
</Wrapper> </Wrapper>
); );
} },
);
const Spacer = styled("div")` const Spacer = styled("div")`
position: absolute; position: absolute;

View File

@@ -25,6 +25,7 @@
"vertical_align_bottom": "Align bottom", "vertical_align_bottom": "Align bottom",
"vertical_align_middle": " Align middle", "vertical_align_middle": " Align middle",
"vertical_align_top": "Align top", "vertical_align_top": "Align top",
"selected_png": "Export Selected area as PNG",
"format_menu": { "format_menu": {
"auto": "Auto", "auto": "Auto",
"number": "Number", "number": "Number",

View File

@@ -43,21 +43,21 @@
"devDependencies": { "devDependencies": {
"@biomejs/biome": "1.9.4", "@biomejs/biome": "1.9.4",
"@chromatic-com/storybook": "^3.2.4", "@chromatic-com/storybook": "^3.2.4",
"@storybook/addon-essentials": "^8.5.3", "@storybook/addon-essentials": "^8.6.0",
"@storybook/addon-interactions": "^8.5.3", "@storybook/addon-interactions": "^8.6.0",
"@storybook/blocks": "^8.5.3", "@storybook/blocks": "^8.6.0",
"@storybook/react": "^8.5.3", "@storybook/react": "^8.6.0",
"@storybook/react-vite": "^8.5.3", "@storybook/react-vite": "^8.6.0",
"@storybook/test": "^8.5.3", "@storybook/test": "^8.6.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"storybook": "^8.5.3", "storybook": "^8.6.0",
"ts-node": "^10.9.2", "ts-node": "^10.9.2",
"typescript": "~5.6.2", "typescript": "~5.6.2",
"vite": "^6.0.5", "vite": "^6.2.0",
"vite-plugin-svgr": "^4.2.0", "vite-plugin-svgr": "^4.2.0",
"vitest": "^2.0.5" "vitest": "^3.0.7"
}, },
"peerDependencies": { "peerDependencies": {
"@types/react": "^18.0.0 || ^19.0.0", "@types/react": "^18.0.0 || ^19.0.0",