SLOPSHOPPER

inline-images

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

newpanerowstimer
v0.1.2MITupdated 2026-10-04nnemirovsky/cc-inline-images
A shopper browsing a rack in a slop shop
Preview · a replayed session in a sandbox
claude · ~/work/app · inline-images
│ ┃ inline-image ✕ › fix the failing auth test and add an audit log call │ ┃ No image. │ ⏺ Read(src/auth.ts) │ ⎿ Read 6 lines │ ⏺ Update(src/auth.ts) │ ⎿ Added 2 lines, removed 1 line │ ⏺ Bash(bun test) │ ⎿ 3 pass, 1 fail │ │ ● Done. refresh now rejects expired claims and logs an audit event. │ │ ✻ Worked for 42s · done 4:20 PM │ │ │ │ ────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── › ? for shortcuts

Draws

Pane · inline-image
No image.
README

Inline Images

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.

Two screenshots Claude sent from a server over ssh, drawn inline under their attachment lines

What it draws

WhereHow
Files Claude sends you (SendUserFile, attachments of SendUserMessage)inline, under the › [image] lines
Images Claude reads with Readinline, 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 existsa ▸ 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.

Why it works over ssh

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.

Requirements

  • Claude Code 2.1.289 or later, with function-hook mods available to your account
  • A terminal with the kitty graphics protocol and its Unicode placeholders: kitty, Ghostty, and terminals built on them (agterm). Elsewhere each image shows as a one-line description instead.
  • The fullscreen renderer (/tui fullscreen) for clicking ▸ lines open

Install

/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.

Hotkeys and scripts

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.

Limits

  • At most 2 MiB per image after decoding. Larger PNGs show a line saying so; JPEG and GIF are scaled down to fit.
  • Inline images are sized to about 100 columns and 24 rows, keeping their aspect ratio. The hotkey pane fits the image to the pane.
  • Pasted images are looked up in the conversation by their [Image #N] marks.
  • Background sessions (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.

Development

claude plugin validate .
claude plugin test .
claude --plugin-dir .

License

MIT. Bundles jpeg-js (BSD-3-Clause, with Apache-2.0 parts) and omggif (MIT); their licenses are in hooks/vendor/.

Source 5 files
hooks/register.tsx 320 lines
1import { 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}
320
hooks/image.ts 126 lines
1import { 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}
126
hooks/vendor/jpeg.js 1150 lines
1// 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 }
1150
hooks/vendor/omggif.js 810 lines
1// 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 }
810
types/index.d.ts 9 lines
1/** 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