@svgrid/enterprise 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/export.ts ADDED
@@ -0,0 +1,512 @@
1
+ /// <reference path="./pdfmake-shims.d.ts" />
2
+ import type { RowData, TableFeatures } from '@svgrid/grid'
3
+ import type { SvGridApi } from '@svgrid/grid'
4
+ import { assertProLicensed } from './license'
5
+ import { installSmartShim, type SmartDataExporterInstance } from './smart-shim'
6
+
7
+ export type ExportFormat = 'xlsx' | 'pdf' | 'csv' | 'tsv' | 'html'
8
+
9
+ export type ExportColumn = {
10
+ /** Data field name on the row object. */
11
+ field: string
12
+ /** Header label to render in the exported file. Defaults to `field`. */
13
+ header?: string
14
+ }
15
+
16
+ /**
17
+ * Per-cell style descriptor. Mirrors a subset of CSS; the keys the
18
+ * underlying exporter honours are font / colour / background / border /
19
+ * alignment. Anything else is ignored gracefully.
20
+ */
21
+ export type ExportCellStyle = {
22
+ color?: string
23
+ backgroundColor?: string
24
+ fontWeight?: 'normal' | 'bold' | number
25
+ fontStyle?: 'normal' | 'italic'
26
+ fontSize?: number | string
27
+ fontFamily?: string
28
+ border?: string
29
+ textAlign?: 'left' | 'right' | 'center'
30
+ verticalAlign?: 'top' | 'middle' | 'bottom'
31
+ }
32
+
33
+ /**
34
+ * Document-level style. Apply blanket styles to the header row, the
35
+ * value rows, or selectively per cell-reference (e.g. `'B2'`).
36
+ */
37
+ export type ExportStyles = {
38
+ /** Style applied to every header cell. */
39
+ headerRow?: ExportCellStyle
40
+ /** Style applied to every data row. Even / odd zebra are derived from this if `rowAlternate` is set. */
41
+ rows?: ExportCellStyle
42
+ /** Optional zebra background for odd-indexed rows. */
43
+ rowAlternate?: ExportCellStyle
44
+ /** Per-cell overrides keyed by Excel-style reference (`'A1'`, `'C3'`). */
45
+ cells?: Record<string, ExportCellStyle>
46
+ }
47
+
48
+ /**
49
+ * Header / footer entries for xlsx + pdf. Each line is rendered top to
50
+ * bottom on the page. Embed an image with `{ image: dataUrl }`; embed
51
+ * text with `{ text: '...', style?: ExportCellStyle }`.
52
+ */
53
+ export type ExportHeaderFooterLine =
54
+ | { text: string; style?: ExportCellStyle }
55
+ | { image: string; width?: number; height?: number }
56
+ | { left?: string; center?: string; right?: string }
57
+
58
+ export type ExportSheet<TData> = {
59
+ /** Sheet/tab label. Required. */
60
+ label: string
61
+ /** Rows for this sheet. */
62
+ rows: ReadonlyArray<TData>
63
+ /** Per-sheet columns. Falls back to the top-level `columns` if omitted. */
64
+ columns?: ReadonlyArray<ExportColumn>
65
+ /** Per-sheet styles. */
66
+ styles?: ExportStyles
67
+ }
68
+
69
+ export type ExportOptions<TData> = {
70
+ format: ExportFormat
71
+ /** Base filename (extension is appended if missing). Defaults to "grid". */
72
+ filename?: string
73
+ /**
74
+ * Columns to include. If omitted, every key of the first row is exported
75
+ * (in original object order) using the field name as the header label.
76
+ */
77
+ columns?: ReadonlyArray<ExportColumn>
78
+ /**
79
+ * Source rows. If omitted, the current displayed rows from the api are used.
80
+ * Provide explicitly when you want to override (e.g. export the whole
81
+ * dataset rather than the filtered view).
82
+ */
83
+ rows?: ReadonlyArray<TData>
84
+ /** PDF only. Page orientation. Defaults to "portrait". */
85
+ pageOrientation?: 'portrait' | 'landscape'
86
+ /**
87
+ * Cell + row styles. Apply once to match a light/dark theme, or per-cell
88
+ * for conditional formatting. xlsx and pdf honour these; csv/tsv ignore
89
+ * them; html bakes them into inline `style=` attributes.
90
+ */
91
+ styles?: ExportStyles
92
+ /** Page header lines (xlsx, pdf, html). Logos go here as `{ image: ... }`. */
93
+ header?: ReadonlyArray<ExportHeaderFooterLine>
94
+ /** Page footer lines. Common pattern: page number on the right. */
95
+ footer?: ReadonlyArray<ExportHeaderFooterLine>
96
+ /**
97
+ * Multi-sheet export (xlsx only). When set, each entry becomes one
98
+ * sheet/tab. The top-level `rows` / `columns` are ignored.
99
+ */
100
+ sheets?: ReadonlyArray<ExportSheet<TData>>
101
+ /**
102
+ * If a row has a column whose value matches one of these field names
103
+ * AND the value looks like a URL or data URL, the cell is exported as
104
+ * an embedded image (xlsx). Defaults to `[]` (no auto-detection).
105
+ */
106
+ imageFields?: ReadonlyArray<string>
107
+ /**
108
+ * Pixel dimensions used when embedding images. Smart's xlsx writer
109
+ * draws the image at this size relative to the cell origin. Defaults
110
+ * to `{ width: 32, height: 32 }`. Set larger for thumbnails, smaller
111
+ * for inline icons.
112
+ */
113
+ imageSize?: { width: number; height: number }
114
+ /**
115
+ * Group flat rows by one or more field names. The exporter wraps each
116
+ * group in an Excel outline row (with the +/- expand button), and
117
+ * emits a `<value> <field>` group header above every cluster. Maps
118
+ * straight to Smart DataExporter's constructor `groupBy` arg. xlsx
119
+ * only; csv/tsv/html flatten the groups back out.
120
+ */
121
+ groupBy?: ReadonlyArray<string>
122
+ /**
123
+ * Mark rows as a hierarchical (tree) data source. Each row should
124
+ * declare its own children either via the `subRows` convention or by
125
+ * matching the parent/child shape Smart expects on the input rows.
126
+ * Mutually exclusive with `groupBy`. xlsx only.
127
+ */
128
+ hierarchical?: boolean
129
+ }
130
+
131
+ let exporterCtorPromise: Promise<
132
+ new (
133
+ options: Record<string, unknown>,
134
+ groupBy?: ReadonlyArray<string>,
135
+ filterBy?: Record<string, unknown>,
136
+ conditionalFormatting?: unknown,
137
+ ) => SmartDataExporterInstance
138
+ > | null = null
139
+
140
+ async function getDataExporter() {
141
+ if (typeof window === 'undefined') {
142
+ throw new Error('@svgrid/enterprise: export requires a browser environment')
143
+ }
144
+ if (!exporterCtorPromise) {
145
+ exporterCtorPromise = (async () => {
146
+ installSmartShim()
147
+ // Side-effect import: the IIFE registers Smart.Utilities.DataExporter
148
+ // on the global Smart namespace that installSmartShim() set up.
149
+ await import('./smart.export.js')
150
+ const Ctor = window.Smart?.Utilities?.DataExporter
151
+ if (!Ctor) {
152
+ throw new Error('@svgrid/enterprise: failed to load Smart.Utilities.DataExporter')
153
+ }
154
+ return Ctor
155
+ })()
156
+ }
157
+ return exporterCtorPromise
158
+ }
159
+
160
+ async function ensureGlobals(format: ExportFormat): Promise<void> {
161
+ const g = globalThis as unknown as { JSZip?: unknown; pdfMake?: unknown }
162
+ if (format === 'xlsx' && g.JSZip == null) {
163
+ let mod: unknown
164
+ try {
165
+ mod = await import('jszip')
166
+ } catch {
167
+ throw new Error(
168
+ '@svgrid/enterprise: xlsx export requires the "jszip" peer dependency. ' +
169
+ 'Install it with: pnpm add jszip',
170
+ )
171
+ }
172
+ g.JSZip = (mod as { default?: unknown }).default ?? mod
173
+ }
174
+ if (format === 'pdf' && g.pdfMake == null) {
175
+ let pdfMakeMod: unknown
176
+ let vfsMod: unknown
177
+ try {
178
+ pdfMakeMod = await import('pdfmake/build/pdfmake')
179
+ vfsMod = await import('pdfmake/build/vfs_fonts')
180
+ } catch {
181
+ throw new Error(
182
+ '@svgrid/enterprise: pdf export requires the "pdfmake" peer dependency. ' +
183
+ 'Install it with: pnpm add pdfmake',
184
+ )
185
+ }
186
+ const pdfMake = ((pdfMakeMod as { default?: unknown }).default ?? pdfMakeMod) as {
187
+ vfs?: Record<string, string>
188
+ createPdf: (def: unknown) => { download(name: string): void; getBlob(cb: (b: Blob) => void): void }
189
+ }
190
+ const vfsRoot = (vfsMod as { default?: unknown }).default ?? vfsMod
191
+ // pdfmake's vfs_fonts file historically exports either { pdfMake: { vfs } }
192
+ // or { default: { vfs } } or { vfs } depending on bundler. Try each shape.
193
+ const candidate =
194
+ (vfsRoot as { pdfMake?: { vfs?: Record<string, string> } }).pdfMake?.vfs ??
195
+ (vfsRoot as { vfs?: Record<string, string> }).vfs ??
196
+ (vfsRoot as { default?: { vfs?: Record<string, string> } }).default?.vfs
197
+ if (candidate) pdfMake.vfs = candidate
198
+ g.pdfMake = pdfMake
199
+ }
200
+ }
201
+
202
+ function buildExportPayload<
203
+ TFeatures extends TableFeatures,
204
+ TData extends RowData,
205
+ >(
206
+ api: SvGridApi<TFeatures, TData>,
207
+ opts: ExportOptions<TData>,
208
+ ): {
209
+ rows: ReadonlyArray<Record<string, unknown>>
210
+ filename: string
211
+ } {
212
+ const sourceRows: ReadonlyArray<TData> = opts.rows ?? api.getDisplayedRows()
213
+ let cols: ReadonlyArray<ExportColumn>
214
+ if (opts.columns && opts.columns.length > 0) {
215
+ cols = opts.columns
216
+ } else {
217
+ // Auto-derive columns from the grid's own columnDefs. This picks up the
218
+ // human-readable `header` labels so the exported file shows what the
219
+ // user sees on-screen (e.g. "Order ID" rather than "orderId"). Only
220
+ // visible, field-backed columns are exported.
221
+ const gridCols = api.getColumns().filter((c) => c.visible && c.field)
222
+ if (gridCols.length > 0) {
223
+ cols = gridCols.map((c) => ({ field: c.field!, header: c.header }))
224
+ } else if (sourceRows.length > 0) {
225
+ cols = Object.keys(sourceRows[0] as Record<string, unknown>)
226
+ .filter((k) => !k.startsWith('_'))
227
+ .map((field) => ({ field }))
228
+ } else {
229
+ cols = []
230
+ }
231
+ }
232
+ const headerRow: Record<string, unknown> = {}
233
+ for (const c of cols) headerRow[c.field] = c.header ?? c.field
234
+
235
+ const projected: Array<Record<string, unknown>> = [headerRow]
236
+ for (const r of sourceRows) {
237
+ const row: Record<string, unknown> = {}
238
+ const src = r as unknown as Record<string, unknown>
239
+ for (const c of cols) row[c.field] = src[c.field]
240
+ projected.push(row)
241
+ }
242
+ const filename = (opts.filename ?? 'grid').trim() || 'grid'
243
+ return { rows: projected, filename }
244
+ }
245
+
246
+ /**
247
+ * Translate the public {@link ExportStyles} shape into the keys the Smart
248
+ * exporter actually consumes:
249
+ * `style.header` → all header cells
250
+ * `style.rows` → all data rows. Cell-prop keys (color, backgroundColor,
251
+ * fontWeight, fontStyle, fontSize, fontFamily,
252
+ * textAlign, verticalAlign, textDecoration, numFmt)
253
+ * are recognised by Smart's `storeCellStyle`.
254
+ * `style.rows.alternationCount` + `alternationStart` +
255
+ * `alternationIndex1BackgroundColor` (etc.) → zebra striping
256
+ * `style.cells.<ref>` → HTML-only per-cell ref overrides
257
+ */
258
+ function translateStyles(styles: ExportStyles | undefined): Record<string, unknown> | undefined {
259
+ if (!styles) return undefined
260
+ const out: Record<string, unknown> = {}
261
+ if (styles.headerRow) out.header = { ...styles.headerRow }
262
+ const rows: Record<string, unknown> = styles.rows ? { ...styles.rows } : {}
263
+ if (styles.rowAlternate) {
264
+ rows.alternationCount = 2
265
+ rows.alternationStart = 0
266
+ if (styles.rowAlternate.backgroundColor)
267
+ rows.alternationIndex1BackgroundColor = styles.rowAlternate.backgroundColor
268
+ if (styles.rowAlternate.color)
269
+ rows.alternationIndex1Color = styles.rowAlternate.color
270
+ }
271
+ if (Object.keys(rows).length > 0) out.rows = rows
272
+ if (styles.cells) out.cells = styles.cells
273
+ return out
274
+ }
275
+
276
+ let _imgIdCounter = 0
277
+ const _imgIdCache = new Map<string, string>()
278
+ function imageIdFor(dataUrl: string): string {
279
+ const cached = _imgIdCache.get(dataUrl)
280
+ if (cached) return cached
281
+ const id = `img${++_imgIdCounter}`
282
+ _imgIdCache.set(dataUrl, id)
283
+ return id
284
+ }
285
+
286
+ /**
287
+ * Build the image object Smart's xlsx writer expects. The exporter pulls
288
+ * `{ id, base64, imageType, width, height }` off the return of
289
+ * `addImageToCell(...).image` and stores it under `this.images` →
290
+ * eventually written to the workbook as `xl/media/imageN.<imageType>`.
291
+ *
292
+ * `base64` may be a full data URL - Smart strips the prefix when writing.
293
+ */
294
+ function makeSmartImage(dataUrl: string, width = 64, height = 64): {
295
+ id: string; base64: string; imageType: string; width: number; height: number
296
+ } {
297
+ // Extract MIME → imageType. Defaults to png for safety.
298
+ const mime = /^data:image\/([a-zA-Z0-9+]+);/i.exec(dataUrl)?.[1]?.toLowerCase() ?? 'png'
299
+ const imageType = mime === 'jpg' ? 'jpeg'
300
+ : mime === 'svg+xml' ? 'svg'
301
+ : mime
302
+ return {
303
+ id: imageIdFor(dataUrl),
304
+ base64: dataUrl,
305
+ imageType,
306
+ width,
307
+ height,
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Build the header / footer arrays the Smart exporter understands.
313
+ * Each input line becomes one full-width row in the spreadsheet with
314
+ * `style.mergeAcross` so the line spans every column.
315
+ *
316
+ * Image lines are written as a data-URL string into the first column and
317
+ * the wrapper's `addImageToCell` hook embeds them as real picture cells.
318
+ */
319
+ function buildContentRows(
320
+ lines: ReadonlyArray<ExportHeaderFooterLine> | undefined,
321
+ datafields: ReadonlyArray<string>,
322
+ ): Array<{ cells: Record<string, unknown>; style?: Record<string, unknown> }> | undefined {
323
+ if (!lines || lines.length === 0 || datafields.length === 0) return undefined
324
+ const first = datafields[0]!
325
+ const out: Array<{ cells: Record<string, unknown>; style?: Record<string, unknown> }> = []
326
+ for (const line of lines) {
327
+ const cells: Record<string, unknown> = {}
328
+ let style: Record<string, unknown> | undefined
329
+ if ('image' in line) {
330
+ cells[first] = line.image
331
+ style = { mergeAcross: true, textAlign: 'left' }
332
+ } else if ('text' in line) {
333
+ cells[first] = line.text
334
+ style = { mergeAcross: true, ...(line.style ?? {}) }
335
+ } else {
336
+ // 3-column { left, center, right }. Distribute across the first,
337
+ // middle, and last datafield.
338
+ const mid = datafields[Math.floor(datafields.length / 2)]!
339
+ const last = datafields[datafields.length - 1]!
340
+ if (line.left !== undefined) cells[first] = line.left
341
+ if (line.center !== undefined) cells[mid] = line.center
342
+ if (line.right !== undefined) cells[last] = line.right
343
+ style = { textAlign: 'left' }
344
+ }
345
+ out.push({ cells, style })
346
+ }
347
+ return out
348
+ }
349
+
350
+ /**
351
+ * Build an `addImageToCell` handler that embeds a value as a real image
352
+ * cell whenever the value is a data URL. Smart calls this for every body
353
+ * cell AND every header/footer cell, so the same handler covers both
354
+ * code paths.
355
+ *
356
+ * `bodyImageSize` controls the embedded pixel size for body cells (the
357
+ * thumbnails). Header/footer image lines use their own size hint
358
+ * already encoded in the `cells[first]` value's metadata - here we just
359
+ * embed with a slightly larger default so logos read well in the page
360
+ * banner.
361
+ */
362
+ function buildImageHandler(
363
+ imageFields: ReadonlyArray<string> | undefined,
364
+ bodyImageSize: { width: number; height: number },
365
+ ): NonNullable<SmartDataExporterInstance['addImageToCell']> {
366
+ const allowed = imageFields && imageFields.length > 0 ? new Set(imageFields) : null
367
+ return (_rowIndex, dataField, value) => {
368
+ if (typeof value !== 'string') return null
369
+ if (!/^data:image\//i.test(value)) return null
370
+ if (allowed && !allowed.has(dataField)) {
371
+ // Header/footer image lines come through the first column too -
372
+ // those cells aren't in `imageFields`, but the wrapper wrote the
373
+ // data URL there itself. Embed bigger (logo banner).
374
+ return { image: makeSmartImage(value, 96, 96), value: '' }
375
+ }
376
+ return { image: makeSmartImage(value, bodyImageSize.width, bodyImageSize.height), value: '' }
377
+ }
378
+ }
379
+
380
+ /**
381
+ * Build the constructor-options object passed to the Smart DataExporter.
382
+ * Constructor only consumes a small set of keys; instance-level options
383
+ * (`headerContent`, `footerContent`, `addImageToCell`) are applied
384
+ * separately in {@link exportGrid} after `new DataExporter(...)`.
385
+ */
386
+ function buildExporterOptions<TData>(
387
+ opts: ExportOptions<TData>,
388
+ ): Record<string, unknown> {
389
+ const out: Record<string, unknown> = {
390
+ exportHeader: true,
391
+ pageOrientation: opts.pageOrientation ?? 'portrait',
392
+ }
393
+ const style = translateStyles(opts.styles)
394
+ if (style) out.style = style
395
+ // Smart's xlsx writer emits Excel-native row outlining when
396
+ // `hierarchical: true` is set on the constructor options OR when the
397
+ // second constructor arg is a non-empty groupBy array. We thread
398
+ // hierarchical through here and groupBy through the constructor call
399
+ // site in exportGrid.
400
+ if (opts.hierarchical) out.hierarchical = true
401
+ return out
402
+ }
403
+
404
+ /**
405
+ * Apply instance-level options the constructor does not consume.
406
+ * Mutates and returns the exporter for fluent use.
407
+ */
408
+ function applyInstanceOptions<TData>(
409
+ exporter: SmartDataExporterInstance,
410
+ opts: ExportOptions<TData>,
411
+ datafields: ReadonlyArray<string>,
412
+ ): SmartDataExporterInstance {
413
+ const headerRows = buildContentRows(opts.header, datafields)
414
+ const footerRows = buildContentRows(opts.footer, datafields)
415
+ if (headerRows) exporter.headerContent = headerRows
416
+ if (footerRows) exporter.footerContent = footerRows
417
+
418
+ // Wire image embedding when the caller opts in via imageFields OR the
419
+ // header/footer carries an image line. Both code paths funnel through
420
+ // the same handler.
421
+ const hasHeaderOrFooterImage =
422
+ (opts.header?.some((l) => 'image' in l) ?? false) ||
423
+ (opts.footer?.some((l) => 'image' in l) ?? false)
424
+ if (opts.imageFields?.length || hasHeaderOrFooterImage) {
425
+ const bodySize = opts.imageSize ?? { width: 32, height: 32 }
426
+ exporter.addImageToCell = buildImageHandler(opts.imageFields, bodySize)
427
+ }
428
+ return exporter
429
+ }
430
+
431
+ export async function exportGrid<
432
+ TFeatures extends TableFeatures,
433
+ TData extends RowData,
434
+ >(api: SvGridApi<TFeatures, TData>, opts: ExportOptions<TData>): Promise<void> {
435
+ assertProLicensed('Export')
436
+ await ensureGlobals(opts.format)
437
+ const Ctor = await getDataExporter()
438
+
439
+ // Multi-sheet path. Smart's `spreadsheets` option takes an array of
440
+ // `{ label, dataSource, columns, dataFields? }`. Each entry becomes one
441
+ // tab; the exporter prepends its own header row built from `columns`.
442
+ if (opts.sheets && opts.sheets.length > 0) {
443
+ if (opts.format !== 'xlsx') {
444
+ throw new Error(`@svgrid/enterprise: multi-sheet export requires format 'xlsx', got '${opts.format}'`)
445
+ }
446
+ // Same column-derivation fallback used by the single-sheet path: pull
447
+ // the human-readable headers from the grid when the caller didn't pass
448
+ // explicit columns.
449
+ const gridCols = api.getColumns().filter((c) => c.visible && c.field)
450
+ const fallbackCols: ReadonlyArray<ExportColumn> =
451
+ gridCols.length > 0
452
+ ? gridCols.map((c) => ({ field: c.field!, header: c.header }))
453
+ : []
454
+ const sheets = opts.sheets.map((sheet) => {
455
+ const cols = (sheet.columns ?? opts.columns ?? (
456
+ fallbackCols.length > 0
457
+ ? fallbackCols
458
+ : sheet.rows.length > 0
459
+ ? Object.keys(sheet.rows[0] as Record<string, unknown>)
460
+ .filter((k) => !k.startsWith('_'))
461
+ .map((field) => ({ field } as ExportColumn))
462
+ : []
463
+ ))
464
+ // Project each row to only the included fields (in column order).
465
+ const dataSource = sheet.rows.map((r) => {
466
+ const src = r as unknown as Record<string, unknown>
467
+ const out: Record<string, unknown> = {}
468
+ for (const c of cols) out[c.field] = src[c.field]
469
+ return out
470
+ })
471
+ return {
472
+ label: sheet.label,
473
+ dataSource,
474
+ columns: cols.map((c) => ({ dataField: c.field, label: c.header ?? c.field })),
475
+ dataFields: cols.map((c) => c.field),
476
+ style: translateStyles(sheet.styles ?? opts.styles),
477
+ }
478
+ })
479
+ const filename = (opts.filename ?? 'grid').trim() || 'grid'
480
+ const ctorOpts = buildExporterOptions(opts)
481
+ // Smart needs at least one main sheet to operate. Use the first sheet
482
+ // as the "main" exportData payload AND keep the full set on the
483
+ // `spreadsheets` property so the workbook ends up with one tab per
484
+ // entry.
485
+ const firstSheet = sheets[0]!
486
+ const exporter = new Ctor(ctorOpts)
487
+ exporter.spreadsheets = sheets
488
+ applyInstanceOptions(exporter, opts, firstSheet.dataFields)
489
+ // First sheet's data goes through the standard exportData path with
490
+ // its own header row.
491
+ const headerRow: Record<string, unknown> = {}
492
+ for (const c of firstSheet.columns) headerRow[c.dataField] = c.label
493
+ const firstSheetRows = [headerRow, ...firstSheet.dataSource]
494
+ exporter.exportData(firstSheetRows, opts.format, filename)
495
+ return
496
+ }
497
+
498
+ const { rows, filename } = buildExportPayload(api, opts)
499
+ if (rows.length <= 1) {
500
+ throw new Error('@svgrid/enterprise: nothing to export - the grid has no rows')
501
+ }
502
+ const datafields = Object.keys(rows[0]!)
503
+ // The constructor's SECOND arg is `groupBy`: when non-empty AND
504
+ // `hierarchical` is NOT set, Smart wraps each group of rows in an
505
+ // Excel outline row with an expand/collapse button at the group key.
506
+ const groupBy = !opts.hierarchical && opts.groupBy && opts.groupBy.length > 0
507
+ ? opts.groupBy
508
+ : undefined
509
+ const exporter = new Ctor(buildExporterOptions(opts), groupBy)
510
+ applyInstanceOptions(exporter, opts, datafields)
511
+ exporter.exportData(rows, opts.format, filename)
512
+ }