snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
23
packages/tldraw/src/lib/assets/BookmarkAssetUtil.ts
Normal file
23
packages/tldraw/src/lib/assets/BookmarkAssetUtil.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import {
|
||||
AssetUtil,
|
||||
TLBookmarkAsset,
|
||||
bookmarkAssetMigrations,
|
||||
bookmarkAssetProps,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
/** @public */
|
||||
export class BookmarkAssetUtil extends AssetUtil<TLBookmarkAsset> {
|
||||
static override type = 'bookmark' as const
|
||||
static override props = bookmarkAssetProps
|
||||
static override migrations = bookmarkAssetMigrations
|
||||
|
||||
override getDefaultProps(): TLBookmarkAsset['props'] {
|
||||
return {
|
||||
title: '',
|
||||
description: '',
|
||||
image: '',
|
||||
favicon: '',
|
||||
src: null,
|
||||
}
|
||||
}
|
||||
}
|
||||
116
packages/tldraw/src/lib/assets/ImageAssetUtil.test.ts
Normal file
116
packages/tldraw/src/lib/assets/ImageAssetUtil.test.ts
Normal file
@@ -0,0 +1,116 @@
|
||||
import { AssetRecordType, MediaHelpers } from '@tldraw/editor'
|
||||
import { describe, expect, it, vi } from 'vitest'
|
||||
import { TestEditor } from '../../test/TestEditor'
|
||||
import { defaultAssetUtils } from '../defaultAssetUtils'
|
||||
import { ImageAssetUtil } from './ImageAssetUtil'
|
||||
|
||||
let editor: TestEditor
|
||||
|
||||
beforeEach(() => {
|
||||
editor = new TestEditor({ assetUtils: defaultAssetUtils })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
editor?.dispose()
|
||||
})
|
||||
|
||||
describe('ImageAssetUtil', () => {
|
||||
describe('getAssetFromFile', () => {
|
||||
it('should include pixelRatio when image has non-1 pixelRatio', async () => {
|
||||
vi.spyOn(MediaHelpers, 'getImageSize').mockResolvedValue({
|
||||
w: 100,
|
||||
h: 200,
|
||||
pixelRatio: 2,
|
||||
})
|
||||
vi.spyOn(MediaHelpers, 'isAnimated').mockResolvedValue(false)
|
||||
|
||||
const file = new File(['test'], 'test.png', { type: 'image/png' })
|
||||
const assetId = AssetRecordType.createId('test')
|
||||
const util = editor.getAssetUtil('image') as ImageAssetUtil
|
||||
const asset = await util.getAssetFromFile(file, assetId)
|
||||
|
||||
expect(asset).not.toBeNull()
|
||||
expect(asset!.props.pixelRatio).toBe(2)
|
||||
expect(asset!.props.w).toBe(100)
|
||||
expect(asset!.props.h).toBe(200)
|
||||
})
|
||||
|
||||
it('should not include pixelRatio when image has pixelRatio of 1', async () => {
|
||||
vi.spyOn(MediaHelpers, 'getImageSize').mockResolvedValue({
|
||||
w: 100,
|
||||
h: 200,
|
||||
pixelRatio: 1,
|
||||
})
|
||||
vi.spyOn(MediaHelpers, 'isAnimated').mockResolvedValue(false)
|
||||
|
||||
const file = new File(['test'], 'test.png', { type: 'image/png' })
|
||||
const assetId = AssetRecordType.createId('test')
|
||||
const util = editor.getAssetUtil('image') as ImageAssetUtil
|
||||
const asset = await util.getAssetFromFile(file, assetId)
|
||||
|
||||
expect(asset).not.toBeNull()
|
||||
expect(asset!.props.pixelRatio).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should pass doc to MediaHelpers.getImageSize for iframe compatibility', async () => {
|
||||
const getImageSizeSpy = vi
|
||||
.spyOn(MediaHelpers, 'getImageSize')
|
||||
.mockResolvedValue({ w: 100, h: 200, pixelRatio: 1 })
|
||||
vi.spyOn(MediaHelpers, 'isAnimated').mockResolvedValue(false)
|
||||
|
||||
const file = new File(['test'], 'test.png', { type: 'image/png' })
|
||||
const assetId = AssetRecordType.createId('test')
|
||||
const util = editor.getAssetUtil('image') as ImageAssetUtil
|
||||
await util.getAssetFromFile(file, assetId)
|
||||
|
||||
expect(getImageSizeSpy).toHaveBeenCalledWith(file, expect.any(Document))
|
||||
})
|
||||
|
||||
it('should set correct asset properties from file', async () => {
|
||||
vi.spyOn(MediaHelpers, 'getImageSize').mockResolvedValue({
|
||||
w: 800,
|
||||
h: 600,
|
||||
pixelRatio: 1,
|
||||
})
|
||||
vi.spyOn(MediaHelpers, 'isAnimated').mockResolvedValue(true)
|
||||
|
||||
const file = new File(['test-content'], 'animated.gif', { type: 'image/gif' })
|
||||
const assetId = AssetRecordType.createId('test2')
|
||||
const util = editor.getAssetUtil('image') as ImageAssetUtil
|
||||
const asset = await util.getAssetFromFile(file, assetId)
|
||||
|
||||
expect(asset).toMatchObject({
|
||||
id: assetId,
|
||||
type: 'image',
|
||||
typeName: 'asset',
|
||||
props: {
|
||||
name: 'animated.gif',
|
||||
src: '',
|
||||
w: 800,
|
||||
h: 600,
|
||||
mimeType: 'image/gif',
|
||||
isAnimated: true,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('should constrain dimensions to maxDimension for static images', async () => {
|
||||
vi.spyOn(MediaHelpers, 'getImageSize').mockResolvedValue({
|
||||
w: 8000,
|
||||
h: 4000,
|
||||
pixelRatio: 1,
|
||||
})
|
||||
vi.spyOn(MediaHelpers, 'isAnimated').mockResolvedValue(false)
|
||||
|
||||
const file = new File(['test'], 'large.png', { type: 'image/png' })
|
||||
const assetId = AssetRecordType.createId('test3')
|
||||
const util = editor.getAssetUtil('image') as ImageAssetUtil
|
||||
const asset = await util.getAssetFromFile(file, assetId)
|
||||
|
||||
expect(asset).not.toBeNull()
|
||||
// Input 8000x4000 should be constrained to 5000x2500
|
||||
expect(asset!.props.w).toBe(5000)
|
||||
expect(asset!.props.h).toBe(2500)
|
||||
})
|
||||
})
|
||||
})
|
||||
77
packages/tldraw/src/lib/assets/ImageAssetUtil.ts
Normal file
77
packages/tldraw/src/lib/assets/ImageAssetUtil.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
import {
|
||||
AssetUtil,
|
||||
DEFAULT_SUPPORTED_IMAGE_TYPES,
|
||||
MediaHelpers,
|
||||
TLAssetId,
|
||||
TLImageAsset,
|
||||
imageAssetMigrations,
|
||||
imageAssetProps,
|
||||
} from '@tldraw/editor'
|
||||
import { DEFAULT_MAX_IMAGE_DIMENSION } from '../defaultExternalContentHandlers'
|
||||
import { containBoxSize } from '../utils/assets/assets'
|
||||
|
||||
/** @public */
|
||||
export class ImageAssetUtil extends AssetUtil<TLImageAsset> {
|
||||
static override type = 'image' as const
|
||||
static override props = imageAssetProps
|
||||
static override migrations = imageAssetMigrations
|
||||
|
||||
override options: {
|
||||
maxDimension: number
|
||||
supportedMimeTypes: readonly string[] | null
|
||||
} = {
|
||||
maxDimension: DEFAULT_MAX_IMAGE_DIMENSION,
|
||||
supportedMimeTypes: null,
|
||||
}
|
||||
|
||||
override getDefaultProps(): TLImageAsset['props'] {
|
||||
return {
|
||||
w: 0,
|
||||
h: 0,
|
||||
name: '',
|
||||
isAnimated: false,
|
||||
mimeType: null,
|
||||
src: null,
|
||||
}
|
||||
}
|
||||
|
||||
override getSupportedMimeTypes(): readonly string[] {
|
||||
return this.options.supportedMimeTypes ?? DEFAULT_SUPPORTED_IMAGE_TYPES
|
||||
}
|
||||
|
||||
override async getAssetFromFile(file: File, assetId: TLAssetId): Promise<TLImageAsset | null> {
|
||||
const doc = this.editor.getContainerDocument()
|
||||
const size = await MediaHelpers.getImageSize(file, doc)
|
||||
const isAnimated = await MediaHelpers.isAnimated(file)
|
||||
const pixelRatio = 'pixelRatio' in size && size.pixelRatio !== 1 ? size.pixelRatio : undefined
|
||||
|
||||
const assetInfo: TLImageAsset = {
|
||||
id: assetId,
|
||||
type: 'image',
|
||||
typeName: 'asset',
|
||||
props: {
|
||||
name: file.name,
|
||||
src: '',
|
||||
w: size.w,
|
||||
h: size.h,
|
||||
fileSize: file.size,
|
||||
mimeType: file.type,
|
||||
isAnimated,
|
||||
...(pixelRatio ? { pixelRatio } : undefined),
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
|
||||
const maxDimension = this.options.maxDimension
|
||||
if (maxDimension && isFinite(maxDimension)) {
|
||||
const originalSize = { w: assetInfo.props.w, h: assetInfo.props.h }
|
||||
const resizedSize = containBoxSize(originalSize, { w: maxDimension, h: maxDimension })
|
||||
if (originalSize !== resizedSize && MediaHelpers.isStaticImageType(file.type)) {
|
||||
assetInfo.props.w = resizedSize.w
|
||||
assetInfo.props.h = resizedSize.h
|
||||
}
|
||||
}
|
||||
|
||||
return assetInfo
|
||||
}
|
||||
}
|
||||
64
packages/tldraw/src/lib/assets/VideoAssetUtil.ts
Normal file
64
packages/tldraw/src/lib/assets/VideoAssetUtil.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
AssetUtil,
|
||||
DEFAULT_SUPPORT_VIDEO_TYPES,
|
||||
MediaHelpers,
|
||||
TLAssetId,
|
||||
TLVideoAsset,
|
||||
videoAssetMigrations,
|
||||
videoAssetProps,
|
||||
} from '@tldraw/editor'
|
||||
|
||||
/** @public */
|
||||
export class VideoAssetUtil extends AssetUtil<TLVideoAsset> {
|
||||
static override type = 'video' as const
|
||||
static override props = videoAssetProps
|
||||
static override migrations = videoAssetMigrations
|
||||
|
||||
override options: {
|
||||
supportedMimeTypes: readonly string[] | null
|
||||
} = {
|
||||
supportedMimeTypes: null,
|
||||
}
|
||||
|
||||
override getDefaultProps(): TLVideoAsset['props'] {
|
||||
return {
|
||||
w: 0,
|
||||
h: 0,
|
||||
name: '',
|
||||
isAnimated: true,
|
||||
mimeType: null,
|
||||
src: null,
|
||||
}
|
||||
}
|
||||
|
||||
override getSupportedMimeTypes(): readonly string[] {
|
||||
return this.options.supportedMimeTypes ?? DEFAULT_SUPPORT_VIDEO_TYPES
|
||||
}
|
||||
|
||||
override async getAssetFromFile(file: File, assetId: TLAssetId): Promise<TLVideoAsset | null> {
|
||||
let fileType = file.type
|
||||
if (fileType === 'video/quicktime') {
|
||||
// hack to make .mov videos work
|
||||
fileType = 'video/mp4'
|
||||
}
|
||||
|
||||
const doc = this.editor.getContainerDocument()
|
||||
const size = await MediaHelpers.getVideoSize(file, doc)
|
||||
|
||||
return {
|
||||
id: assetId,
|
||||
type: 'video',
|
||||
typeName: 'asset',
|
||||
props: {
|
||||
name: file.name,
|
||||
src: '',
|
||||
w: size.w,
|
||||
h: size.h,
|
||||
fileSize: file.size,
|
||||
mimeType: fileType,
|
||||
isAnimated: true,
|
||||
},
|
||||
meta: {},
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user