Skip to content

Commit

Permalink
Add toggle to fit raw image (on by default)
Browse files Browse the repository at this point in the history
  • Loading branch information
axelboc committed May 2, 2024
1 parent 11084ed commit efb6da0
Show file tree
Hide file tree
Showing 11 changed files with 153 additions and 52 deletions.
1 change: 1 addition & 0 deletions packages/app/src/vis-packs/core/configs.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { RawConfigProvider } from './raw/config';
export { MatrixConfigProvider } from './matrix/config';
export { LineConfigProvider } from './line/config';
export { HeatmapConfigProvider } from './heatmap/config';
Expand Down
28 changes: 22 additions & 6 deletions packages/app/src/vis-packs/core/raw/RawToolbar.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,37 @@
import { ExportMenu, Toolbar } from '@h5web/lib';
import { ExportMenu, Separator, ToggleBtn, Toolbar } from '@h5web/lib';
import { MdOutlineFitScreen } from 'react-icons/md';

import type { ExportFormat, ExportURL } from '../../../providers/models';
import type { RawConfig } from './config';

interface Props {
isImage: boolean;
config: RawConfig;
getExportURL: ((format: ExportFormat) => ExportURL) | undefined;
}

function RawToolbar(props: Props) {
const { getExportURL } = props;
const { isImage, config, getExportURL } = props;
const { fitImage, toggleFitImage } = config;

return (
<Toolbar>
<ToggleBtn
label="Fit image"
icon={MdOutlineFitScreen}
value={fitImage}
disabled={!isImage}
onToggle={toggleFitImage}
/>

{getExportURL && (
<ExportMenu
entries={[{ format: 'json', url: getExportURL('json') }]}
align="right"
/>
<>
<Separator />
<ExportMenu
entries={[{ format: 'json', url: getExportURL('json') }]}
align="right"
/>
</>
)}
</Toolbar>
);
Expand Down
54 changes: 35 additions & 19 deletions packages/app/src/vis-packs/core/raw/RawVisContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { RawVis } from '@h5web/lib';
import { RawImageVis, RawVis } from '@h5web/lib';
import { isBinaryImage } from '@h5web/lib/src/vis/raw/utils';
import { assertDataset, assertNonNullShape } from '@h5web/shared/guards';
import { createPortal } from 'react-dom';

Expand All @@ -7,38 +8,53 @@ import visualizerStyles from '../../../visualizer/Visualizer.module.css';
import type { VisContainerProps } from '../../models';
import VisBoundary from '../../VisBoundary';
import ValueFetcher from '../ValueFetcher';
import { useRawConfig } from './config';
import RawToolbar from './RawToolbar';

function RawVisContainer(props: VisContainerProps) {
const { entity, toolbarContainer } = props;
assertDataset(entity);
assertNonNullShape(entity);

const config = useRawConfig();
const { getExportURL } = useDataContext();

return (
<VisBoundary>
<ValueFetcher
dataset={entity}
render={(value) => (
<>
{toolbarContainer &&
createPortal(
<RawToolbar
getExportURL={
getExportURL &&
((format) => getExportURL(format, entity, undefined, value))
}
/>,
toolbarContainer,
render={(value) => {
const isImage = value instanceof Uint8Array && isBinaryImage(value);

return (
<>
{toolbarContainer &&
createPortal(
<RawToolbar
isImage={isImage}
config={config}
getExportURL={
getExportURL &&
((format) =>
getExportURL(format, entity, undefined, value))
}
/>,
toolbarContainer,
)}

{isImage ? (
<RawImageVis
className={visualizerStyles.vis}
value={value}
title={entity.name}
fit={config.fitImage}
/>
) : (
<RawVis className={visualizerStyles.vis} value={value} />
)}
<RawVis
className={visualizerStyles.vis}
value={value}
title={entity.name}
/>
</>
)}
</>
);
}}
/>
</VisBoundary>
);
Expand Down
42 changes: 42 additions & 0 deletions packages/app/src/vis-packs/core/raw/config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { createContext, useContext, useState } from 'react';
import type { StoreApi } from 'zustand';
import { createStore, useStore } from 'zustand';
import { persist } from 'zustand/middleware';

import type { ConfigProviderProps } from '../../models';

export interface RawConfig {
fitImage: boolean;
toggleFitImage: () => void;
}

function createRawConfigStore() {
return createStore<RawConfig>()(
persist(
(set): RawConfig => ({
fitImage: true,
toggleFitImage: () => set((state) => ({ fitImage: !state.fitImage })),
}),
{
name: 'h5web:raw',
version: 1,
},
),
);
}

const StoreContext = createContext({} as StoreApi<RawConfig>);

export function RawConfigProvider(props: ConfigProviderProps) {
const { children } = props;

const [store] = useState(createRawConfigStore);

return (
<StoreContext.Provider value={store}>{children}</StoreContext.Provider>
);
}

export function useRawConfig(): RawConfig {
return useStore(useContext(StoreContext));
}
2 changes: 2 additions & 0 deletions packages/app/src/vis-packs/core/visualizations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
HeatmapConfigProvider,
LineConfigProvider,
MatrixConfigProvider,
RawConfigProvider,
RgbConfigProvider,
} from './configs';
import {
Expand Down Expand Up @@ -70,6 +71,7 @@ export const CORE_VIS = {
name: Vis.Raw,
Icon: FiCpu,
Container: RawVisContainer,
ConfigProvider: RawConfigProvider,
supportsDataset: hasNonNullShape,
},

Expand Down
1 change: 1 addition & 0 deletions packages/lib/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export { default as SnapshotBtn } from './toolbar/controls/SnapshotBtn';
export { default as ExportMenu } from './toolbar/controls/ExportMenu';
export { default as CellWidthInput } from './toolbar/controls/CellWidthInput';
export { default as RawVis } from './vis/raw/RawVis';
export { default as RawImageVis } from './vis/raw/RawImageVis';
export { default as ScalarVis } from './vis/scalar/ScalarVis';
export { default as RgbVis } from './vis/rgb/RgbVis';
export { default as VisMesh } from './vis/shared/VisMesh';
Expand Down
21 changes: 21 additions & 0 deletions packages/lib/src/vis/raw/RawImageVis.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
.root {
flex: 1; /* fill height if inside flex container in consumer app */
overflow: auto;
scrollbar-width: thin;
}

.img {
display: block;
}

.img[data-fit] {
max-width: 100%;
max-height: 100%;
}

.img:not([data-fit]) {
/* Remove size restriction explicitly in case consumer app applies
* its own "responsive image" styles (like VS Code). */
max-width: none;
max-height: none;
}
26 changes: 26 additions & 0 deletions packages/lib/src/vis/raw/RawImageVis.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ClassStyleAttrs } from '../models';
import styles from './RawImageVis.module.css';

interface Props extends ClassStyleAttrs {
value: Uint8Array;
title?: string;
fit?: boolean;
}

function RawImageVis(props: Props) {
const { value, title, fit, className = '', style } = props;

return (
<div className={`${styles.root} ${className}`} style={style}>
<img
className={styles.img}
src={URL.createObjectURL(new Blob([value]))}
alt={title}
data-keep-colors
data-fit={fit || undefined}
/>
</div>
);
}

export default RawImageVis;
11 changes: 1 addition & 10 deletions packages/lib/src/vis/raw/RawVis.module.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
.root {
flex: 1; /* fill height if inside flex container in consumer app */
overflow: auto;
scrollbar-width: thin;
}

.raw {
Expand All @@ -12,16 +13,6 @@
font-size: inherit;
}

.img {
display: block;

/* Ensure image size is not restricted, since container can scroll.
(This notably overrides VS Code's default webview styles, which include
`img { max-width/height: 100%; }`.) */
max-width: none;
max-height: none;
}

.fallback {
composes: fallback from global;
}
17 changes: 1 addition & 16 deletions packages/lib/src/vis/raw/RawVis.tsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
import type { ClassStyleAttrs } from '../models';
import styles from './RawVis.module.css';
import { isImage } from './utils';

const LARGE_THRESHOLD = 1_000_000;

interface Props extends ClassStyleAttrs {
value: unknown;
title?: string;
}

function RawVis(props: Props) {
const { value, title, className = '', style } = props;

if (value instanceof Uint8Array && isImage(value)) {
return (
<div className={`${styles.root} ${className}`} style={style}>
<img
className={styles.img}
src={URL.createObjectURL(new Blob([value]))}
alt={title}
data-keep-colors
/>
</div>
);
}
const { value, className = '', style } = props;

const valueAsStr =
value instanceof Uint8Array
Expand Down
2 changes: 1 addition & 1 deletion packages/lib/src/vis/raw/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ const MAGIC_NUMBERS = [
[0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a], // PNG
];

export function isImage(binArray: Uint8Array): boolean {
export function isBinaryImage(binArray: Uint8Array): boolean {
return MAGIC_NUMBERS.some(
(nums) => binArray.slice(0, nums.length).toString() === nums.toString(),
);
Expand Down

0 comments on commit efb6da0

Please sign in to comment.