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

This commit is contained in:
upstream-mirror
2026-09-23 16:12:52 +05:00
commit a774ac673a
4541 changed files with 770499 additions and 0 deletions

View 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,
}
}
}

View 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)
})
})
})

View 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
}
}

View 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: {},
}
}
}