Images inline in the Claude Code transcript through the terminal's kitty graphics: files Claude sends or reads, pasted images, image paths, locally and over ssh

Images inline in the Claude Code transcript, drawn by your terminal with kitty graphics. Screenshots Claude takes and sends you show up right under the message instead of as a path to copy and open somewhere else. It works the same in a session on a remote server over ssh, with nothing copied back to your machine.

| Where | How |
|---|---|
Files Claude sends you (SendUserFile, attachments of SendUserMessage) | inline, under the › [image] lines |
Images Claude reads with Read | inline, under the read |
Images you paste ([Image #N]) | inline, under your message |
| Image paths in Claude's replies, your prompts and shell output, when the file exists | a ▸ path line; click it to unfold the image, click again to fold it |
| A path handed to the session from outside (see Hotkeys) | a pane beside the transcript |
PNG is drawn as is. JPEG and GIF (first frame) are decoded by a small bundled decoder and scaled to fit, so a server needs no image tools. WebP shows a line saying it is not drawn.
The plugin runs inside Claude Code, which is on the server in a remote session. It reads the image file there and hands the bytes to the terminal inside the normal output stream. Your terminal decodes and paints them. Nothing is copied with scp, and any server with the plugin installed works the same way.
/tui fullscreen) for clicking ▸ lines open/plugin marketplace add nnemirovsky/cc-inline-images
/plugin install inline-images@inline-images
Install it on every machine Claude Code runs on, including servers you ssh into.
The plugin watches an inbox file for its session and opens a pane with the image whose path appears there. A hotkey can use that to show the image under the cursor or in the selection, from the machine where the session runs:
# Local session: name the file after the terminal pane, if your terminal exports one
printf '%s' "$path" > ~/.cache/inline-images/inbox/"$AGTERM_SESSION_ID"
# Any session, local or remote: name it after the Claude Code session id
ssh server "mkdir -p ~/.cache/inline-images/inbox && cat > ~/.cache/inline-images/inbox/$session_id" <<<"$path"
The plugin checks once a second and empties the file when it takes the request, so a file still holding the path after a couple of seconds means no session took it. Relative paths resolve against the session's working directory.
[Image #N] marks.claude --bg, viewed with claude attach) do not ask the terminal whether it can draw images, so they draw nothing unless told it can. Set CLAUDE_CODE_FORCE_TERMINAL_IMAGES=1 in the env block of ~/.claude/settings.json on the machine that runs them, and only when every terminal you attach from draws kitty graphics. Without it the plugin leaves those sessions alone.claude plugin validate .
claude plugin test .
claude --plugin-dir .
MIT. Bundles jpeg-js (BSD-3-Clause, with Apache-2.0 parts) and omggif (MIT); their licenses are in hooks/vendor/.
hooks/register.tsx 320 lines1import { atom, read, update } from 'claude-code'
2import type { EngineInterface, Register, RenderInput } from 'claude-code'
3
4import { cells, imagePaths, mimeOf, picture } from './image'
5import type { Picture } from './image'
6
7// Images in the transcript, drawn by the terminal (kitty graphics) right where
8// they come up:
9//
10// * files Claude sends you (SendUserFile, SendUserMessage attachments)
11// * images Claude reads (Read)
12// * images you paste ([Image #N])
13// * image paths in replies, prompts and shell output: a line to click open
14//
15// and a pane for an image path handed to the session from outside (the
16// agterm hotkey writes it to an inbox file next to the session).
17//
18// The mod runs where Claude Code runs, so on a server it reads the file there
19// and the pixels reach the terminal inside the ssh stream: no copying.
20
21const open = atom({ plugin: 'inline-images', key: 'open' } as const, [])
22const pane = atom({ plugin: 'inline-images', key: 'pane' } as const, null)
23
24const PANE = 'inline-image'
25const INBOX = '.cache/inline-images/inbox'
26
27// Decoded pictures and file checks, so a redraw does not decode again.
28const pictures = new Map<string, Picture>()
29const existing = new Map<string, boolean>()
30type Pasted = { mime: string; base64: string }
31let pasted: { key: string; images: Pasted[] }[] | undefined
32
33function remember(key: string, pic: Picture): Picture {
34 if (pictures.size > 64) pictures.delete(pictures.keys().next().value!)
35 pictures.set(key, pic)
36 return pic
37}
38
39function fromBase64(key: string, base64: string, mime: string): Picture {
40 return pictures.get(key) ?? remember(key, picture(Uint8Array.fromBase64(base64), mime))
41}
42
43// A background session (`claude --bg`, viewed through `claude attach`) never
44// asks the terminal about kitty graphics, so it draws an Image as its alt text
45// unless CLAUDE_CODE_FORCE_TERMINAL_IMAGES is set. Without it the mod stays out
46// of the way and leaves the inbox alone, so a hotkey falls back to its viewer.
47let isBackground: boolean | undefined
48let cannotDraw: boolean | undefined
49
50async function background($: EngineInterface): Promise<boolean> {
51 isBackground ??= (await $.env.get('CLAUDE_CODE_SESSION_KIND')) === 'bg'
52 return isBackground
53}
54
55async function inBackground($: EngineInterface): Promise<boolean> {
56 cannotDraw ??= (await background($)) && !(await $.env.get('CLAUDE_CODE_FORCE_TERMINAL_IMAGES'))
57 return cannotDraw
58}
59
60async function resolvePath($: EngineInterface, path: string): Promise<string> {
61 if (path.startsWith('~/')) return `${(await $.env.get('HOME')) ?? ''}${path.slice(1)}`
62 if (path.startsWith('/')) return path
63 return `${await $.session.cwd()}/${path.replace(/^\.\//, '')}`
64}
65
66async function isImageFile($: EngineInterface, path: string): Promise<boolean> {
67 const cached = existing.get(path)
68 if (cached !== undefined) return cached
69 let isFile = false
70 try {
71 isFile = (await $.fs.stat(path)).kind === 'file'
72 } catch {
73 isFile = false
74 }
75 existing.set(path, isFile)
76 return isFile
77}
78
79async function fromFile($: EngineInterface, path: string): Promise<Picture> {
80 const mime = mimeOf(path)
81 if (!mime) return { kind: 'error', reason: 'not an image' }
82 try {
83 const stat = await $.fs.stat(path)
84 const key = `${path}@${stat.mtimeMs}`
85 const hit = pictures.get(key)
86 if (hit) return hit
87 const { base64 } = await $.fs.read(path, { as: 'bytes' })
88 return remember(key, picture(Uint8Array.fromBase64(base64), mime))
89 } catch {
90 return { kind: 'error', reason: 'cannot read the file' }
91 }
92}
93
94type ReadImage = { type: 'image'; file: { base64: string; type: string } }
95type Attachment = { path: string; isImage?: boolean }
96
97function readImage(output: unknown): ReadImage | undefined {
98 const o = output as ReadImage | undefined
99 return o?.type === 'image' && typeof o.file?.base64 === 'string' ? o : undefined
100}
101
102function attachments(output: unknown): Attachment[] {
103 const list = (output as { attachments?: Attachment[] } | undefined)?.attachments
104 return Array.isArray(list) ? list.filter(a => a.isImage === true && typeof a.path === 'string') : []
105}
106
107// The images of the user message whose text carries these [Image #N] marks.
108async function pastedImages($: EngineInterface, text: string): Promise<Pasted[]> {
109 const marks = text.match(/\[Image #\d+\]/g) ?? []
110 if (marks.length === 0) return []
111 const find = () => pasted?.findLast(m => marks.every(mark => m.key.includes(mark)))
112 if (!find()) {
113 const messages = await $.session.messages({ as: 'api' })
114 pasted = messages
115 .filter(m => m.role === 'user')
116 .map(m => {
117 const blocks = m.content as { type: string; text?: string; source?: { type: string; media_type?: string; data?: string } }[]
118 return {
119 key: blocks.filter(b => b.type === 'text').map(b => b.text ?? '').join('\n'),
120 images: blocks
121 .filter(b => b.type === 'image' && b.source?.type === 'base64' && b.source.data)
122 .map(b => ({ mime: b.source!.media_type ?? 'image/png', base64: b.source!.data! })),
123 }
124 })
125 .filter(m => m.images.length > 0)
126 }
127 return find()?.images ?? []
128}
129
130// The element table, typed for the terminal; callers check the surface first.
131type Els = { Box: any; Image: any; Text: any; Button: any }
132function els($: EngineInterface, e: RenderInput): Els {
133 return $.ui.resolve(e) as unknown as Els
134}
135
136// One picture as the surface draws it, or a dim line saying why not.
137function drawPicture($: EngineInterface, e: RenderInput, pic: Picture, maxCols: number, maxRows: number, alt: string) {
138 if (e.surface !== 'terminal') return undefined
139 const { Box, Image, Text } = els($, e)
140 if (pic.kind === 'error') return <Text dimColor>{` [${alt}: ${pic.reason}]`}</Text>
141 const size = cells(pic.width, pic.height, maxCols, maxRows)
142 const source =
143 pic.kind === 'png' ? { png: pic.base64 } : { rgba: pic.base64, width: pic.width, height: pic.height }
144 return (
145 <Box marginLeft={2}>
146 <Image source={source} columns={size.columns} rows={size.rows} alt={alt} />
147 </Box>
148 )
149}
150
151function width(e: RenderInput): number {
152 return Math.min(120, (e.viewport?.columns ?? 100) - 8)
153}
154
155// Lines to click open for the image paths a text mentions, each followed by
156// the picture while it is open.
157async function clickable($: EngineInterface, e: RenderInput, text: string) {
158 if (e.surface !== 'terminal') return []
159 const found: { shown: string; path: string }[] = []
160 for (const shown of imagePaths(text)) {
161 const path = await resolvePath($, shown)
162 if (await isImageFile($, path)) found.push({ shown, path })
163 }
164 if (found.length === 0) return []
165 const { Box, Button } = els($, e)
166 const isOpen = await read($, open)
167 const rows = []
168 for (const [i, f] of found.entries()) {
169 const key = `${e.requestId}|${f.path}`
170 const shown = isOpen.includes(key)
171 rows.push(
172 <Box key={`img-${i}`} marginLeft={2}>
173 <Button
174 key={`toggle-${i}`}
175 plain
176 label={`${shown ? '▾' : '▸'} ${f.shown}`}
177 onPress={() => update($, open, list => (list.includes(key) ? list.filter(k => k !== key) : [...list, key]))}
178 />
179 </Box>,
180 )
181 if (shown) rows.push(drawPicture($, e, await fromFile($, f.path), width(e), 30, f.shown))
182 }
183 return rows
184}
185
186export const register: Register = on => {
187 on('session.start', async ($, e, next) => {
188 const home = (await $.env.get('HOME')) ?? ''
189 if (await inBackground($)) return next(e)
190 // The daemon hands a background session the pane id of the terminal that
191 // started it, which belongs to another session; answer to the session id only.
192 const paneId = (await background($)) ? undefined : await $.env.get('AGTERM_SESSION_ID')
193 const names = [await $.session.id(), paneId].filter((n): n is string => !!n)
194 $.clock.every(1000, () => {
195 void (async () => {
196 for (const name of names) {
197 const file = `${home}/${INBOX}/${name}`
198 if (!(await $.fs.exists(file))) continue
199 const requested = (await $.fs.read(file)).trim()
200 if (!requested) continue
201 await $.fs.write(file, '')
202 const path = await resolvePath($, requested)
203 await update($, pane, () => path)
204 await $.ui.open({ id: PANE, title: path.split('/').pop() ?? 'image', focus: true, closeOnEscape: true })
205 }
206 })().catch(() => undefined)
207 })
208 return next(e)
209 })
210
211 // A new prompt may carry pasted images the cache has not seen.
212 on('session.append', { door: 'prompt' }, ($, e, next) => {
213 pasted = undefined
214 return next(e)
215 })
216
217 // Images Claude reads: in fullscreen the reads fold into one group line.
218 on('ui.render', { component: 'ToolGroup' }, async ($, e, next) => {
219 const drawn = await next(e)
220 if (await inBackground($)) return drawn
221 if (e.surface !== 'terminal') return drawn
222 const images = e.props.calls
223 .map((c, i) => ({ c, i, img: c.tool === 'Read' ? readImage(c.output) : undefined }))
224 .filter(x => x.img !== undefined)
225 // Shell output folded into the group: its image paths, to click open.
226 const shell = e.props.calls
227 .filter(c => c.tool === 'Bash')
228 .map(c => (c.output as { stdout?: string } | undefined)?.stdout ?? '')
229 .join('\n')
230 const links = await clickable($, e, shell)
231 if (images.length === 0 && links.length === 0) return drawn
232 const { Box } = els($, e)
233 return (
234 <Box flexDirection="column">
235 {drawn}
236 {images.map(({ c, i, img }) =>
237 drawPicture($, e, fromBase64(c.tool_use_id ?? `${e.requestId}:${i}`, img!.file.base64, img!.file.type), width(e), 24, 'image'),
238 )}
239 {links}
240 </Box>
241 )
242 })
243
244 // A standalone tool row's result: Read images, sent files, shell output.
245 on('ui.render', { component: 'ToolResult' }, async ($, e, next) => {
246 const drawn = await next(e)
247 if (await inBackground($)) return drawn
248 if (e.surface !== 'terminal' || e.props.isErrored) return drawn
249 const { Box } = els($, e)
250 const extra = []
251 const img = e.props.tool === 'Read' ? readImage(e.props.output) : undefined
252 if (img) extra.push(drawPicture($, e, fromBase64(e.props.tool_use_id, img.file.base64, img.file.type), width(e), 24, 'image'))
253 for (const a of attachments(e.props.output)) {
254 extra.push(drawPicture($, e, await fromFile($, await resolvePath($, a.path)), width(e), 24, a.path.split('/').pop() ?? 'image'))
255 }
256 if (e.props.tool === 'Bash') {
257 const out = e.props.output as { stdout?: string } | undefined
258 extra.push(...(await clickable($, e, out?.stdout ?? '')))
259 }
260 if (extra.length === 0) return drawn
261 return (
262 <Box flexDirection="column">
263 {drawn}
264 {extra}
265 </Box>
266 )
267 })
268
269 // Your prompts: pasted images inline, mentioned paths to click open.
270 on('ui.render', { component: 'UserMessage' }, async ($, e, next) => {
271 const drawn = await next(e)
272 if (await inBackground($)) return drawn
273 if (e.surface !== 'terminal' || e.props.origin.kind !== 'composer') return drawn
274 const images = await pastedImages($, e.props.text)
275 const extra = [
276 ...images.map((p, i) => drawPicture($, e, fromBase64(`${e.requestId}:${i}`, p.base64, p.mime), width(e), 16, 'pasted image')),
277 ...(await clickable($, e, e.props.text)),
278 ]
279 if (extra.length === 0) return drawn
280 const { Box } = els($, e)
281 return (
282 <Box flexDirection="column">
283 {drawn}
284 {extra}
285 </Box>
286 )
287 })
288
289 // Claude's replies: mentioned paths to click open.
290 on('ui.render', { component: 'AssistantMessage' }, async ($, e, next) => {
291 const drawn = await next(e)
292 if (await inBackground($)) return drawn
293 const extra = await clickable($, e, e.props.text)
294 if (extra.length === 0) return drawn
295 const { Box } = els($, e)
296 return (
297 <Box flexDirection="column">
298 {drawn}
299 {extra}
300 </Box>
301 )
302 })
303
304 // The pane the hotkey opens: the whole image, fitted to the pane.
305 on('ui.render', { component: 'Pane', requestId: PANE }, async ($, e) => {
306 const { Box, Text } = els($, e)
307 const path = await read($, pane)
308 if (!path) return <Text dimColor>No image.</Text>
309 const pic = await fromFile($, path)
310 return (
311 <Box flexDirection="column">
312 <Text dimColor wrap="truncate-start">
313 {path}
314 </Text>
315 {drawPicture($, e, pic, e.props.bodyColumns - 2, Math.max(4, e.props.scroll.bodyRows - 1), path.split('/').pop() ?? 'image')}
316 </Box>
317 )
318 })
319}
320hooks/image.ts 126 lines1import { decode as decodeJpeg } from './vendor/jpeg.js'
2import { GifReader } from './vendor/omggif.js'
3
4// Turns image bytes into something the terminal's Image element takes: a PNG
5// as is, or decoded RGBA pixels for JPEG and GIF. Pure functions, no `$`.
6
7export type Picture =
8 | { kind: 'png'; base64: string; width: number; height: number }
9 | { kind: 'rgba'; base64: string; width: number; height: number }
10 | { kind: 'error'; reason: string }
11
12// The Image element takes at most 2 MiB of decoded bytes and 2048 pixels a side.
13const MAX_BYTES = 2 * 1024 * 1024
14const MAX_SIDE = 1600
15const MAX_PIXELS = Math.floor(MAX_BYTES / 4)
16
17const MIME: Record<string, string> = {
18 png: 'image/png',
19 jpg: 'image/jpeg',
20 jpeg: 'image/jpeg',
21 gif: 'image/gif',
22 webp: 'image/webp',
23}
24
25export function mimeOf(path: string): string | undefined {
26 const ext = path.toLowerCase().match(/\.([a-z]+)$/)?.[1]
27 return ext ? MIME[ext] : undefined
28}
29
30function pngSize(b: Uint8Array): { width: number; height: number } | undefined {
31 if (b.length < 24 || b[0] !== 0x89 || b[1] !== 0x50 || b[2] !== 0x4e || b[3] !== 0x47) return undefined
32 const at = (i: number) => ((b[i]! << 24) | (b[i + 1]! << 16) | (b[i + 2]! << 8) | b[i + 3]!) >>> 0
33 return { width: at(16), height: at(20) }
34}
35
36// Area-average downscale of RGBA pixels, so text in screenshots stays legible.
37function downscale(src: Uint8Array, w: number, h: number, tw: number, th: number): Uint8Array {
38 const out = new Uint8Array(tw * th * 4)
39 for (let y = 0; y < th; y++) {
40 const y0 = Math.floor((y * h) / th)
41 const y1 = Math.max(y0 + 1, Math.floor(((y + 1) * h) / th))
42 for (let x = 0; x < tw; x++) {
43 const x0 = Math.floor((x * w) / tw)
44 const x1 = Math.max(x0 + 1, Math.floor(((x + 1) * w) / tw))
45 let r = 0
46 let g = 0
47 let b = 0
48 let a = 0
49 for (let sy = y0; sy < y1; sy++) {
50 for (let sx = x0; sx < x1; sx++) {
51 const i = (sy * w + sx) * 4
52 r += src[i]!
53 g += src[i + 1]!
54 b += src[i + 2]!
55 a += src[i + 3]!
56 }
57 }
58 const n = (y1 - y0) * (x1 - x0)
59 const o = (y * tw + x) * 4
60 out[o] = r / n
61 out[o + 1] = g / n
62 out[o + 2] = b / n
63 out[o + 3] = a / n
64 }
65 }
66 return out
67}
68
69function rgba(pixels: Uint8Array, width: number, height: number): Picture {
70 const scale = Math.min(1, MAX_SIDE / width, MAX_SIDE / height, Math.sqrt(MAX_PIXELS / (width * height)))
71 if (scale >= 1) return { kind: 'rgba', base64: pixels.toBase64(), width, height }
72 const tw = Math.max(1, Math.floor(width * scale))
73 const th = Math.max(1, Math.floor(height * scale))
74 return { kind: 'rgba', base64: downscale(pixels, width, height, tw, th).toBase64(), width: tw, height: th }
75}
76
77export function picture(bytes: Uint8Array, mime: string): Picture {
78 try {
79 if (mime === 'image/png') {
80 const size = pngSize(bytes)
81 if (!size) return { kind: 'error', reason: 'not a PNG' }
82 if (bytes.length > MAX_BYTES) return { kind: 'error', reason: 'PNG over 2 MiB' }
83 return { kind: 'png', base64: bytes.toBase64(), ...size }
84 }
85 if (mime === 'image/jpeg') {
86 const img = decodeJpeg(bytes, { useTArray: true, formatAsRGBA: true, maxMemoryUsageInMB: 512 })
87 return rgba(img.data, img.width, img.height)
88 }
89 if (mime === 'image/gif') {
90 const gif = new GifReader(bytes)
91 const pixels = new Uint8Array(gif.width * gif.height * 4)
92 gif.decodeAndBlitFrameRGBA(0, pixels)
93 return rgba(pixels, gif.width, gif.height)
94 }
95 return { kind: 'error', reason: `${mime.replace('image/', '').toUpperCase()} is not drawn` }
96 } catch {
97 return { kind: 'error', reason: 'could not decode' }
98 }
99}
100
101// A cell is about twice as tall as wide, and about 8 image pixels across at
102// common font sizes; small images are not blown up past that.
103export function cells(width: number, height: number, maxCols: number, maxRows: number): { columns: number; rows: number } {
104 const limitCols = Math.max(4, Math.min(255, maxCols))
105 const limitRows = Math.max(1, Math.min(255, maxRows))
106 let columns = Math.min(limitCols, Math.max(4, Math.ceil(width / 8)))
107 let rows = Math.max(1, Math.round((columns * height) / width / 2))
108 if (rows > limitRows) {
109 rows = limitRows
110 columns = Math.max(4, Math.min(limitCols, Math.round((rows * 2 * width) / height)))
111 }
112 return { columns, rows }
113}
114
115// Image paths in free text: absolute, ~/, ./ or relative with a directory.
116const PATH = /(?:~\/|\.{1,2}\/|\/)?(?:[\w@%+=,.~-]+\/)*[\w@%+=,.~-]+\.(?:png|jpe?g|gif|webp)\b/gi
117
118export function imagePaths(text: string): string[] {
119 const seen = new Set<string>()
120 for (const m of text.matchAll(PATH)) {
121 const p = m[0]
122 if (p.includes('/')) seen.add(p)
123 }
124 return [...seen]
125}
126hooks/vendor/jpeg.js 1150 lines1// Vendored from jpeg-js 0.4.4 (lib/decoder.js), BSD-3-Clause / Apache-2.0; see LICENSE-jpeg-js.
2// Changed: ES module export instead of module.exports.
3/* -*- tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- /
4/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab: */
5/*
6 Copyright 2011 notmasteryet
7
8 Licensed under the Apache License, Version 2.0 (the "License");
9 you may not use this file except in compliance with the License.
10 You may obtain a copy of the License at
11
12 http://www.apache.org/licenses/LICENSE-2.0
13
14 Unless required by applicable law or agreed to in writing, software
15 distributed under the License is distributed on an "AS IS" BASIS,
16 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
17 See the License for the specific language governing permissions and
18 limitations under the License.
19*/
20
21// - The JPEG specification can be found in the ITU CCITT Recommendation T.81
22// (www.w3.org/Graphics/JPEG/itu-t81.pdf)
23// - The JFIF specification can be found in the JPEG File Interchange Format
24// (www.w3.org/Graphics/JPEG/jfif3.pdf)
25// - The Adobe Application-Specific JPEG markers in the Supporting the DCT Filters
26// in PostScript Level 2, Technical Note #5116
27// (partners.adobe.com/public/developer/en/ps/sdk/5116.DCT_Filter.pdf)
28
29var JpegImage = (function jpegImage() {
30 "use strict";
31 var dctZigZag = new Int32Array([
32 0,
33 1, 8,
34 16, 9, 2,
35 3, 10, 17, 24,
36 32, 25, 18, 11, 4,
37 5, 12, 19, 26, 33, 40,
38 48, 41, 34, 27, 20, 13, 6,
39 7, 14, 21, 28, 35, 42, 49, 56,
40 57, 50, 43, 36, 29, 22, 15,
41 23, 30, 37, 44, 51, 58,
42 59, 52, 45, 38, 31,
43 39, 46, 53, 60,
44 61, 54, 47,
45 55, 62,
46 63
47 ]);
48
49 var dctCos1 = 4017 // cos(pi/16)
50 var dctSin1 = 799 // sin(pi/16)
51 var dctCos3 = 3406 // cos(3*pi/16)
52 var dctSin3 = 2276 // sin(3*pi/16)
53 var dctCos6 = 1567 // cos(6*pi/16)
54 var dctSin6 = 3784 // sin(6*pi/16)
55 var dctSqrt2 = 5793 // sqrt(2)
56 var dctSqrt1d2 = 2896 // sqrt(2) / 2
57
58 function constructor() {
59 }
60
61 function buildHuffmanTable(codeLengths, values) {
62 var k = 0, code = [], i, j, length = 16;
63 while (length > 0 && !codeLengths[length - 1])
64 length--;
65 code.push({children: [], index: 0});
66 var p = code[0], q;
67 for (i = 0; i < length; i++) {
68 for (j = 0; j < codeLengths[i]; j++) {
69 p = code.pop();
70 p.children[p.index] = values[k];
71 while (p.index > 0) {
72 if (code.length === 0)
73 throw new Error('Could not recreate Huffman Table');
74 p = code.pop();
75 }
76 p.index++;
77 code.push(p);
78 while (code.length <= i) {
79 code.push(q = {children: [], index: 0});
80 p.children[p.index] = q.children;
81 p = q;
82 }
83 k++;
84 }
85 if (i + 1 < length) {
86 // p here points to last code
87 code.push(q = {children: [], index: 0});
88 p.children[p.index] = q.children;
89 p = q;
90 }
91 }
92 return code[0].children;
93 }
94
95 function decodeScan(data, offset,
96 frame, components, resetInterval,
97 spectralStart, spectralEnd,
98 successivePrev, successive, opts) {
99 var precision = frame.precision;
100 var samplesPerLine = frame.samplesPerLine;
101 var scanLines = frame.scanLines;
102 var mcusPerLine = frame.mcusPerLine;
103 var progressive = frame.progressive;
104 var maxH = frame.maxH, maxV = frame.maxV;
105
106 var startOffset = offset, bitsData = 0, bitsCount = 0;
107 function readBit() {
108 if (bitsCount > 0) {
109 bitsCount--;
110 return (bitsData >> bitsCount) & 1;
111 }
112 bitsData = data[offset++];
113 if (bitsData == 0xFF) {
114 var nextByte = data[offset++];
115 if (nextByte) {
116 throw new Error("unexpected marker: " + ((bitsData << 8) | nextByte).toString(16));
117 }
118 // unstuff 0
119 }
120 bitsCount = 7;
121 return bitsData >>> 7;
122 }
123 function decodeHuffman(tree) {
124 var node = tree, bit;
125 while ((bit = readBit()) !== null) {
126 node = node[bit];
127 if (typeof node === 'number')
128 return node;
129 if (typeof node !== 'object')
130 throw new Error("invalid huffman sequence");
131 }
132 return null;
133 }
134 function receive(length) {
135 var n = 0;
136 while (length > 0) {
137 var bit = readBit();
138 if (bit === null) return;
139 n = (n << 1) | bit;
140 length--;
141 }
142 return n;
143 }
144 function receiveAndExtend(length) {
145 var n = receive(length);
146 if (n >= 1 << (length - 1))
147 return n;
148 return n + (-1 << length) + 1;
149 }
150 function decodeBaseline(component, zz) {
151 var t = decodeHuffman(component.huffmanTableDC);
152 var diff = t === 0 ? 0 : receiveAndExtend(t);
153 zz[0]= (component.pred += diff);
154 var k = 1;
155 while (k < 64) {
156 var rs = decodeHuffman(component.huffmanTableAC);
157 var s = rs & 15, r = rs >> 4;
158 if (s === 0) {
159 if (r < 15)
160 break;
161 k += 16;
162 continue;
163 }
164 k += r;
165 var z = dctZigZag[k];
166 zz[z] = receiveAndExtend(s);
167 k++;
168 }
169 }
170 function decodeDCFirst(component, zz) {
171 var t = decodeHuffman(component.huffmanTableDC);
172 var diff = t === 0 ? 0 : (receiveAndExtend(t) << successive);
173 zz[0] = (component.pred += diff);
174 }
175 function decodeDCSuccessive(component, zz) {
176 zz[0] |= readBit() << successive;
177 }
178 var eobrun = 0;
179 function decodeACFirst(component, zz) {
180 if (eobrun > 0) {
181 eobrun--;
182 return;
183 }
184 var k = spectralStart, e = spectralEnd;
185 while (k <= e) {
186 var rs = decodeHuffman(component.huffmanTableAC);
187 var s = rs & 15, r = rs >> 4;
188 if (s === 0) {
189 if (r < 15) {
190 eobrun = receive(r) + (1 << r) - 1;
191 break;
192 }
193 k += 16;
194 continue;
195 }
196 k += r;
197 var z = dctZigZag[k];
198 zz[z] = receiveAndExtend(s) * (1 << successive);
199 k++;
200 }
201 }
202 var successiveACState = 0, successiveACNextValue;
203 function decodeACSuccessive(component, zz) {
204 var k = spectralStart, e = spectralEnd, r = 0;
205 while (k <= e) {
206 var z = dctZigZag[k];
207 var direction = zz[z] < 0 ? -1 : 1;
208 switch (successiveACState) {
209 case 0: // initial state
210 var rs = decodeHuffman(component.huffmanTableAC);
211 var s = rs & 15, r = rs >> 4;
212 if (s === 0) {
213 if (r < 15) {
214 eobrun = receive(r) + (1 << r);
215 successiveACState = 4;
216 } else {
217 r = 16;
218 successiveACState = 1;
219 }
220 } else {
221 if (s !== 1)
222 throw new Error("invalid ACn encoding");
223 successiveACNextValue = receiveAndExtend(s);
224 successiveACState = r ? 2 : 3;
225 }
226 continue;
227 case 1: // skipping r zero items
228 case 2:
229 if (zz[z])
230 zz[z] += (readBit() << successive) * direction;
231 else {
232 r--;
233 if (r === 0)
234 successiveACState = successiveACState == 2 ? 3 : 0;
235 }
236 break;
237 case 3: // set value for a zero item
238 if (zz[z])
239 zz[z] += (readBit() << successive) * direction;
240 else {
241 zz[z] = successiveACNextValue << successive;
242 successiveACState = 0;
243 }
244 break;
245 case 4: // eob
246 if (zz[z])
247 zz[z] += (readBit() << successive) * direction;
248 break;
249 }
250 k++;
251 }
252 if (successiveACState === 4) {
253 eobrun--;
254 if (eobrun === 0)
255 successiveACState = 0;
256 }
257 }
258 function decodeMcu(component, decode, mcu, row, col) {
259 var mcuRow = (mcu / mcusPerLine) | 0;
260 var mcuCol = mcu % mcusPerLine;
261 var blockRow = mcuRow * component.v + row;
262 var blockCol = mcuCol * component.h + col;
263 // If the block is missing and we're in tolerant mode, just skip it.
264 if (component.blocks[blockRow] === undefined && opts.tolerantDecoding)
265 return;
266 decode(component, component.blocks[blockRow][blockCol]);
267 }
268 function decodeBlock(component, decode, mcu) {
269 var blockRow = (mcu / component.blocksPerLine) | 0;
270 var blockCol = mcu % component.blocksPerLine;
271 // If the block is missing and we're in tolerant mode, just skip it.
272 if (component.blocks[blockRow] === undefined && opts.tolerantDecoding)
273 return;
274 decode(component, component.blocks[blockRow][blockCol]);
275 }
276
277 var componentsLength = components.length;
278 var component, i, j, k, n;
279 var decodeFn;
280 if (progressive) {
281 if (spectralStart === 0)
282 decodeFn = successivePrev === 0 ? decodeDCFirst : decodeDCSuccessive;
283 else
284 decodeFn = successivePrev === 0 ? decodeACFirst : decodeACSuccessive;
285 } else {
286 decodeFn = decodeBaseline;
287 }
288
289 var mcu = 0, marker;
290 var mcuExpected;
291 if (componentsLength == 1) {
292 mcuExpected = components[0].blocksPerLine * components[0].blocksPerColumn;
293 } else {
294 mcuExpected = mcusPerLine * frame.mcusPerColumn;
295 }
296 if (!resetInterval) resetInterval = mcuExpected;
297
298 var h, v;
299 while (mcu < mcuExpected) {
300 // reset interval stuff
301 for (i = 0; i < componentsLength; i++)
302 components[i].pred = 0;
303 eobrun = 0;
304
305 if (componentsLength == 1) {
306 component = components[0];
307 for (n = 0; n < resetInterval; n++) {
308 decodeBlock(component, decodeFn, mcu);
309 mcu++;
310 }
311 } else {
312 for (n = 0; n < resetInterval; n++) {
313 for (i = 0; i < componentsLength; i++) {
314 component = components[i];
315 h = component.h;
316 v = component.v;
317 for (j = 0; j < v; j++) {
318 for (k = 0; k < h; k++) {
319 decodeMcu(component, decodeFn, mcu, j, k);
320 }
321 }
322 }
323 mcu++;
324
325 // If we've reached our expected MCU's, stop decoding
326 if (mcu === mcuExpected) break;
327 }
328 }
329
330 if (mcu === mcuExpected) {
331 // Skip trailing bytes at the end of the scan - until we reach the next marker
332 do {
333 if (data[offset] === 0xFF) {
334 if (data[offset + 1] !== 0x00) {
335 break;
336 }
337 }
338 offset += 1;
339 } while (offset < data.length - 2);
340 }
341
342 // find marker
343 bitsCount = 0;
344 marker = (data[offset] << 8) | data[offset + 1];
345 if (marker < 0xFF00) {
346 throw new Error("marker was not found");
347 }
348
349 if (marker >= 0xFFD0 && marker <= 0xFFD7) { // RSTx
350 offset += 2;
351 }
352 else
353 break;
354 }
355
356 return offset - startOffset;
357 }
358
359 function buildComponentData(frame, component) {
360 var lines = [];
361 var blocksPerLine = component.blocksPerLine;
362 var blocksPerColumn = component.blocksPerColumn;
363 var samplesPerLine = blocksPerLine << 3;
364 // Only 1 used per invocation of this function and garbage collected after invocation, so no need to account for its memory footprint.
365 var R = new Int32Array(64), r = new Uint8Array(64);
366
367 // A port of poppler's IDCT method which in turn is taken from:
368 // Christoph Loeffler, Adriaan Ligtenberg, George S. Moschytz,
369 // "Practical Fast 1-D DCT Algorithms with 11 Multiplications",
370 // IEEE Intl. Conf. on Acoustics, Speech & Signal Processing, 1989,
371 // 988-991.
372 function quantizeAndInverse(zz, dataOut, dataIn) {
373 var qt = component.quantizationTable;
374 var v0, v1, v2, v3, v4, v5, v6, v7, t;
375 var p = dataIn;
376 var i;
377
378 // dequant
379 for (i = 0; i < 64; i++)
380 p[i] = zz[i] * qt[i];
381
382 // inverse DCT on rows
383 for (i = 0; i < 8; ++i) {
384 var row = 8 * i;
385
386 // check for all-zero AC coefficients
387 if (p[1 + row] == 0 && p[2 + row] == 0 && p[3 + row] == 0 &&
388 p[4 + row] == 0 && p[5 + row] == 0 && p[6 + row] == 0 &&
389 p[7 + row] == 0) {
390 t = (dctSqrt2 * p[0 + row] + 512) >> 10;
391 p[0 + row] = t;
392 p[1 + row] = t;
393 p[2 + row] = t;
394 p[3 + row] = t;
395 p[4 + row] = t;
396 p[5 + row] = t;
397 p[6 + row] = t;
398 p[7 + row] = t;
399 continue;
400 }
401
402 // stage 4
403 v0 = (dctSqrt2 * p[0 + row] + 128) >> 8;
404 v1 = (dctSqrt2 * p[4 + row] + 128) >> 8;
405 v2 = p[2 + row];
406 v3 = p[6 + row];
407 v4 = (dctSqrt1d2 * (p[1 + row] - p[7 + row]) + 128) >> 8;
408 v7 = (dctSqrt1d2 * (p[1 + row] + p[7 + row]) + 128) >> 8;
409 v5 = p[3 + row] << 4;
410 v6 = p[5 + row] << 4;
411
412 // stage 3
413 t = (v0 - v1+ 1) >> 1;
414 v0 = (v0 + v1 + 1) >> 1;
415 v1 = t;
416 t = (v2 * dctSin6 + v3 * dctCos6 + 128) >> 8;
417 v2 = (v2 * dctCos6 - v3 * dctSin6 + 128) >> 8;
418 v3 = t;
419 t = (v4 - v6 + 1) >> 1;
420 v4 = (v4 + v6 + 1) >> 1;
421 v6 = t;
422 t = (v7 + v5 + 1) >> 1;
423 v5 = (v7 - v5 + 1) >> 1;
424 v7 = t;
425
426 // stage 2
427 t = (v0 - v3 + 1) >> 1;
428 v0 = (v0 + v3 + 1) >> 1;
429 v3 = t;
430 t = (v1 - v2 + 1) >> 1;
431 v1 = (v1 + v2 + 1) >> 1;
432 v2 = t;
433 t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12;
434 v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12;
435 v7 = t;
436 t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12;
437 v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12;
438 v6 = t;
439
440 // stage 1
441 p[0 + row] = v0 + v7;
442 p[7 + row] = v0 - v7;
443 p[1 + row] = v1 + v6;
444 p[6 + row] = v1 - v6;
445 p[2 + row] = v2 + v5;
446 p[5 + row] = v2 - v5;
447 p[3 + row] = v3 + v4;
448 p[4 + row] = v3 - v4;
449 }
450
451 // inverse DCT on columns
452 for (i = 0; i < 8; ++i) {
453 var col = i;
454
455 // check for all-zero AC coefficients
456 if (p[1*8 + col] == 0 && p[2*8 + col] == 0 && p[3*8 + col] == 0 &&
457 p[4*8 + col] == 0 && p[5*8 + col] == 0 && p[6*8 + col] == 0 &&
458 p[7*8 + col] == 0) {
459 t = (dctSqrt2 * dataIn[i+0] + 8192) >> 14;
460 p[0*8 + col] = t;
461 p[1*8 + col] = t;
462 p[2*8 + col] = t;
463 p[3*8 + col] = t;
464 p[4*8 + col] = t;
465 p[5*8 + col] = t;
466 p[6*8 + col] = t;
467 p[7*8 + col] = t;
468 continue;
469 }
470
471 // stage 4
472 v0 = (dctSqrt2 * p[0*8 + col] + 2048) >> 12;
473 v1 = (dctSqrt2 * p[4*8 + col] + 2048) >> 12;
474 v2 = p[2*8 + col];
475 v3 = p[6*8 + col];
476 v4 = (dctSqrt1d2 * (p[1*8 + col] - p[7*8 + col]) + 2048) >> 12;
477 v7 = (dctSqrt1d2 * (p[1*8 + col] + p[7*8 + col]) + 2048) >> 12;
478 v5 = p[3*8 + col];
479 v6 = p[5*8 + col];
480
481 // stage 3
482 t = (v0 - v1 + 1) >> 1;
483 v0 = (v0 + v1 + 1) >> 1;
484 v1 = t;
485 t = (v2 * dctSin6 + v3 * dctCos6 + 2048) >> 12;
486 v2 = (v2 * dctCos6 - v3 * dctSin6 + 2048) >> 12;
487 v3 = t;
488 t = (v4 - v6 + 1) >> 1;
489 v4 = (v4 + v6 + 1) >> 1;
490 v6 = t;
491 t = (v7 + v5 + 1) >> 1;
492 v5 = (v7 - v5 + 1) >> 1;
493 v7 = t;
494
495 // stage 2
496 t = (v0 - v3 + 1) >> 1;
497 v0 = (v0 + v3 + 1) >> 1;
498 v3 = t;
499 t = (v1 - v2 + 1) >> 1;
500 v1 = (v1 + v2 + 1) >> 1;
501 v2 = t;
502 t = (v4 * dctSin3 + v7 * dctCos3 + 2048) >> 12;
503 v4 = (v4 * dctCos3 - v7 * dctSin3 + 2048) >> 12;
504 v7 = t;
505 t = (v5 * dctSin1 + v6 * dctCos1 + 2048) >> 12;
506 v5 = (v5 * dctCos1 - v6 * dctSin1 + 2048) >> 12;
507 v6 = t;
508
509 // stage 1
510 p[0*8 + col] = v0 + v7;
511 p[7*8 + col] = v0 - v7;
512 p[1*8 + col] = v1 + v6;
513 p[6*8 + col] = v1 - v6;
514 p[2*8 + col] = v2 + v5;
515 p[5*8 + col] = v2 - v5;
516 p[3*8 + col] = v3 + v4;
517 p[4*8 + col] = v3 - v4;
518 }
519
520 // convert to 8-bit integers
521 for (i = 0; i < 64; ++i) {
522 var sample = 128 + ((p[i] + 8) >> 4);
523 dataOut[i] = sample < 0 ? 0 : sample > 0xFF ? 0xFF : sample;
524 }
525 }
526
527 requestMemoryAllocation(samplesPerLine * blocksPerColumn * 8);
528
529 var i, j;
530 for (var blockRow = 0; blockRow < blocksPerColumn; blockRow++) {
531 var scanLine = blockRow << 3;
532 for (i = 0; i < 8; i++)
533 lines.push(new Uint8Array(samplesPerLine));
534 for (var blockCol = 0; blockCol < blocksPerLine; blockCol++) {
535 quantizeAndInverse(component.blocks[blockRow][blockCol], r, R);
536
537 var offset = 0, sample = blockCol << 3;
538 for (j = 0; j < 8; j++) {
539 var line = lines[scanLine + j];
540 for (i = 0; i < 8; i++)
541 line[sample + i] = r[offset++];
542 }
543 }
544 }
545 return lines;
546 }
547
548 function clampTo8bit(a) {
549 return a < 0 ? 0 : a > 255 ? 255 : a;
550 }
551
552 constructor.prototype = {
553 load: function load(path) {
554 var xhr = new XMLHttpRequest();
555 xhr.open("GET", path, true);
556 xhr.responseType = "arraybuffer";
557 xhr.onload = (function() {
558 // TODO catch parse error
559 var data = new Uint8Array(xhr.response || xhr.mozResponseArrayBuffer);
560 this.parse(data);
561 if (this.onload)
562 this.onload();
563 }).bind(this);
564 xhr.send(null);
565 },
566 parse: function parse(data) {
567 var maxResolutionInPixels = this.opts.maxResolutionInMP * 1000 * 1000;
568 var offset = 0, length = data.length;
569 function readUint16() {
570 var value = (data[offset] << 8) | data[offset + 1];
571 offset += 2;
572 return value;
573 }
574 function readDataBlock() {
575 var length = readUint16();
576 var array = data.subarray(offset, offset + length - 2);
577 offset += array.length;
578 return array;
579 }
580 function prepareComponents(frame) {
581 // According to the JPEG standard, the sampling factor must be between 1 and 4
582 // See https://github.com/libjpeg-turbo/libjpeg-turbo/blob/9abeff46d87bd201a952e276f3e4339556a403a3/libjpeg.txt#L1138-L1146
583 var maxH = 1, maxV = 1;
584 var component, componentId;
585 for (componentId in frame.components) {
586 if (frame.components.hasOwnProperty(componentId)) {
587 component = frame.components[componentId];
588 if (maxH < component.h) maxH = component.h;
589 if (maxV < component.v) maxV = component.v;
590 }
591 }
592 var mcusPerLine = Math.ceil(frame.samplesPerLine / 8 / maxH);
593 var mcusPerColumn = Math.ceil(frame.scanLines / 8 / maxV);
594 for (componentId in frame.components) {
595 if (frame.components.hasOwnProperty(componentId)) {
596 component = frame.components[componentId];
597 var blocksPerLine = Math.ceil(Math.ceil(frame.samplesPerLine / 8) * component.h / maxH);
598 var blocksPerColumn = Math.ceil(Math.ceil(frame.scanLines / 8) * component.v / maxV);
599 var blocksPerLineForMcu = mcusPerLine * component.h;
600 var blocksPerColumnForMcu = mcusPerColumn * component.v;
601 var blocksToAllocate = blocksPerColumnForMcu * blocksPerLineForMcu;
602 var blocks = [];
603
604 // Each block is a Int32Array of length 64 (4 x 64 = 256 bytes)
605 requestMemoryAllocation(blocksToAllocate * 256);
606
607 for (var i = 0; i < blocksPerColumnForMcu; i++) {
608 var row = [];
609 for (var j = 0; j < blocksPerLineForMcu; j++)
610 row.push(new Int32Array(64));
611 blocks.push(row);
612 }
613 component.blocksPerLine = blocksPerLine;
614 component.blocksPerColumn = blocksPerColumn;
615 component.blocks = blocks;
616 }
617 }
618 frame.maxH = maxH;
619 frame.maxV = maxV;
620 frame.mcusPerLine = mcusPerLine;
621 frame.mcusPerColumn = mcusPerColumn;
622 }
623 var jfif = null;
624 var adobe = null;
625 var pixels = null;
626 var frame, resetInterval;
627 var quantizationTables = [], frames = [];
628 var huffmanTablesAC = [], huffmanTablesDC = [];
629 var fileMarker = readUint16();
630 var malformedDataOffset = -1;
631 this.comments = [];
632 if (fileMarker != 0xFFD8) { // SOI (Start of Image)
633 throw new Error("SOI not found");
634 }
635
636 fileMarker = readUint16();
637 while (fileMarker != 0xFFD9) { // EOI (End of image)
638 var i, j, l;
639 switch(fileMarker) {
640 case 0xFF00: break;
641 case 0xFFE0: // APP0 (Application Specific)
642 case 0xFFE1: // APP1
643 case 0xFFE2: // APP2
644 case 0xFFE3: // APP3
645 case 0xFFE4: // APP4
646 case 0xFFE5: // APP5
647 case 0xFFE6: // APP6
648 case 0xFFE7: // APP7
649 case 0xFFE8: // APP8
650 case 0xFFE9: // APP9
651 case 0xFFEA: // APP10
652 case 0xFFEB: // APP11
653 case 0xFFEC: // APP12
654 case 0xFFED: // APP13
655 case 0xFFEE: // APP14
656 case 0xFFEF: // APP15
657 case 0xFFFE: // COM (Comment)
658 var appData = readDataBlock();
659
660 if (fileMarker === 0xFFFE) {
661 var comment = String.fromCharCode.apply(null, appData);
662 this.comments.push(comment);
663 }
664
665 if (fileMarker === 0xFFE0) {
666 if (appData[0] === 0x4A && appData[1] === 0x46 && appData[2] === 0x49 &&
667 appData[3] === 0x46 && appData[4] === 0) { // 'JFIF\x00'
668 jfif = {
669 version: { major: appData[5], minor: appData[6] },
670 densityUnits: appData[7],
671 xDensity: (appData[8] << 8) | appData[9],
672 yDensity: (appData[10] << 8) | appData[11],
673 thumbWidth: appData[12],
674 thumbHeight: appData[13],
675 thumbData: appData.subarray(14, 14 + 3 * appData[12] * appData[13])
676 };
677 }
678 }
679 // TODO APP1 - Exif
680 if (fileMarker === 0xFFE1) {
681 if (appData[0] === 0x45 &&
682 appData[1] === 0x78 &&
683 appData[2] === 0x69 &&
684 appData[3] === 0x66 &&
685 appData[4] === 0) { // 'EXIF\x00'
686 this.exifBuffer = appData.subarray(5, appData.length);
687 }
688 }
689
690 if (fileMarker === 0xFFEE) {
691 if (appData[0] === 0x41 && appData[1] === 0x64 && appData[2] === 0x6F &&
692 appData[3] === 0x62 && appData[4] === 0x65 && appData[5] === 0) { // 'Adobe\x00'
693 adobe = {
694 version: appData[6],
695 flags0: (appData[7] << 8) | appData[8],
696 flags1: (appData[9] << 8) | appData[10],
697 transformCode: appData[11]
698 };
699 }
700 }
701 break;
702
703 case 0xFFDB: // DQT (Define Quantization Tables)
704 var quantizationTablesLength = readUint16();
705 var quantizationTablesEnd = quantizationTablesLength + offset - 2;
706 while (offset < quantizationTablesEnd) {
707 var quantizationTableSpec = data[offset++];
708 requestMemoryAllocation(64 * 4);
709 var tableData = new Int32Array(64);
710 if ((quantizationTableSpec >> 4) === 0) { // 8 bit values
711 for (j = 0; j < 64; j++) {
712 var z = dctZigZag[j];
713 tableData[z] = data[offset++];
714 }
715 } else if ((quantizationTableSpec >> 4) === 1) { //16 bit
716 for (j = 0; j < 64; j++) {
717 var z = dctZigZag[j];
718 tableData[z] = readUint16();
719 }
720 } else
721 throw new Error("DQT: invalid table spec");
722 quantizationTables[quantizationTableSpec & 15] = tableData;
723 }
724 break;
725
726 case 0xFFC0: // SOF0 (Start of Frame, Baseline DCT)
727 case 0xFFC1: // SOF1 (Start of Frame, Extended DCT)
728 case 0xFFC2: // SOF2 (Start of Frame, Progressive DCT)
729 readUint16(); // skip data length
730 frame = {};
731 frame.extended = (fileMarker === 0xFFC1);
732 frame.progressive = (fileMarker === 0xFFC2);
733 frame.precision = data[offset++];
734 frame.scanLines = readUint16();
735 frame.samplesPerLine = readUint16();
736 frame.components = {};
737 frame.componentsOrder = [];
738
739 var pixelsInFrame = frame.scanLines * frame.samplesPerLine;
740 if (pixelsInFrame > maxResolutionInPixels) {
741 var exceededAmount = Math.ceil((pixelsInFrame - maxResolutionInPixels) / 1e6);
742 throw new Error(`maxResolutionInMP limit exceeded by ${exceededAmount}MP`);
743 }
744
745 var componentsCount = data[offset++], componentId;
746 var maxH = 0, maxV = 0;
747 for (i = 0; i < componentsCount; i++) {
748 componentId = data[offset];
749 var h = data[offset + 1] >> 4;
750 var v = data[offset + 1] & 15;
751 var qId = data[offset + 2];
752
753 if ( h <= 0 || v <= 0 ) {
754 throw new Error('Invalid sampling factor, expected values above 0');
755 }
756
757 frame.componentsOrder.push(componentId);
758 frame.components[componentId] = {
759 h: h,
760 v: v,
761 quantizationIdx: qId
762 };
763 offset += 3;
764 }
765 prepareComponents(frame);
766 frames.push(frame);
767 break;
768
769 case 0xFFC4: // DHT (Define Huffman Tables)
770 var huffmanLength = readUint16();
771 for (i = 2; i < huffmanLength;) {
772 var huffmanTableSpec = data[offset++];
773 var codeLengths = new Uint8Array(16);
774 var codeLengthSum = 0;
775 for (j = 0; j < 16; j++, offset++) {
776 codeLengthSum += (codeLengths[j] = data[offset]);
777 }
778 requestMemoryAllocation(16 + codeLengthSum);
779 var huffmanValues = new Uint8Array(codeLengthSum);
780 for (j = 0; j < codeLengthSum; j++, offset++)
781 huffmanValues[j] = data[offset];
782 i += 17 + codeLengthSum;
783
784 ((huffmanTableSpec >> 4) === 0 ?
785 huffmanTablesDC : huffmanTablesAC)[huffmanTableSpec & 15] =
786 buildHuffmanTable(codeLengths, huffmanValues);
787 }
788 break;
789
790 case 0xFFDD: // DRI (Define Restart Interval)
791 readUint16(); // skip data length
792 resetInterval = readUint16();
793 break;
794
795 case 0xFFDC: // Number of Lines marker
796 readUint16() // skip data length
797 readUint16() // Ignore this data since it represents the image height
798 break;
799
800 case 0xFFDA: // SOS (Start of Scan)
801 var scanLength = readUint16();
802 var selectorsCount = data[offset++];
803 var components = [], component;
804 for (i = 0; i < selectorsCount; i++) {
805 component = frame.components[data[offset++]];
806 var tableSpec = data[offset++];
807 component.huffmanTableDC = huffmanTablesDC[tableSpec >> 4];
808 component.huffmanTableAC = huffmanTablesAC[tableSpec & 15];
809 components.push(component);
810 }
811 var spectralStart = data[offset++];
812 var spectralEnd = data[offset++];
813 var successiveApproximation = data[offset++];
814 var processed = decodeScan(data, offset,
815 frame, components, resetInterval,
816 spectralStart, spectralEnd,
817 successiveApproximation >> 4, successiveApproximation & 15, this.opts);
818 offset += processed;
819 break;
820
821 case 0xFFFF: // Fill bytes
822 if (data[offset] !== 0xFF) { // Avoid skipping a valid marker.
823 offset--;
824 }
825 break;
826 default:
827 if (data[offset - 3] == 0xFF &&
828 data[offset - 2] >= 0xC0 && data[offset - 2] <= 0xFE) {
829 // could be incorrect encoding -- last 0xFF byte of the previous
830 // block was eaten by the encoder
831 offset -= 3;
832 break;
833 }
834 else if (fileMarker === 0xE0 || fileMarker == 0xE1) {
835 // Recover from malformed APP1 markers popular in some phone models.
836 // See https://github.com/eugeneware/jpeg-js/issues/82
837 if (malformedDataOffset !== -1) {
838 throw new Error(`first unknown JPEG marker at offset ${malformedDataOffset.toString(16)}, second unknown JPEG marker ${fileMarker.toString(16)} at offset ${(offset - 1).toString(16)}`);
839 }
840 malformedDataOffset = offset - 1;
841 const nextOffset = readUint16();
842 if (data[offset + nextOffset - 2] === 0xFF) {
843 offset += nextOffset - 2;
844 break;
845 }
846 }
847 throw new Error("unknown JPEG marker " + fileMarker.toString(16));
848 }
849 fileMarker = readUint16();
850 }
851 if (frames.length != 1)
852 throw new Error("only single frame JPEGs supported");
853
854 // set each frame's components quantization table
855 for (var i = 0; i < frames.length; i++) {
856 var cp = frames[i].components;
857 for (var j in cp) {
858 cp[j].quantizationTable = quantizationTables[cp[j].quantizationIdx];
859 delete cp[j].quantizationIdx;
860 }
861 }
862
863 this.width = frame.samplesPerLine;
864 this.height = frame.scanLines;
865 this.jfif = jfif;
866 this.adobe = adobe;
867 this.components = [];
868 for (var i = 0; i < frame.componentsOrder.length; i++) {
869 var component = frame.components[frame.componentsOrder[i]];
870 this.components.push({
871 lines: buildComponentData(frame, component),
872 scaleX: component.h / frame.maxH,
873 scaleY: component.v / frame.maxV
874 });
875 }
876 },
877 getData: function getData(width, height) {
878 var scaleX = this.width / width, scaleY = this.height / height;
879
880 var component1, component2, component3, component4;
881 var component1Line, component2Line, component3Line, component4Line;
882 var x, y;
883 var offset = 0;
884 var Y, Cb, Cr, K, C, M, Ye, R, G, B;
885 var colorTransform;
886 var dataLength = width * height * this.components.length;
887 requestMemoryAllocation(dataLength);
888 var data = new Uint8Array(dataLength);
889 switch (this.components.length) {
890 case 1:
891 component1 = this.components[0];
892 for (y = 0; y < height; y++) {
893 component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)];
894 for (x = 0; x < width; x++) {
895 Y = component1Line[0 | (x * component1.scaleX * scaleX)];
896
897 data[offset++] = Y;
898 }
899 }
900 break;
901 case 2:
902 // PDF might compress two component data in custom colorspace
903 component1 = this.components[0];
904 component2 = this.components[1];
905 for (y = 0; y < height; y++) {
906 component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)];
907 component2Line = component2.lines[0 | (y * component2.scaleY * scaleY)];
908 for (x = 0; x < width; x++) {
909 Y = component1Line[0 | (x * component1.scaleX * scaleX)];
910 data[offset++] = Y;
911 Y = component2Line[0 | (x * component2.scaleX * scaleX)];
912 data[offset++] = Y;
913 }
914 }
915 break;
916 case 3:
917 // The default transform for three components is true
918 colorTransform = true;
919 // The adobe transform marker overrides any previous setting
920 if (this.adobe && this.adobe.transformCode)
921 colorTransform = true;
922 else if (typeof this.opts.colorTransform !== 'undefined')
923 colorTransform = !!this.opts.colorTransform;
924
925 component1 = this.components[0];
926 component2 = this.components[1];
927 component3 = this.components[2];
928 for (y = 0; y < height; y++) {
929 component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)];
930 component2Line = component2.lines[0 | (y * component2.scaleY * scaleY)];
931 component3Line = component3.lines[0 | (y * component3.scaleY * scaleY)];
932 for (x = 0; x < width; x++) {
933 if (!colorTransform) {
934 R = component1Line[0 | (x * component1.scaleX * scaleX)];
935 G = component2Line[0 | (x * component2.scaleX * scaleX)];
936 B = component3Line[0 | (x * component3.scaleX * scaleX)];
937 } else {
938 Y = component1Line[0 | (x * component1.scaleX * scaleX)];
939 Cb = component2Line[0 | (x * component2.scaleX * scaleX)];
940 Cr = component3Line[0 | (x * component3.scaleX * scaleX)];
941
942 R = clampTo8bit(Y + 1.402 * (Cr - 128));
943 G = clampTo8bit(Y - 0.3441363 * (Cb - 128) - 0.71413636 * (Cr - 128));
944 B = clampTo8bit(Y + 1.772 * (Cb - 128));
945 }
946
947 data[offset++] = R;
948 data[offset++] = G;
949 data[offset++] = B;
950 }
951 }
952 break;
953 case 4:
954 if (!this.adobe)
955 throw new Error('Unsupported color mode (4 components)');
956 // The default transform for four components is false
957 colorTransform = false;
958 // The adobe transform marker overrides any previous setting
959 if (this.adobe && this.adobe.transformCode)
960 colorTransform = true;
961 else if (typeof this.opts.colorTransform !== 'undefined')
962 colorTransform = !!this.opts.colorTransform;
963
964 component1 = this.components[0];
965 component2 = this.components[1];
966 component3 = this.components[2];
967 component4 = this.components[3];
968 for (y = 0; y < height; y++) {
969 component1Line = component1.lines[0 | (y * component1.scaleY * scaleY)];
970 component2Line = component2.lines[0 | (y * component2.scaleY * scaleY)];
971 component3Line = component3.lines[0 | (y * component3.scaleY * scaleY)];
972 component4Line = component4.lines[0 | (y * component4.scaleY * scaleY)];
973 for (x = 0; x < width; x++) {
974 if (!colorTransform) {
975 C = component1Line[0 | (x * component1.scaleX * scaleX)];
976 M = component2Line[0 | (x * component2.scaleX * scaleX)];
977 Ye = component3Line[0 | (x * component3.scaleX * scaleX)];
978 K = component4Line[0 | (x * component4.scaleX * scaleX)];
979 } else {
980 Y = component1Line[0 | (x * component1.scaleX * scaleX)];
981 Cb = component2Line[0 | (x * component2.scaleX * scaleX)];
982 Cr = component3Line[0 | (x * component3.scaleX * scaleX)];
983 K = component4Line[0 | (x * component4.scaleX * scaleX)];
984
985 C = 255 - clampTo8bit(Y + 1.402 * (Cr - 128));
986 M = 255 - clampTo8bit(Y - 0.3441363 * (Cb - 128) - 0.71413636 * (Cr - 128));
987 Ye = 255 - clampTo8bit(Y + 1.772 * (Cb - 128));
988 }
989 data[offset++] = 255-C;
990 data[offset++] = 255-M;
991 data[offset++] = 255-Ye;
992 data[offset++] = 255-K;
993 }
994 }
995 break;
996 default:
997 throw new Error('Unsupported color mode');
998 }
999 return data;
1000 },
1001 copyToImageData: function copyToImageData(imageData, formatAsRGBA) {
1002 var width = imageData.width, height = imageData.height;
1003 var imageDataArray = imageData.data;
1004 var data = this.getData(width, height);
1005 var i = 0, j = 0, x, y;
1006 var Y, K, C, M, R, G, B;
1007 switch (this.components.length) {
1008 case 1:
1009 for (y = 0; y < height; y++) {
1010 for (x = 0; x < width; x++) {
1011 Y = data[i++];
1012
1013 imageDataArray[j++] = Y;
1014 imageDataArray[j++] = Y;
1015 imageDataArray[j++] = Y;
1016 if (formatAsRGBA) {
1017 imageDataArray[j++] = 255;
1018 }
1019 }
1020 }
1021 break;
1022 case 3:
1023 for (y = 0; y < height; y++) {
1024 for (x = 0; x < width; x++) {
1025 R = data[i++];
1026 G = data[i++];
1027 B = data[i++];
1028
1029 imageDataArray[j++] = R;
1030 imageDataArray[j++] = G;
1031 imageDataArray[j++] = B;
1032 if (formatAsRGBA) {
1033 imageDataArray[j++] = 255;
1034 }
1035 }
1036 }
1037 break;
1038 case 4:
1039 for (y = 0; y < height; y++) {
1040 for (x = 0; x < width; x++) {
1041 C = data[i++];
1042 M = data[i++];
1043 Y = data[i++];
1044 K = data[i++];
1045
1046 R = 255 - clampTo8bit(C * (1 - K / 255) + K);
1047 G = 255 - clampTo8bit(M * (1 - K / 255) + K);
1048 B = 255 - clampTo8bit(Y * (1 - K / 255) + K);
1049
1050 imageDataArray[j++] = R;
1051 imageDataArray[j++] = G;
1052 imageDataArray[j++] = B;
1053 if (formatAsRGBA) {
1054 imageDataArray[j++] = 255;
1055 }
1056 }
1057 }
1058 break;
1059 default:
1060 throw new Error('Unsupported color mode');
1061 }
1062 }
1063 };
1064
1065
1066 // We cap the amount of memory used by jpeg-js to avoid unexpected OOMs from untrusted content.
1067 var totalBytesAllocated = 0;
1068 var maxMemoryUsageBytes = 0;
1069 function requestMemoryAllocation(increaseAmount = 0) {
1070 var totalMemoryImpactBytes = totalBytesAllocated + increaseAmount;
1071 if (totalMemoryImpactBytes > maxMemoryUsageBytes) {
1072 var exceededAmount = Math.ceil((totalMemoryImpactBytes - maxMemoryUsageBytes) / 1024 / 1024);
1073 throw new Error(`maxMemoryUsageInMB limit exceeded by at least ${exceededAmount}MB`);
1074 }
1075
1076 totalBytesAllocated = totalMemoryImpactBytes;
1077 }
1078
1079 constructor.resetMaxMemoryUsage = function (maxMemoryUsageBytes_) {
1080 totalBytesAllocated = 0;
1081 maxMemoryUsageBytes = maxMemoryUsageBytes_;
1082 };
1083
1084 constructor.getBytesAllocated = function () {
1085 return totalBytesAllocated;
1086 };
1087
1088 constructor.requestMemoryAllocation = requestMemoryAllocation;
1089
1090 return constructor;
1091})();
1092
1093
1094function decode(jpegData, userOpts = {}) {
1095 var defaultOpts = {
1096 // "undefined" means "Choose whether to transform colors based on the image’s color model."
1097 colorTransform: undefined,
1098 useTArray: false,
1099 formatAsRGBA: true,
1100 tolerantDecoding: true,
1101 maxResolutionInMP: 100, // Don't decode more than 100 megapixels
1102 maxMemoryUsageInMB: 512, // Don't decode if memory footprint is more than 512MB
1103 };
1104
1105 var opts = {...defaultOpts, ...userOpts};
1106 var arr = new Uint8Array(jpegData);
1107 var decoder = new JpegImage();
1108 decoder.opts = opts;
1109 // If this constructor ever supports async decoding this will need to be done differently.
1110 // Until then, treating as singleton limit is fine.
1111 JpegImage.resetMaxMemoryUsage(opts.maxMemoryUsageInMB * 1024 * 1024);
1112 decoder.parse(arr);
1113
1114 var channels = (opts.formatAsRGBA) ? 4 : 3;
1115 var bytesNeeded = decoder.width * decoder.height * channels;
1116 try {
1117 JpegImage.requestMemoryAllocation(bytesNeeded);
1118 var image = {
1119 width: decoder.width,
1120 height: decoder.height,
1121 exifBuffer: decoder.exifBuffer,
1122 data: opts.useTArray ?
1123 new Uint8Array(bytesNeeded) :
1124 Buffer.alloc(bytesNeeded)
1125 };
1126 if(decoder.comments.length > 0) {
1127 image["comments"] = decoder.comments;
1128 }
1129 } catch (err) {
1130 if (err instanceof RangeError) {
1131 throw new Error("Could not allocate enough memory for the image. " +
1132 "Required: " + bytesNeeded);
1133 }
1134
1135 if (err instanceof ReferenceError) {
1136 if (err.message === "Buffer is not defined") {
1137 throw new Error("Buffer is not globally defined in this environment. " +
1138 "Consider setting useTArray to true");
1139 }
1140 }
1141 throw err;
1142 }
1143
1144 decoder.copyToImageData(image, opts.formatAsRGBA);
1145
1146 return image;
1147}
1148
1149export { decode }
1150hooks/vendor/omggif.js 810 lines1// Vendored from omggif 1.0.10, MIT; see LICENSE-omggif.
2// Changed: ES module export instead of exports.
3// (c) Dean McNamee <dean@gmail.com>, 2013.
4//
5// https://github.com/deanm/omggif
6//
7// Permission is hereby granted, free of charge, to any person obtaining a copy
8// of this software and associated documentation files (the "Software"), to
9// deal in the Software without restriction, including without limitation the
10// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
11// sell copies of the Software, and to permit persons to whom the Software is
12// furnished to do so, subject to the following conditions:
13//
14// The above copyright notice and this permission notice shall be included in
15// all copies or substantial portions of the Software.
16//
17// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
22// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
23// IN THE SOFTWARE.
24//
25// omggif is a JavaScript implementation of a GIF 89a encoder and decoder,
26// including animation and compression. It does not rely on any specific
27// underlying system, so should run in the browser, Node, or Plask.
28
29"use strict";
30
31function GifWriter(buf, width, height, gopts) {
32 var p = 0;
33
34 var gopts = gopts === undefined ? { } : gopts;
35 var loop_count = gopts.loop === undefined ? null : gopts.loop;
36 var global_palette = gopts.palette === undefined ? null : gopts.palette;
37
38 if (width <= 0 || height <= 0 || width > 65535 || height > 65535)
39 throw new Error("Width/Height invalid.");
40
41 function check_palette_and_num_colors(palette) {
42 var num_colors = palette.length;
43 if (num_colors < 2 || num_colors > 256 || num_colors & (num_colors-1)) {
44 throw new Error(
45 "Invalid code/color length, must be power of 2 and 2 .. 256.");
46 }
47 return num_colors;
48 }
49
50 // - Header.
51 buf[p++] = 0x47; buf[p++] = 0x49; buf[p++] = 0x46; // GIF
52 buf[p++] = 0x38; buf[p++] = 0x39; buf[p++] = 0x61; // 89a
53
54 // Handling of Global Color Table (palette) and background index.
55 var gp_num_colors_pow2 = 0;
56 var background = 0;
57 if (global_palette !== null) {
58 var gp_num_colors = check_palette_and_num_colors(global_palette);
59 while (gp_num_colors >>= 1) ++gp_num_colors_pow2;
60 gp_num_colors = 1 << gp_num_colors_pow2;
61 --gp_num_colors_pow2;
62 if (gopts.background !== undefined) {
63 background = gopts.background;
64 if (background >= gp_num_colors)
65 throw new Error("Background index out of range.");
66 // The GIF spec states that a background index of 0 should be ignored, so
67 // this is probably a mistake and you really want to set it to another
68 // slot in the palette. But actually in the end most browsers, etc end
69 // up ignoring this almost completely (including for dispose background).
70 if (background === 0)
71 throw new Error("Background index explicitly passed as 0.");
72 }
73 }
74
75 // - Logical Screen Descriptor.
76 // NOTE(deanm): w/h apparently ignored by implementations, but set anyway.
77 buf[p++] = width & 0xff; buf[p++] = width >> 8 & 0xff;
78 buf[p++] = height & 0xff; buf[p++] = height >> 8 & 0xff;
79 // NOTE: Indicates 0-bpp original color resolution (unused?).
80 buf[p++] = (global_palette !== null ? 0x80 : 0) | // Global Color Table Flag.
81 gp_num_colors_pow2; // NOTE: No sort flag (unused?).
82 buf[p++] = background; // Background Color Index.
83 buf[p++] = 0; // Pixel aspect ratio (unused?).
84
85 // - Global Color Table
86 if (global_palette !== null) {
87 for (var i = 0, il = global_palette.length; i < il; ++i) {
88 var rgb = global_palette[i];
89 buf[p++] = rgb >> 16 & 0xff;
90 buf[p++] = rgb >> 8 & 0xff;
91 buf[p++] = rgb & 0xff;
92 }
93 }
94
95 if (loop_count !== null) { // Netscape block for looping.
96 if (loop_count < 0 || loop_count > 65535)
97 throw new Error("Loop count invalid.")
98 // Extension code, label, and length.
99 buf[p++] = 0x21; buf[p++] = 0xff; buf[p++] = 0x0b;
100 // NETSCAPE2.0
101 buf[p++] = 0x4e; buf[p++] = 0x45; buf[p++] = 0x54; buf[p++] = 0x53;
102 buf[p++] = 0x43; buf[p++] = 0x41; buf[p++] = 0x50; buf[p++] = 0x45;
103 buf[p++] = 0x32; buf[p++] = 0x2e; buf[p++] = 0x30;
104 // Sub-block
105 buf[p++] = 0x03; buf[p++] = 0x01;
106 buf[p++] = loop_count & 0xff; buf[p++] = loop_count >> 8 & 0xff;
107 buf[p++] = 0x00; // Terminator.
108 }
109
110
111 var ended = false;
112
113 this.addFrame = function(x, y, w, h, indexed_pixels, opts) {
114 if (ended === true) { --p; ended = false; } // Un-end.
115
116 opts = opts === undefined ? { } : opts;
117
118 // TODO(deanm): Bounds check x, y. Do they need to be within the virtual
119 // canvas width/height, I imagine?
120 if (x < 0 || y < 0 || x > 65535 || y > 65535)
121 throw new Error("x/y invalid.")
122
123 if (w <= 0 || h <= 0 || w > 65535 || h > 65535)
124 throw new Error("Width/Height invalid.")
125
126 if (indexed_pixels.length < w * h)
127 throw new Error("Not enough pixels for the frame size.");
128
129 var using_local_palette = true;
130 var palette = opts.palette;
131 if (palette === undefined || palette === null) {
132 using_local_palette = false;
133 palette = global_palette;
134 }
135
136 if (palette === undefined || palette === null)
137 throw new Error("Must supply either a local or global palette.");
138
139 var num_colors = check_palette_and_num_colors(palette);
140
141 // Compute the min_code_size (power of 2), destroying num_colors.
142 var min_code_size = 0;
143 while (num_colors >>= 1) ++min_code_size;
144 num_colors = 1 << min_code_size; // Now we can easily get it back.
145
146 var delay = opts.delay === undefined ? 0 : opts.delay;
147
148 // From the spec:
149 // 0 - No disposal specified. The decoder is
150 // not required to take any action.
151 // 1 - Do not dispose. The graphic is to be left
152 // in place.
153 // 2 - Restore to background color. The area used by the
154 // graphic must be restored to the background color.
155 // 3 - Restore to previous. The decoder is required to
156 // restore the area overwritten by the graphic with
157 // what was there prior to rendering the graphic.
158 // 4-7 - To be defined.
159 // NOTE(deanm): Dispose background doesn't really work, apparently most
160 // browsers ignore the background palette index and clear to transparency.
161 var disposal = opts.disposal === undefined ? 0 : opts.disposal;
162 if (disposal < 0 || disposal > 3) // 4-7 is reserved.
163 throw new Error("Disposal out of range.");
164
165 var use_transparency = false;
166 var transparent_index = 0;
167 if (opts.transparent !== undefined && opts.transparent !== null) {
168 use_transparency = true;
169 transparent_index = opts.transparent;
170 if (transparent_index < 0 || transparent_index >= num_colors)
171 throw new Error("Transparent color index.");
172 }
173
174 if (disposal !== 0 || use_transparency || delay !== 0) {
175 // - Graphics Control Extension
176 buf[p++] = 0x21; buf[p++] = 0xf9; // Extension / Label.
177 buf[p++] = 4; // Byte size.
178
179 buf[p++] = disposal << 2 | (use_transparency === true ? 1 : 0);
180 buf[p++] = delay & 0xff; buf[p++] = delay >> 8 & 0xff;
181 buf[p++] = transparent_index; // Transparent color index.
182 buf[p++] = 0; // Block Terminator.
183 }
184
185 // - Image Descriptor
186 buf[p++] = 0x2c; // Image Seperator.
187 buf[p++] = x & 0xff; buf[p++] = x >> 8 & 0xff; // Left.
188 buf[p++] = y & 0xff; buf[p++] = y >> 8 & 0xff; // Top.
189 buf[p++] = w & 0xff; buf[p++] = w >> 8 & 0xff;
190 buf[p++] = h & 0xff; buf[p++] = h >> 8 & 0xff;
191 // NOTE: No sort flag (unused?).
192 // TODO(deanm): Support interlace.
193 buf[p++] = using_local_palette === true ? (0x80 | (min_code_size-1)) : 0;
194
195 // - Local Color Table
196 if (using_local_palette === true) {
197 for (var i = 0, il = palette.length; i < il; ++i) {
198 var rgb = palette[i];
199 buf[p++] = rgb >> 16 & 0xff;
200 buf[p++] = rgb >> 8 & 0xff;
201 buf[p++] = rgb & 0xff;
202 }
203 }
204
205 p = GifWriterOutputLZWCodeStream(
206 buf, p, min_code_size < 2 ? 2 : min_code_size, indexed_pixels);
207
208 return p;
209 };
210
211 this.end = function() {
212 if (ended === false) {
213 buf[p++] = 0x3b; // Trailer.
214 ended = true;
215 }
216 return p;
217 };
218
219 this.getOutputBuffer = function() { return buf; };
220 this.setOutputBuffer = function(v) { buf = v; };
221 this.getOutputBufferPosition = function() { return p; };
222 this.setOutputBufferPosition = function(v) { p = v; };
223}
224
225// Main compression routine, palette indexes -> LZW code stream.
226// |index_stream| must have at least one entry.
227function GifWriterOutputLZWCodeStream(buf, p, min_code_size, index_stream) {
228 buf[p++] = min_code_size;
229 var cur_subblock = p++; // Pointing at the length field.
230
231 var clear_code = 1 << min_code_size;
232 var code_mask = clear_code - 1;
233 var eoi_code = clear_code + 1;
234 var next_code = eoi_code + 1;
235
236 var cur_code_size = min_code_size + 1; // Number of bits per code.
237 var cur_shift = 0;
238 // We have at most 12-bit codes, so we should have to hold a max of 19
239 // bits here (and then we would write out).
240 var cur = 0;
241
242 function emit_bytes_to_buffer(bit_block_size) {
243 while (cur_shift >= bit_block_size) {
244 buf[p++] = cur & 0xff;
245 cur >>= 8; cur_shift -= 8;
246 if (p === cur_subblock + 256) { // Finished a subblock.
247 buf[cur_subblock] = 255;
248 cur_subblock = p++;
249 }
250 }
251 }
252
253 function emit_code(c) {
254 cur |= c << cur_shift;
255 cur_shift += cur_code_size;
256 emit_bytes_to_buffer(8);
257 }
258
259 // I am not an expert on the topic, and I don't want to write a thesis.
260 // However, it is good to outline here the basic algorithm and the few data
261 // structures and optimizations here that make this implementation fast.
262 // The basic idea behind LZW is to build a table of previously seen runs
263 // addressed by a short id (herein called output code). All data is
264 // referenced by a code, which represents one or more values from the
265 // original input stream. All input bytes can be referenced as the same
266 // value as an output code. So if you didn't want any compression, you
267 // could more or less just output the original bytes as codes (there are
268 // some details to this, but it is the idea). In order to achieve
269 // compression, values greater then the input range (codes can be up to
270 // 12-bit while input only 8-bit) represent a sequence of previously seen
271 // inputs. The decompressor is able to build the same mapping while
272 // decoding, so there is always a shared common knowledge between the
273 // encoding and decoder, which is also important for "timing" aspects like
274 // how to handle variable bit width code encoding.
275 //
276 // One obvious but very important consequence of the table system is there
277 // is always a unique id (at most 12-bits) to map the runs. 'A' might be
278 // 4, then 'AA' might be 10, 'AAA' 11, 'AAAA' 12, etc. This relationship
279 // can be used for an effecient lookup strategy for the code mapping. We
280 // need to know if a run has been seen before, and be able to map that run
281 // to the output code. Since we start with known unique ids (input bytes),
282 // and then from those build more unique ids (table entries), we can
283 // continue this chain (almost like a linked list) to always have small
284 // integer values that represent the current byte chains in the encoder.
285 // This means instead of tracking the input bytes (AAAABCD) to know our
286 // current state, we can track the table entry for AAAABC (it is guaranteed
287 // to exist by the nature of the algorithm) and the next character D.
288 // Therefor the tuple of (table_entry, byte) is guaranteed to also be
289 // unique. This allows us to create a simple lookup key for mapping input
290 // sequences to codes (table indices) without having to store or search
291 // any of the code sequences. So if 'AAAA' has a table entry of 12, the
292 // tuple of ('AAAA', K) for any input byte K will be unique, and can be our
293 // key. This leads to a integer value at most 20-bits, which can always
294 // fit in an SMI value and be used as a fast sparse array / object key.
295
296 // Output code for the current contents of the index buffer.
297 var ib_code = index_stream[0] & code_mask; // Load first input index.
298 var code_table = { }; // Key'd on our 20-bit "tuple".
299
300 emit_code(clear_code); // Spec says first code should be a clear code.
301
302 // First index already loaded, process the rest of the stream.
303 for (var i = 1, il = index_stream.length; i < il; ++i) {
304 var k = index_stream[i] & code_mask;
305 var cur_key = ib_code << 8 | k; // (prev, k) unique tuple.
306 var cur_code = code_table[cur_key]; // buffer + k.
307
308 // Check if we have to create a new code table entry.
309 if (cur_code === undefined) { // We don't have buffer + k.
310 // Emit index buffer (without k).
311 // This is an inline version of emit_code, because this is the core
312 // writing routine of the compressor (and V8 cannot inline emit_code
313 // because it is a closure here in a different context). Additionally
314 // we can call emit_byte_to_buffer less often, because we can have
315 // 30-bits (from our 31-bit signed SMI), and we know our codes will only
316 // be 12-bits, so can safely have 18-bits there without overflow.
317 // emit_code(ib_code);
318 cur |= ib_code << cur_shift;
319 cur_shift += cur_code_size;
320 while (cur_shift >= 8) {
321 buf[p++] = cur & 0xff;
322 cur >>= 8; cur_shift -= 8;
323 if (p === cur_subblock + 256) { // Finished a subblock.
324 buf[cur_subblock] = 255;
325 cur_subblock = p++;
326 }
327 }
328
329 if (next_code === 4096) { // Table full, need a clear.
330 emit_code(clear_code);
331 next_code = eoi_code + 1;
332 cur_code_size = min_code_size + 1;
333 code_table = { };
334 } else { // Table not full, insert a new entry.
335 // Increase our variable bit code sizes if necessary. This is a bit
336 // tricky as it is based on "timing" between the encoding and
337 // decoder. From the encoders perspective this should happen after
338 // we've already emitted the index buffer and are about to create the
339 // first table entry that would overflow our current code bit size.
340 if (next_code >= (1 << cur_code_size)) ++cur_code_size;
341 code_table[cur_key] = next_code++; // Insert into code table.
342 }
343
344 ib_code = k; // Index buffer to single input k.
345 } else {
346 ib_code = cur_code; // Index buffer to sequence in code table.
347 }
348 }
349
350 emit_code(ib_code); // There will still be something in the index buffer.
351 emit_code(eoi_code); // End Of Information.
352
353 // Flush / finalize the sub-blocks stream to the buffer.
354 emit_bytes_to_buffer(1);
355
356 // Finish the sub-blocks, writing out any unfinished lengths and
357 // terminating with a sub-block of length 0. If we have already started
358 // but not yet used a sub-block it can just become the terminator.
359 if (cur_subblock + 1 === p) { // Started but unused.
360 buf[cur_subblock] = 0;
361 } else { // Started and used, write length and additional terminator block.
362 buf[cur_subblock] = p - cur_subblock - 1;
363 buf[p++] = 0;
364 }
365 return p;
366}
367
368function GifReader(buf) {
369 var p = 0;
370
371 // - Header (GIF87a or GIF89a).
372 if (buf[p++] !== 0x47 || buf[p++] !== 0x49 || buf[p++] !== 0x46 ||
373 buf[p++] !== 0x38 || (buf[p++]+1 & 0xfd) !== 0x38 || buf[p++] !== 0x61) {
374 throw new Error("Invalid GIF 87a/89a header.");
375 }
376
377 // - Logical Screen Descriptor.
378 var width = buf[p++] | buf[p++] << 8;
379 var height = buf[p++] | buf[p++] << 8;
380 var pf0 = buf[p++]; // <Packed Fields>.
381 var global_palette_flag = pf0 >> 7;
382 var num_global_colors_pow2 = pf0 & 0x7;
383 var num_global_colors = 1 << (num_global_colors_pow2 + 1);
384 var background = buf[p++];
385 buf[p++]; // Pixel aspect ratio (unused?).
386
387 var global_palette_offset = null;
388 var global_palette_size = null;
389
390 if (global_palette_flag) {
391 global_palette_offset = p;
392 global_palette_size = num_global_colors;
393 p += num_global_colors * 3; // Seek past palette.
394 }
395
396 var no_eof = true;
397
398 var frames = [ ];
399
400 var delay = 0;
401 var transparent_index = null;
402 var disposal = 0; // 0 - No disposal specified.
403 var loop_count = null;
404
405 this.width = width;
406 this.height = height;
407
408 while (no_eof && p < buf.length) {
409 switch (buf[p++]) {
410 case 0x21: // Graphics Control Extension Block
411 switch (buf[p++]) {
412 case 0xff: // Application specific block
413 // Try if it's a Netscape block (with animation loop counter).
414 if (buf[p ] !== 0x0b || // 21 FF already read, check block size.
415 // NETSCAPE2.0
416 buf[p+1 ] == 0x4e && buf[p+2 ] == 0x45 && buf[p+3 ] == 0x54 &&
417 buf[p+4 ] == 0x53 && buf[p+5 ] == 0x43 && buf[p+6 ] == 0x41 &&
418 buf[p+7 ] == 0x50 && buf[p+8 ] == 0x45 && buf[p+9 ] == 0x32 &&
419 buf[p+10] == 0x2e && buf[p+11] == 0x30 &&
420 // Sub-block
421 buf[p+12] == 0x03 && buf[p+13] == 0x01 && buf[p+16] == 0) {
422 p += 14;
423 loop_count = buf[p++] | buf[p++] << 8;
424 p++; // Skip terminator.
425 } else { // We don't know what it is, just try to get past it.
426 p += 12;
427 while (true) { // Seek through subblocks.
428 var block_size = buf[p++];
429 // Bad block size (ex: undefined from an out of bounds read).
430 if (!(block_size >= 0)) throw Error("Invalid block size");
431 if (block_size === 0) break; // 0 size is terminator
432 p += block_size;
433 }
434 }
435 break;
436
437 case 0xf9: // Graphics Control Extension
438 if (buf[p++] !== 0x4 || buf[p+4] !== 0)
439 throw new Error("Invalid graphics extension block.");
440 var pf1 = buf[p++];
441 delay = buf[p++] | buf[p++] << 8;
442 transparent_index = buf[p++];
443 if ((pf1 & 1) === 0) transparent_index = null;
444 disposal = pf1 >> 2 & 0x7;
445 p++; // Skip terminator.
446 break;
447
448 case 0xfe: // Comment Extension.
449 while (true) { // Seek through subblocks.
450 var block_size = buf[p++];
451 // Bad block size (ex: undefined from an out of bounds read).
452 if (!(block_size >= 0)) throw Error("Invalid block size");
453 if (block_size === 0) break; // 0 size is terminator
454 // console.log(buf.slice(p, p+block_size).toString('ascii'));
455 p += block_size;
456 }
457 break;
458
459 default:
460 throw new Error(
461 "Unknown graphic control label: 0x" + buf[p-1].toString(16));
462 }
463 break;
464
465 case 0x2c: // Image Descriptor.
466 var x = buf[p++] | buf[p++] << 8;
467 var y = buf[p++] | buf[p++] << 8;
468 var w = buf[p++] | buf[p++] << 8;
469 var h = buf[p++] | buf[p++] << 8;
470 var pf2 = buf[p++];
471 var local_palette_flag = pf2 >> 7;
472 var interlace_flag = pf2 >> 6 & 1;
473 var num_local_colors_pow2 = pf2 & 0x7;
474 var num_local_colors = 1 << (num_local_colors_pow2 + 1);
475 var palette_offset = global_palette_offset;
476 var palette_size = global_palette_size;
477 var has_local_palette = false;
478 if (local_palette_flag) {
479 var has_local_palette = true;
480 palette_offset = p; // Override with local palette.
481 palette_size = num_local_colors;
482 p += num_local_colors * 3; // Seek past palette.
483 }
484
485 var data_offset = p;
486
487 p++; // codesize
488 while (true) {
489 var block_size = buf[p++];
490 // Bad block size (ex: undefined from an out of bounds read).
491 if (!(block_size >= 0)) throw Error("Invalid block size");
492 if (block_size === 0) break; // 0 size is terminator
493 p += block_size;
494 }
495
496 frames.push({x: x, y: y, width: w, height: h,
497 has_local_palette: has_local_palette,
498 palette_offset: palette_offset,
499 palette_size: palette_size,
500 data_offset: data_offset,
501 data_length: p - data_offset,
502 transparent_index: transparent_index,
503 interlaced: !!interlace_flag,
504 delay: delay,
505 disposal: disposal});
506 break;
507
508 case 0x3b: // Trailer Marker (end of file).
509 no_eof = false;
510 break;
511
512 default:
513 throw new Error("Unknown gif block: 0x" + buf[p-1].toString(16));
514 break;
515 }
516 }
517
518 this.numFrames = function() {
519 return frames.length;
520 };
521
522 this.loopCount = function() {
523 return loop_count;
524 };
525
526 this.frameInfo = function(frame_num) {
527 if (frame_num < 0 || frame_num >= frames.length)
528 throw new Error("Frame index out of range.");
529 return frames[frame_num];
530 }
531
532 this.decodeAndBlitFrameBGRA = function(frame_num, pixels) {
533 var frame = this.frameInfo(frame_num);
534 var num_pixels = frame.width * frame.height;
535 var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
536 GifReaderLZWOutputIndexStream(
537 buf, frame.data_offset, index_stream, num_pixels);
538 var palette_offset = frame.palette_offset;
539
540 // NOTE(deanm): It seems to be much faster to compare index to 256 than
541 // to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
542 // the profile, not sure if it's related to using a Uint8Array.
543 var trans = frame.transparent_index;
544 if (trans === null) trans = 256;
545
546 // We are possibly just blitting to a portion of the entire frame.
547 // That is a subrect within the framerect, so the additional pixels
548 // must be skipped over after we finished a scanline.
549 var framewidth = frame.width;
550 var framestride = width - framewidth;
551 var xleft = framewidth; // Number of subrect pixels left in scanline.
552
553 // Output indicies of the top left and bottom right corners of the subrect.
554 var opbeg = ((frame.y * width) + frame.x) * 4;
555 var opend = ((frame.y + frame.height) * width + frame.x) * 4;
556 var op = opbeg;
557
558 var scanstride = framestride * 4;
559
560 // Use scanstride to skip past the rows when interlacing. This is skipping
561 // 7 rows for the first two passes, then 3 then 1.
562 if (frame.interlaced === true) {
563 scanstride += width * 4 * 7; // Pass 1.
564 }
565
566 var interlaceskip = 8; // Tracking the row interval in the current pass.
567
568 for (var i = 0, il = index_stream.length; i < il; ++i) {
569 var index = index_stream[i];
570
571 if (xleft === 0) { // Beginning of new scan line
572 op += scanstride;
573 xleft = framewidth;
574 if (op >= opend) { // Catch the wrap to switch passes when interlacing.
575 scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
576 // interlaceskip / 2 * 4 is interlaceskip << 1.
577 op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
578 interlaceskip >>= 1;
579 }
580 }
581
582 if (index === trans) {
583 op += 4;
584 } else {
585 var r = buf[palette_offset + index * 3];
586 var g = buf[palette_offset + index * 3 + 1];
587 var b = buf[palette_offset + index * 3 + 2];
588 pixels[op++] = b;
589 pixels[op++] = g;
590 pixels[op++] = r;
591 pixels[op++] = 255;
592 }
593 --xleft;
594 }
595 };
596
597 // I will go to copy and paste hell one day...
598 this.decodeAndBlitFrameRGBA = function(frame_num, pixels) {
599 var frame = this.frameInfo(frame_num);
600 var num_pixels = frame.width * frame.height;
601 var index_stream = new Uint8Array(num_pixels); // At most 8-bit indices.
602 GifReaderLZWOutputIndexStream(
603 buf, frame.data_offset, index_stream, num_pixels);
604 var palette_offset = frame.palette_offset;
605
606 // NOTE(deanm): It seems to be much faster to compare index to 256 than
607 // to === null. Not sure why, but CompareStub_EQ_STRICT shows up high in
608 // the profile, not sure if it's related to using a Uint8Array.
609 var trans = frame.transparent_index;
610 if (trans === null) trans = 256;
611
612 // We are possibly just blitting to a portion of the entire frame.
613 // That is a subrect within the framerect, so the additional pixels
614 // must be skipped over after we finished a scanline.
615 var framewidth = frame.width;
616 var framestride = width - framewidth;
617 var xleft = framewidth; // Number of subrect pixels left in scanline.
618
619 // Output indicies of the top left and bottom right corners of the subrect.
620 var opbeg = ((frame.y * width) + frame.x) * 4;
621 var opend = ((frame.y + frame.height) * width + frame.x) * 4;
622 var op = opbeg;
623
624 var scanstride = framestride * 4;
625
626 // Use scanstride to skip past the rows when interlacing. This is skipping
627 // 7 rows for the first two passes, then 3 then 1.
628 if (frame.interlaced === true) {
629 scanstride += width * 4 * 7; // Pass 1.
630 }
631
632 var interlaceskip = 8; // Tracking the row interval in the current pass.
633
634 for (var i = 0, il = index_stream.length; i < il; ++i) {
635 var index = index_stream[i];
636
637 if (xleft === 0) { // Beginning of new scan line
638 op += scanstride;
639 xleft = framewidth;
640 if (op >= opend) { // Catch the wrap to switch passes when interlacing.
641 scanstride = framestride * 4 + width * 4 * (interlaceskip-1);
642 // interlaceskip / 2 * 4 is interlaceskip << 1.
643 op = opbeg + (framewidth + framestride) * (interlaceskip << 1);
644 interlaceskip >>= 1;
645 }
646 }
647
648 if (index === trans) {
649 op += 4;
650 } else {
651 var r = buf[palette_offset + index * 3];
652 var g = buf[palette_offset + index * 3 + 1];
653 var b = buf[palette_offset + index * 3 + 2];
654 pixels[op++] = r;
655 pixels[op++] = g;
656 pixels[op++] = b;
657 pixels[op++] = 255;
658 }
659 --xleft;
660 }
661 };
662}
663
664function GifReaderLZWOutputIndexStream(code_stream, p, output, output_length) {
665 var min_code_size = code_stream[p++];
666
667 var clear_code = 1 << min_code_size;
668 var eoi_code = clear_code + 1;
669 var next_code = eoi_code + 1;
670
671 var cur_code_size = min_code_size + 1; // Number of bits per code.
672 // NOTE: This shares the same name as the encoder, but has a different
673 // meaning here. Here this masks each code coming from the code stream.
674 var code_mask = (1 << cur_code_size) - 1;
675 var cur_shift = 0;
676 var cur = 0;
677
678 var op = 0; // Output pointer.
679
680 var subblock_size = code_stream[p++];
681
682 // TODO(deanm): Would using a TypedArray be any faster? At least it would
683 // solve the fast mode / backing store uncertainty.
684 // var code_table = Array(4096);
685 var code_table = new Int32Array(4096); // Can be signed, we only use 20 bits.
686
687 var prev_code = null; // Track code-1.
688
689 while (true) {
690 // Read up to two bytes, making sure we always 12-bits for max sized code.
691 while (cur_shift < 16) {
692 if (subblock_size === 0) break; // No more data to be read.
693
694 cur |= code_stream[p++] << cur_shift;
695 cur_shift += 8;
696
697 if (subblock_size === 1) { // Never let it get to 0 to hold logic above.
698 subblock_size = code_stream[p++]; // Next subblock.
699 } else {
700 --subblock_size;
701 }
702 }
703
704 // TODO(deanm): We should never really get here, we should have received
705 // and EOI.
706 if (cur_shift < cur_code_size)
707 break;
708
709 var code = cur & code_mask;
710 cur >>= cur_code_size;
711 cur_shift -= cur_code_size;
712
713 // TODO(deanm): Maybe should check that the first code was a clear code,
714 // at least this is what you're supposed to do. But actually our encoder
715 // now doesn't emit a clear code first anyway.
716 if (code === clear_code) {
717 // We don't actually have to clear the table. This could be a good idea
718 // for greater error checking, but we don't really do any anyway. We
719 // will just track it with next_code and overwrite old entries.
720
721 next_code = eoi_code + 1;
722 cur_code_size = min_code_size + 1;
723 code_mask = (1 << cur_code_size) - 1;
724
725 // Don't update prev_code ?
726 prev_code = null;
727 continue;
728 } else if (code === eoi_code) {
729 break;
730 }
731
732 // We have a similar situation as the decoder, where we want to store
733 // variable length entries (code table entries), but we want to do in a
734 // faster manner than an array of arrays. The code below stores sort of a
735 // linked list within the code table, and then "chases" through it to
736 // construct the dictionary entries. When a new entry is created, just the
737 // last byte is stored, and the rest (prefix) of the entry is only
738 // referenced by its table entry. Then the code chases through the
739 // prefixes until it reaches a single byte code. We have to chase twice,
740 // first to compute the length, and then to actually copy the data to the
741 // output (backwards, since we know the length). The alternative would be
742 // storing something in an intermediate stack, but that doesn't make any
743 // more sense. I implemented an approach where it also stored the length
744 // in the code table, although it's a bit tricky because you run out of
745 // bits (12 + 12 + 8), but I didn't measure much improvements (the table
746 // entries are generally not the long). Even when I created benchmarks for
747 // very long table entries the complexity did not seem worth it.
748 // The code table stores the prefix entry in 12 bits and then the suffix
749 // byte in 8 bits, so each entry is 20 bits.
750
751 var chase_code = code < next_code ? code : prev_code;
752
753 // Chase what we will output, either {CODE} or {CODE-1}.
754 var chase_length = 0;
755 var chase = chase_code;
756 while (chase > clear_code) {
757 chase = code_table[chase] >> 8;
758 ++chase_length;
759 }
760
761 var k = chase;
762
763 var op_end = op + chase_length + (chase_code !== code ? 1 : 0);
764 if (op_end > output_length) {
765 console.log("Warning, gif stream longer than expected.");
766 return;
767 }
768
769 // Already have the first byte from the chase, might as well write it fast.
770 output[op++] = k;
771
772 op += chase_length;
773 var b = op; // Track pointer, writing backwards.
774
775 if (chase_code !== code) // The case of emitting {CODE-1} + k.
776 output[op++] = k;
777
778 chase = chase_code;
779 while (chase_length--) {
780 chase = code_table[chase];
781 output[--b] = chase & 0xff; // Write backwards.
782 chase >>= 8; // Pull down to the prefix code.
783 }
784
785 if (prev_code !== null && next_code < 4096) {
786 code_table[next_code++] = prev_code << 8 | k;
787 // TODO(deanm): Figure out this clearing vs code growth logic better. I
788 // have an feeling that it should just happen somewhere else, for now it
789 // is awkward between when we grow past the max and then hit a clear code.
790 // For now just check if we hit the max 12-bits (then a clear code should
791 // follow, also of course encoded in 12-bits).
792 if (next_code >= code_mask+1 && cur_code_size < 12) {
793 ++cur_code_size;
794 code_mask = code_mask << 1 | 1;
795 }
796 }
797
798 prev_code = code;
799 }
800
801 if (op !== output_length) {
802 console.log("Warning, gif stream shorter than expected.");
803 }
804
805 return output;
806}
807
808// CommonJS.
809export { GifReader }
810types/index.d.ts 9 lines1/** Keys `<row>|<path>` of the image paths clicked open. */
2export type InlineImagesOpen = string[]
3
4declare module 'claude-code' {
5 interface PluginState {
6 'inline-images': { open: InlineImagesOpen; pane: string | null }
7 }
8}
9