Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add toggle to fit raw image (on by default) #1633

Merged
merged 1 commit into from
May 2, 2024
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
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