diff --git a/CHANGELOG.md b/CHANGELOG.md index df38dea..370e0e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,10 +9,20 @@ smaller bundle size. It also opens up new possibilities for new features such as font shaping. -### Breaking +### Removed + +- Support for font and image data as base64-encoded strings and + `ArrayBuffer`. Data must now be provided as `Uint8Array`. -- Font and image data must now be provided as `Uint8Array`. - Base64-encoded strings and `ArrayBuffer`s are no longer accepted. +- The `images` property in the document definition. Images should be + referred to by URL instead. The `ImagesDefinition` and + `ImageDefinition` types have been removed. + +- The `fonts` property in the document definition. Fonts must now be + registered with `PdfMaker.registerFont()`. The `FontsDefinition` and + `FontDefinition` types have been removed. + +### Breaking - Text height is now based on the OS/2 typographic metrics (`sTypoAscender` / `sTypoDescender`) instead of the hhea table values. diff --git a/src/api/PdfMaker.ts b/src/api/PdfMaker.ts index 19e6473..f7679dc 100644 --- a/src/api/PdfMaker.ts +++ b/src/api/PdfMaker.ts @@ -1,5 +1,5 @@ import { FontStore } from '../font-store.ts'; -import { ImageStore } from '../image-store.ts'; +import { createImageLoader } from '../image-loader.ts'; import { layoutPages } from '../layout/layout.ts'; import type { MakerCtx } from '../maker-ctx.ts'; import { readDocumentDefinition } from '../read-document.ts'; @@ -18,12 +18,11 @@ export type FontConfig = { * Generates PDF documents. */ export class PdfMaker { - #ctx: MakerCtx; + #fontStore: FontStore; + #resourceRoot?: string; constructor() { - const fontStore = new FontStore(); - const imageStore = new ImageStore(); - this.#ctx = { fontStore, imageStore }; + this.#fontStore = new FontStore(); } /** @@ -35,7 +34,7 @@ export class PdfMaker { * the meta data cannot be extracted from the font. */ registerFont(data: Uint8Array, config?: FontConfig): void { - this.#ctx.fontStore.registerFont(data, config); + this.#fontStore.registerFont(data, config); } /** @@ -45,7 +44,7 @@ export class PdfMaker { * @param root The root directory to read resources from. */ setResourceRoot(root: string): void { - this.#ctx.imageStore.setResourceRoot(root); + this.#resourceRoot = root; } /** @@ -56,19 +55,10 @@ export class PdfMaker { */ async makePdf(definition: DocumentDefinition): Promise { const def = readAs(definition, 'definition', readDocumentDefinition); - const ctx = { ...this.#ctx }; - if (def.fonts) { - ctx.fontStore = new FontStore(def.fonts); - console.warn( - 'Registering fonts via document definition is deprecated. Use PdfMaker.registerFont() instead.', - ); - } - if (def.images) { - ctx.imageStore = new ImageStore(def.images); - console.warn( - 'Registering images via document definition is deprecated. Use URLs to include images instead.', - ); - } + const ctx: MakerCtx = { + fontStore: this.#fontStore, + imageLoader: createImageLoader(this.#resourceRoot), + }; if (def.dev?.guides != null) ctx.guides = def.dev.guides; const pages = await layoutPages(def, ctx); return await renderDocument(def, pages); diff --git a/src/api/document.ts b/src/api/document.ts index 98fc616..f01edf8 100644 --- a/src/api/document.ts +++ b/src/api/document.ts @@ -51,23 +51,6 @@ export type DocumentDefinition = { */ margin?: Length | BoxLengths | ((info: PageInfo) => Length | BoxLengths); - /** - * The fonts to use in the document. There is no default. Each font that is used in the document - * must be registered. Not needed for documents that contain only graphics. - * - * @deprecated Register fonts with `PdfMaker` instead. - */ - fonts?: FontsDefinition; - - /** - * Pre-defined image data. These images can be used by their name in - * the document. This is only needed if images cannot be loaded - * directly from the file system. - * - * @deprecated Use URLs to include images. - */ - images?: ImagesDefinition; - /** * Metadata to include in the PDF's *document information dictionary*. */ @@ -224,60 +207,6 @@ export type CustomInfoAttrs = CustomInfoProps; */ export type CustomInfoProps = Record<`XX${string}`, string>; -/** - * An object that defines the fonts to use in the document. - * - * @deprecated Register fonts with `PdfMaker` instead. - */ -export type FontsDefinition = { [name: string]: FontDefinition[] }; - -/** - * The definition of a single font. - * - * @deprecated Register fonts with `PdfMaker` instead. - */ -export type FontDefinition = { - /** - * The font data as a Uint8Array. - * - * Supports TrueType font files (`.ttf`) and OpenType (`.otf`) font - * files with TrueType outlines. - */ - data: Uint8Array; - - /** - * Whether this is a bold font. - */ - bold?: boolean; - - /** - * Whether this is an italic font. - */ - italic?: boolean; -}; - -/** - * Pre-defined image data. These images can be used by their name in the - * document. This is only needed if images cannot be loaded directly - * from the file system. - * - * @deprecated Use URLs to include images. - */ -export type ImagesDefinition = { [name: string]: ImageDefinition }; - -/** - * The definition of a single image. - */ -export type ImageDefinition = { - /** - * The image data as a Uint8Array. - * Supported image formats are PNG and JPEG. - * - * @deprecated Use URLs to include images. - */ - data: Uint8Array; -}; - /** * Information about the current page, provided to functions that create * page-specific headers, footers, and margins. diff --git a/src/font-store.test.ts b/src/font-store.test.ts index 0f29846..5ab7005 100644 --- a/src/font-store.test.ts +++ b/src/font-store.test.ts @@ -5,7 +5,6 @@ import type { PDFEmbeddedFont } from '@ralfstx/pdf-core'; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; import { FontStore } from './font-store.ts'; -import type { FontDef } from './fonts.ts'; import { mkData } from './test/test-utils.ts'; vi.mock('@ralfstx/pdf-core', async (importOriginal) => { @@ -39,14 +38,6 @@ vi.mock('@ralfstx/pdf-core', async (importOriginal) => { }); describe('FontStore', () => { - let normalFont: FontDef; - let italicFont: FontDef; - let obliqueFont: FontDef; - let boldFont: FontDef; - let italicBoldFont: FontDef; - let obliqueBoldFont: FontDef; - let otherFont: FontDef; - describe('registerFont', () => { let robotoRegular: Uint8Array; let robotoLightItalic: Uint8Array; @@ -68,8 +59,8 @@ describe('FontStore', () => { const selected1 = await store.selectFont({ fontFamily: 'Roboto' }); const selected2 = await store.selectFont({ fontFamily: 'Roboto Light', fontStyle: 'italic' }); - expect(selected1.pdfFont.fontName).toBe('Roboto'); - expect(selected2.pdfFont.fontName).toBe('Roboto Light Italic'); + expect(selected1.fontName).toBe('Roboto'); + expect(selected2.fontName).toBe('Roboto Light Italic'); }); it('registers font with custom config', async () => { @@ -84,8 +75,8 @@ describe('FontStore', () => { const selected1 = await store.selectFont({ fontFamily: 'Custom Name' }); const selected2 = await store.selectFont({ fontFamily: 'Custom Name', fontWeight: 'bold' }); - expect(selected1.pdfFont.fontName).toBe('Roboto Light Italic'); - expect(selected2.pdfFont.fontName).toBe('Roboto'); + expect(selected1.fontName).toBe('Roboto Light Italic'); + expect(selected2.fontName).toBe('Roboto'); }); }); @@ -93,23 +84,7 @@ describe('FontStore', () => { let store: FontStore; beforeEach(() => { - normalFont = fakeFontDef('Test'); - italicFont = fakeFontDef('Test', { style: 'italic' }); - obliqueFont = fakeFontDef('Test', { style: 'oblique' }); - boldFont = fakeFontDef('Test', { weight: 700 }); - italicBoldFont = fakeFontDef('Test', { style: 'italic', weight: 700 }); - obliqueBoldFont = fakeFontDef('Test', { style: 'oblique', weight: 700 }); - otherFont = fakeFontDef('Other'); - - store = new FontStore([ - normalFont, - italicFont, - obliqueFont, - boldFont, - italicBoldFont, - obliqueBoldFont, - otherFont, - ]); + store = createTestStore(); }); afterEach(() => { @@ -137,7 +112,9 @@ describe('FontStore', () => { }); it('rejects when no matching font style can be found', async () => { - store = new FontStore([normalFont, boldFont]); + const store = new FontStore(); + registerFakeFont(store, 'Test'); + registerFakeFont(store, 'Test', { weight: 700 }); await expect(store.selectFont({ fontFamily: 'Test', fontStyle: 'italic' })).rejects.toThrow( new Error("Could not load font for 'Test', style=italic, weight=normal", { @@ -154,56 +131,62 @@ describe('FontStore', () => { const font3 = await store.selectFont({ fontFamily, fontStyle: 'italic' }); const font4 = await store.selectFont({ fontFamily, fontStyle: 'italic', fontWeight: 'bold' }); - expect(font1.pdfFont.fontName).toBe('MockFont:Test:normal:400'); - expect(font2.pdfFont.fontName).toBe('MockFont:Test:normal:700'); - expect(font3.pdfFont.fontName).toBe('MockFont:Test:italic:400'); - expect(font4.pdfFont.fontName).toBe('MockFont:Test:italic:700'); + expect(font1.fontName).toBe('MockFont:Test:normal:400'); + expect(font2.fontName).toBe('MockFont:Test:normal:700'); + expect(font3.fontName).toBe('MockFont:Test:italic:400'); + expect(font4.fontName).toBe('MockFont:Test:italic:700'); }); it('selects first matching font if no family specified', async () => { const font1 = await store.selectFont({}); - expect(font1.pdfFont.fontName).toBe('MockFont:Test:normal:400'); + expect(font1.fontName).toBe('MockFont:Test:normal:400'); const font2 = await store.selectFont({ fontWeight: 'bold' }); - expect(font2.pdfFont.fontName).toBe('MockFont:Test:normal:700'); + expect(font2.fontName).toBe('MockFont:Test:normal:700'); const font3 = await store.selectFont({ fontStyle: 'italic' }); - expect(font3.pdfFont.fontName).toBe('MockFont:Test:italic:400'); + expect(font3.fontName).toBe('MockFont:Test:italic:400'); const font4 = await store.selectFont({ fontStyle: 'italic', fontWeight: 'bold' }); - expect(font4.pdfFont.fontName).toBe('MockFont:Test:italic:700'); + expect(font4.fontName).toBe('MockFont:Test:italic:700'); }); it('selects font with matching font family', async () => { await expect(store.selectFont({ fontFamily: 'Other' })).resolves.toEqual( - expect.objectContaining({ name: 'MockFont:Other:normal:400' }), + expect.objectContaining({ fontName: 'MockFont:Other:normal:400' }), ); }); it('falls back to oblique when no italic font can be found', async () => { - store = new FontStore([normalFont, obliqueFont, boldFont, obliqueBoldFont]); + const store = new FontStore(); + registerFakeFont(store, 'Test'); + registerFakeFont(store, 'Test', { style: 'oblique' }); + registerFakeFont(store, 'Test', { weight: 700 }); + registerFakeFont(store, 'Test', { style: 'oblique', weight: 700 }); await expect(store.selectFont({ fontFamily: 'Test', fontStyle: 'italic' })).resolves.toEqual( - expect.objectContaining({ name: 'MockFont:Test:oblique:400' }), + expect.objectContaining({ fontName: 'MockFont:Test:oblique:400' }), ); }); it('falls back to italic when no oblique font can be found', async () => { - store = new FontStore([normalFont, italicFont, boldFont, italicBoldFont]); + const store = new FontStore(); + registerFakeFont(store, 'Test'); + registerFakeFont(store, 'Test', { style: 'italic' }); + registerFakeFont(store, 'Test', { weight: 700 }); + registerFakeFont(store, 'Test', { style: 'italic', weight: 700 }); const font = await store.selectFont({ fontFamily: 'Test', fontStyle: 'italic' }); - expect(font.pdfFont).toEqual( - expect.objectContaining({ fontName: 'MockFont:Test:italic:400' }), - ); + expect(font).toEqual(expect.objectContaining({ fontName: 'MockFont:Test:italic:400' })); }); it('falls back when no matching font weight can be found', async () => { await expect(store.selectFont({ fontFamily: 'Other', fontWeight: 'bold' })).resolves.toEqual( - expect.objectContaining({ name: 'MockFont:Other:normal:400' }), + expect.objectContaining({ fontName: 'MockFont:Other:normal:400' }), ); await expect(store.selectFont({ fontFamily: 'Other', fontWeight: 200 })).resolves.toEqual( - expect.objectContaining({ name: 'MockFont:Other:normal:400' }), + expect.objectContaining({ fontName: 'MockFont:Other:normal:400' }), ); }); @@ -228,9 +211,25 @@ describe('FontStore', () => { }); }); -function fakeFontDef(family: string, options?: Partial): FontDef { +function registerFakeFont( + store: FontStore, + family: string, + options?: { style?: string; weight?: number }, +) { const style = options?.style ?? 'normal'; const weight = options?.weight ?? 400; - const data = options?.data ?? mkData([family, style, weight].join(':')); - return { family, style, weight, data }; + const data = mkData([family, style, weight].join(':')); + store.registerFont(data, { family, style: style as 'normal', weight }); +} + +function createTestStore(): FontStore { + const store = new FontStore(); + registerFakeFont(store, 'Test'); + registerFakeFont(store, 'Test', { style: 'italic' }); + registerFakeFont(store, 'Test', { style: 'oblique' }); + registerFakeFont(store, 'Test', { weight: 700 }); + registerFakeFont(store, 'Test', { style: 'italic', weight: 700 }); + registerFakeFont(store, 'Test', { style: 'oblique', weight: 700 }); + registerFakeFont(store, 'Other'); + return store; } diff --git a/src/font-store.ts b/src/font-store.ts index 31123da..2461066 100644 --- a/src/font-store.ts +++ b/src/font-store.ts @@ -1,17 +1,16 @@ -import { PDFEmbeddedFont } from '@ralfstx/pdf-core'; +import { PDFEmbeddedFont, type PDFFont } from '@ralfstx/pdf-core'; import type { FontConfig } from './api/PdfMaker.ts'; import type { FontWeight } from './api/text.ts'; -import type { Font, FontDef, FontSelector } from './fonts.ts'; +import type { FontDef, FontSelector } from './fonts.ts'; import { weightToNumber } from './fonts.ts'; -import { pickDefined } from './types.ts'; export class FontStore { readonly #fontDefs: FontDef[]; - #fontCache: Record> = {}; + #fontCache: Record> = {}; - constructor(fontDefs?: FontDef[]) { - this.#fontDefs = fontDefs ?? []; + constructor() { + this.#fontDefs = []; } registerFont(data: Uint8Array, config?: FontConfig): void { @@ -19,18 +18,18 @@ export class FontStore { const family = config?.family ?? pdfFont.familyName; const style = config?.style ?? pdfFont.style; const weight = weightToNumber(config?.weight ?? pdfFont.weight); - this.#fontDefs.push({ family, style, weight, data }); + this.#fontDefs.push({ family, style, weight, data, pdfFont }); this.#fontCache = {}; // Invalidate cache } - async selectFont(selector: FontSelector): Promise { + async selectFont(selector: FontSelector): Promise { const cacheKey = [ selector.fontFamily ?? 'any', selector.fontStyle ?? 'normal', selector.fontWeight ?? 'normal', ].join(':'); try { - return await (this.#fontCache[cacheKey] ??= this._loadFont(selector, cacheKey)); + return await (this.#fontCache[cacheKey] ??= this._loadFont(selector)); } catch (error) { const { fontFamily: family, fontStyle: style, fontWeight: weight } = selector; const selectorStr = `'${family}', style=${style ?? 'normal'}, weight=${weight ?? 'normal'}`; @@ -38,18 +37,9 @@ export class FontStore { } } - _loadFont(selector: FontSelector, key: string): Promise { + _loadFont(selector: FontSelector): Promise { const selectedFontDef = selectFontDef(this.#fontDefs, selector); - const pdfFont = new PDFEmbeddedFont(selectedFontDef.data); - return Promise.resolve( - pickDefined({ - key, - name: pdfFont.fontName ?? selectedFontDef.family, // TODO ?? pdfFont.postscriptName - style: selector.fontStyle ?? 'normal', - weight: weightToNumber(selector.fontWeight ?? 400), - pdfFont, - }), - ); + return Promise.resolve(selectedFontDef.pdfFont ?? new PDFEmbeddedFont(selectedFontDef.data)); } } diff --git a/src/fonts.test.ts b/src/fonts.test.ts index cccddb8..ab90d9e 100644 --- a/src/fonts.test.ts +++ b/src/fonts.test.ts @@ -1,59 +1,7 @@ import { describe, expect, it } from 'vitest'; import type { FontWeight } from './api/text.ts'; -import { readFonts, weightToNumber } from './fonts.ts'; - -describe('readFonts', () => { - it('returns fonts array', () => { - const fontsDef = { - Test: [ - { data: mkData('Test_Sans_Normal') }, - { data: mkData('Test_Sans_Italic'), italic: true }, - { data: mkData('Test_Sans_Bold'), bold: true }, - { data: mkData('Test_Sans_BoldItalic'), italic: true, bold: true }, - ], - Other: [{ data: mkData('Other_Normal') }], - }; - - const fonts = readFonts(fontsDef); - - expect(fonts).toEqual([ - { family: 'Test', style: 'normal', weight: 400, data: mkData('Test_Sans_Normal') }, - { family: 'Test', style: 'italic', weight: 400, data: mkData('Test_Sans_Italic') }, - { family: 'Test', style: 'normal', weight: 700, data: mkData('Test_Sans_Bold') }, - { family: 'Test', style: 'italic', weight: 700, data: mkData('Test_Sans_BoldItalic') }, - { family: 'Other', style: 'normal', weight: 400, data: mkData('Other_Normal') }, - ]); - }); - - it('throws on missing input', () => { - expect(() => readFonts(undefined)).toThrow(new TypeError('Expected object, got: undefined')); - }); - - it('throws on invalid type', () => { - expect(() => readFonts(23)).toThrow(new TypeError('Expected object, got: 23')); - }); - - it('throws on invalid italic value', () => { - const fn = () => readFonts({ Test: [{ data: 'data', italic: 23 }] }); - - expect(fn).toThrow( - new TypeError('Invalid value for "Test/0/italic": Expected boolean, got: 23'), - ); - }); - - it('throws on invalid bold value', () => { - const fn = () => readFonts({ Test: [{ data: 'data', bold: 23 }] }); - - expect(fn).toThrow(new TypeError('Invalid value for "Test/0/bold": Expected boolean, got: 23')); - }); - - it('throws on missing data', () => { - const fn = () => readFonts({ Test: [{ italic: true }] }); - - expect(fn).toThrow(new TypeError('Invalid value for "Test/0": Missing value for "data"')); - }); -}); +import { weightToNumber } from './fonts.ts'; describe('weightToNumber', () => { it('supports keywords `normal` and `bold`', () => { @@ -79,7 +27,3 @@ describe('weightToNumber', () => { expect(() => weightToNumber(0.1)).toThrow(new Error('Invalid font weight: 0.1')); }); }); - -function mkData(value: string) { - return new Uint8Array(value.split('').map((c) => c.charCodeAt(0))); -} diff --git a/src/fonts.ts b/src/fonts.ts index a2c6f55..1b50503 100644 --- a/src/fonts.ts +++ b/src/fonts.ts @@ -1,9 +1,7 @@ -import type { PDFFont } from '@ralfstx/pdf-core'; +import type { PDFEmbeddedFont } from '@ralfstx/pdf-core'; import type { FontStyle, FontWeight } from './api/text.ts'; -import { readBinaryData } from './binary-data.ts'; import { printValue } from './print-value.ts'; -import { optional, readAs, readBoolean, readObject, required, types } from './types.ts'; /** * The resolved definition of a font. @@ -13,14 +11,7 @@ export type FontDef = { style: FontStyle; weight: number; data: Uint8Array; -}; - -export type Font = { - key: string; - name: string; - style: FontStyle; - weight: number; - pdfFont: PDFFont; + pdfFont?: PDFEmbeddedFont; }; export type FontSelector = { @@ -29,27 +20,6 @@ export type FontSelector = { fontWeight?: FontWeight; }; -export function readFonts(input: unknown): FontDef[] { - return Object.entries(readObject(input)).flatMap(([name, fontDef]) => { - return readAs(fontDef, name, required(types.array(readFont))).map( - (font) => ({ family: name, ...font }) as FontDef, - ); - }); -} - -export function readFont(input: unknown): Partial { - const obj = readObject(input, { - italic: optional((value) => readBoolean(value) || undefined), - bold: optional((value) => readBoolean(value) || undefined), - data: required(readBinaryData), - }); - return { - style: obj.italic ? 'italic' : 'normal', - weight: obj.bold ? 700 : 400, - data: obj.data, - } as FontDef; -} - export function weightToNumber(weight: FontWeight): number { if (weight === 'normal') { return 400; diff --git a/src/frame.ts b/src/frame.ts index 05e87fc..ae4cfb9 100644 --- a/src/frame.ts +++ b/src/frame.ts @@ -1,5 +1,5 @@ -import type { Font } from './fonts.ts'; -import type { Image } from './images.ts'; +import type { PDFFont, PDFImage } from '@ralfstx/pdf-core'; + import type { Color } from './read-color.ts'; import type { PathCommand } from './svg-paths.ts'; @@ -36,7 +36,7 @@ export type TextRowObject = { export type TextSegmentObject = { text: string; - font: Font; + font: PDFFont; fontSize: number; color?: Color; rise?: number; @@ -65,7 +65,7 @@ export type ImageObject = { y: number; width: number; height: number; - image: Image; + image: PDFImage; }; export type GraphicsObject = { diff --git a/src/image-loader.test.ts b/src/image-loader.test.ts new file mode 100644 index 0000000..fecd333 --- /dev/null +++ b/src/image-loader.test.ts @@ -0,0 +1,88 @@ +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; + +import { createImageLoader } from './image-loader.ts'; + +const baseDir = import.meta.dirname; + +describe('createImageLoader', () => { + let torusPng: Uint8Array; + + beforeAll(async () => { + torusPng = new Uint8Array(await readFile(join(baseDir, './test/resources/torus.png'))); + vi.spyOn(globalThis, 'fetch').mockImplementation((req: RequestInfo | URL) => { + const url = req instanceof URL ? req.href : (req as string); + if (url.endsWith('/torus.png')) { + return Promise.resolve(new Response(Buffer.from(torusPng))); + } + return Promise.resolve(new Response('Not found', { status: 404, statusText: 'Not Found' })); + }); + }); + + afterAll(() => { + vi.restoreAllMocks(); + }); + + it('loads image from file URL', async () => { + const loadImage = createImageLoader(baseDir); + + const image = await loadImage('file:/test/resources/torus.png'); + + expect(image.width).toBe(256); + expect(image.height).toBe(192); + }); + + it('loads image from data URL', async () => { + const loadImage = createImageLoader(); + const dataUrl = `data:image/png;base64,${Buffer.from(torusPng).toString('base64')}`; + + const image = await loadImage(dataUrl); + + expect(image.width).toBe(256); + expect(image.height).toBe(192); + }); + + it('loads image from http URL', async () => { + const loadImage = createImageLoader(); + + const image = await loadImage('http://example.com/torus.png'); + + expect(image.width).toBe(256); + expect(image.height).toBe(192); + }); + + it('reads width and height from JPEG image', async () => { + const loadImage = createImageLoader(baseDir); + + const image = await loadImage('file:/test/resources/liberty.jpg'); + + expect(image.width).toBe(160); + expect(image.height).toBe(240); + }); + + it('reads width and height from PNG image', async () => { + const loadImage = createImageLoader(baseDir); + + const image = await loadImage('file:/test/resources/torus.png'); + + expect(image.width).toBe(256); + expect(image.height).toBe(192); + }); + + it('returns same image for same URL', async () => { + const loadImage = createImageLoader(baseDir); + const url = 'file:/test/resources/liberty.jpg'; + + const [image1, image2] = await Promise.all([loadImage(url), loadImage(url)]); + + expect(image1).toBe(image2); + }); + + it('rejects for unsupported URL', async () => { + const loadImage = createImageLoader(); + + await expect(loadImage('foo')).rejects.toThrow("Invalid URL: 'foo'"); + }); +}); diff --git a/src/image-loader.ts b/src/image-loader.ts new file mode 100644 index 0000000..d38b78e --- /dev/null +++ b/src/image-loader.ts @@ -0,0 +1,40 @@ +import { PDFImage } from '@ralfstx/pdf-core'; + +import { createDataLoader, type DataLoader } from './data-loader.ts'; + +type ImageFormat = 'jpeg' | 'png'; + +export type ImageLoader = (url: string) => Promise; + +export function createImageLoader(resourceRoot?: string): ImageLoader { + const dataLoader = createDataLoader(resourceRoot ? { resourceRoot } : undefined); + const cache: Record> = {}; + return (url) => (cache[url] ??= loadImage(url, dataLoader)); +} + +async function loadImage(url: string, dataLoader: DataLoader): Promise { + const { data } = await dataLoader(url); + const format = determineImageFormat(data); + return format === 'jpeg' ? PDFImage.fromJpeg(data) : PDFImage.fromPng(data); +} + +function determineImageFormat(data: Uint8Array): ImageFormat { + if (isPng(data)) return 'png'; + if (isJpeg(data)) return 'jpeg'; + throw new Error('Unknown image format'); +} + +function isJpeg(data: Uint8Array): boolean { + return data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff; +} + +function isPng(data: Uint8Array): boolean { + return hasBytes(data, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); +} + +function hasBytes(data: Uint8Array, offset: number, bytes: number[]) { + for (let i = 0; i < bytes.length; i++) { + if (data[offset + i] !== bytes[i]) return false; + } + return true; +} diff --git a/src/image-store.test.ts b/src/image-store.test.ts deleted file mode 100644 index 8878f7c..0000000 --- a/src/image-store.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { readFile } from 'node:fs/promises'; -import { join } from 'node:path'; - -import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'; - -import { ImageStore } from './image-store.ts'; - -const baseDir = import.meta.dirname; - -describe('ImageStore', () => { - let libertyJpg: Uint8Array; - let torusPng: Uint8Array; - let store: ImageStore; - - beforeAll(async () => { - [libertyJpg, torusPng] = await Promise.all([ - readFile(join(baseDir, './test/resources/liberty.jpg')).then((data) => new Uint8Array(data)), - readFile(join(baseDir, './test/resources/torus.png')).then((data) => new Uint8Array(data)), - ]); - vi.spyOn(globalThis, 'fetch').mockImplementation((req: RequestInfo | URL) => { - const url = req instanceof URL ? req.href : (req as string); - if (url.endsWith('/liberty.jpg')) { - return Promise.resolve(new Response(Buffer.from(libertyJpg))); - } - if (url.endsWith('/torus.png')) { - return Promise.resolve(new Response(Buffer.from(torusPng))); - } - return Promise.resolve(new Response('Not found', { status: 404, statusText: 'Not Found' })); - }); - store = new ImageStore(); - store.setResourceRoot(baseDir); - }); - - afterAll(() => { - vi.restoreAllMocks(); - }); - - it('rejects if image could not be loaded', async () => { - await expect(store.selectImage('foo')).rejects.toThrow(new Error("Could not load image 'foo'")); - }); - - it('loads registered images (deprecated)', async () => { - const store = new ImageStore([ - { name: 'liberty', data: libertyJpg, format: 'jpeg' }, - { name: 'torus', data: torusPng, format: 'png' }, - ]); - - const torus = await store.selectImage('torus'); - const liberty = await store.selectImage('liberty'); - - expect(torus).toEqual(expect.objectContaining({ url: 'torus', format: 'png' })); - expect(liberty).toEqual(expect.objectContaining({ url: 'liberty', format: 'jpeg' })); - }); - - it('loads image from file system (deprecated)', async () => { - const torusPath = join(baseDir, './test/resources/torus.png'); - - const image = await store.selectImage(torusPath); - - expect(image).toEqual(expect.objectContaining({ url: torusPath, format: 'png' })); - }); - - it('loads image from file URL', async () => { - const fileUrl = 'file:/test/resources/torus.png'; - - const image = await store.selectImage(fileUrl); - - expect(image).toEqual(expect.objectContaining({ url: fileUrl, format: 'png' })); - }); - - it('loads image from data URL', async () => { - const dataUrl = `data:image/png;base64,${Buffer.from(torusPng).toString('base64')}`; - - const image = await store.selectImage(dataUrl); - - expect(image).toEqual(expect.objectContaining({ url: dataUrl, format: 'png' })); - }); - - it('loads image from http URL', async () => { - const httpUrl = 'http://example.com/torus.png'; - - const image = await store.selectImage(httpUrl); - - expect(image).toEqual(expect.objectContaining({ url: httpUrl, format: 'png' })); - }); - - it('reads format, width and height from JPEG image', async () => { - const libertyUrl = 'file:/test/resources/liberty.jpg'; - - const image = await store.selectImage(libertyUrl); - - expect(image).toEqual(expect.objectContaining({ format: 'jpeg', width: 160, height: 240 })); - }); - - it('reads format, width and height from PNG image', async () => { - const torusUrl = 'file:/test/resources/torus.png'; - - const image = await store.selectImage(torusUrl); - - expect(image).toEqual(expect.objectContaining({ format: 'png', width: 256, height: 192 })); - }); - - it('loads image only once for one URL', async () => { - const store = new ImageStore(); - store.setResourceRoot(baseDir); - const url = 'file:/test/resources/liberty.jpg'; - - const [image1, image2] = await Promise.all([store.selectImage(url), store.selectImage(url)]); - - expect(image1).toBe(image2); - }); - - it('returns same image object for concurrent calls', async () => { - const libertyUrl = 'file:/test/resources/liberty.jpg'; - - const [image1, image2] = await Promise.all([ - store.selectImage(libertyUrl), - store.selectImage(libertyUrl), - ]); - - expect(image1).toBe(image2); - }); -}); diff --git a/src/image-store.ts b/src/image-store.ts deleted file mode 100644 index e7bc237..0000000 --- a/src/image-store.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { PDFImage } from '@ralfstx/pdf-core'; - -import { createDataLoader, type DataLoader } from './data-loader.ts'; -import { readRelativeFile } from './fs.ts'; -import type { Image, ImageDef, ImageFormat } from './images.ts'; - -export class ImageStore { - readonly #images: ImageDef[]; - readonly #imageCache: Record> = {}; - #dataLoader: DataLoader; - - constructor(images?: ImageDef[]) { - this.#images = images ?? []; - this.#dataLoader = createDataLoader(); - } - - setResourceRoot(root: string) { - this.#dataLoader = createDataLoader({ resourceRoot: root }); - } - - selectImage(url: string): Promise { - return (this.#imageCache[url] ??= this.loadImage(url)); - } - - async loadImage(url: string): Promise { - const data = await this.loadImageData(url); - const format = determineImageFormat(data); - if (format === 'jpeg') { - const pdfImage = PDFImage.fromJpeg(data); - const { width, height } = pdfImage; - return { url, format, width, height, pdfImage }; - } - if (format === 'png') { - const pdfImage = PDFImage.fromPng(data); - const { width, height } = pdfImage; - return { url, format, width, height, pdfImage }; - } - throw new Error(`Unsupported image format: ${format}`); - } - - async loadImageData(url: string): Promise { - const imageDef = this.#images.find((image) => image.name === url); - if (imageDef) { - return imageDef.data; - } - - const urlSchema = /^(\w+):/.exec(url)?.[1]; - try { - if (urlSchema) { - const { data } = await this.#dataLoader(url); - return data; - } - console.warn( - `Loading images from file names is deprecated ('${url}'). Use file:/ URLs instead.`, - ); - const data = await readRelativeFile('/', url.replace(/^\/+/, '')); - return new Uint8Array(data); - } catch (error) { - throw new Error(`Could not load image '${url}'`, { cause: error }); - } - } -} - -function determineImageFormat(data: Uint8Array): ImageFormat { - if (isPng(data)) return 'png'; - if (isJpeg(data)) return 'jpeg'; - throw new Error('Unknown image format'); -} - -function isJpeg(data: Uint8Array): boolean { - return data[0] === 0xff && data[1] === 0xd8 && data[2] === 0xff; -} - -function isPng(data: Uint8Array): boolean { - // check PNG signature - return hasBytes(data, 0, [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]); -} - -function hasBytes(data: Uint8Array, offset: number, bytes: number[]) { - for (let i = 0; i < bytes.length; i++) { - if (data[offset + i] !== bytes[i]) return false; - } - return true; -} diff --git a/src/images.test.ts b/src/images.test.ts deleted file mode 100644 index 2edd27c..0000000 --- a/src/images.test.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { readImages } from './images.ts'; -import { mkData } from './test/test-utils.ts'; - -describe('readImages', () => { - it('returns an empty array for missing images definition', () => { - const images = readImages({}); - - expect(images).toEqual([]); - }); - - it('returns images array', () => { - const imagesDef = { - foo: { data: mkData('Foo') }, - bar: { data: mkData('Bar'), format: 'jpeg' }, - baz: { data: mkData('Baz'), format: 'png' }, - }; - - const images = readImages(imagesDef); - - expect(images).toEqual([ - { name: 'foo', data: mkData('Foo'), format: 'jpeg' }, - { name: 'bar', data: mkData('Bar'), format: 'jpeg' }, - { name: 'baz', data: mkData('Baz'), format: 'png' }, - ]); - }); - - it('throws on invalid type', () => { - const fn = () => readImages(23); - - expect(fn).toThrow(new TypeError('Expected object, got: 23')); - }); - - it('throws on invalid image definition', () => { - const fn = () => readImages({ foo: 23 }); - - expect(fn).toThrow(new TypeError('Invalid value for "foo": Expected object, got: 23')); - }); - - it('throws on invalid image data', () => { - const fn = () => readImages({ foo: { data: 23 } }); - - expect(fn).toThrow(new TypeError('Invalid value for "foo/data": Expected Uint8Array, got: 23')); - }); -}); diff --git a/src/images.ts b/src/images.ts deleted file mode 100644 index 599a39a..0000000 --- a/src/images.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { PDFImage } from '@ralfstx/pdf-core'; - -import { readBinaryData } from './binary-data.ts'; -import { optional, readAs, readObject, required, types } from './types.ts'; - -const imageFormats = ['jpeg', 'png']; -export type ImageFormat = (typeof imageFormats)[number]; - -export type ImageDef = { - name: string; - data: Uint8Array; - format: ImageFormat; -}; - -export type Image = { - url: string; - width: number; - height: number; - format: ImageFormat; - pdfImage: PDFImage; -}; - -export function readImages(input: unknown): ImageDef[] { - return Object.entries(readObject(input)).map(([name, imageDef]) => { - const { data, format } = readAs(imageDef, name, required(readImage)); - return { name, data, format: format ?? 'jpeg' }; - }); -} - -function readImage(input: unknown) { - return readObject(input, { - data: required(readBinaryData), - format: optional(types.string({ enum: imageFormats })), - }) as { data: Uint8Array; format?: ImageFormat }; -} diff --git a/src/layout/layout-image.test.ts b/src/layout/layout-image.test.ts index 083bc21..eab2862 100644 --- a/src/layout/layout-image.test.ts +++ b/src/layout/layout-image.test.ts @@ -1,27 +1,26 @@ import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { Box } from '../box.ts'; -import { ImageStore } from '../image-store.ts'; import type { MakerCtx } from '../maker-ctx.ts'; import type { ImageBlock } from '../read-block.ts'; import { fakeImage } from '../test/test-utils.ts'; import { layoutImageContent } from './layout-image.ts'; +const mockImageLoader = vi.fn((selector: string) => { + const match = /^img-(\d+)-(\d+)$/.exec(selector); + if (match) { + return Promise.resolve(fakeImage(Number(match[1]), Number(match[2]))); + } + throw new Error(`Unknown image: ${selector}`); +}); + describe('layout-image', () => { let box: Box; let ctx: MakerCtx; beforeEach(() => { - const imageStore = new ImageStore(); - imageStore.selectImage = vi.fn((selector: string) => { - const match = /^img-(\d+)-(\d+)$/.exec(selector); - if (match) { - return Promise.resolve(fakeImage(selector, Number(match[1]), Number(match[2]))); - } - throw new Error(`Unknown image: ${selector}`); - }); box = { x: 20, y: 30, width: 400, height: 700 }; - ctx = { imageStore } as MakerCtx; + ctx = { imageLoader: mockImageLoader } as unknown as MakerCtx; }); describe('layoutImageContent', () => { @@ -41,14 +40,13 @@ describe('layout-image', () => { }); }); - it('passes width and height to ImageStore', async () => { + it('calls imageLoader with the image URL', async () => { const block = { image: 'img-720-480', width: 30, height: 40 }; box = { x: 20, y: 30, width: 200, height: 100 }; await layoutImageContent(block, box, ctx); - // eslint-disable-next-line @typescript-eslint/unbound-method - expect(ctx.imageStore.selectImage).toHaveBeenCalledWith('img-720-480'); + expect(mockImageLoader).toHaveBeenCalledWith('img-720-480'); }); ['img-720-480', 'img-72-48'].forEach((image) => { diff --git a/src/layout/layout-image.ts b/src/layout/layout-image.ts index bf2291f..cc78d08 100644 --- a/src/layout/layout-image.ts +++ b/src/layout/layout-image.ts @@ -1,6 +1,7 @@ +import type { PDFImage } from '@ralfstx/pdf-core'; + import type { Box, Pos, Size } from '../box.ts'; import type { ImageObject, RenderObject } from '../frame.ts'; -import type { Image } from '../images.ts'; import type { MakerCtx } from '../maker-ctx.ts'; import type { ImageBlock } from '../read-block.ts'; import type { LayoutContent } from './layout.ts'; @@ -10,7 +11,7 @@ export async function layoutImageContent( box: Box, ctx: MakerCtx, ): Promise { - const image = await ctx.imageStore.selectImage(block.image); + const image = await ctx.imageLoader(block.image); const hasFixedWidth = block.width != null; const hasFixedHeight = block.height != null; const scale = getScale(image, box, hasFixedWidth, hasFixedHeight); @@ -46,6 +47,6 @@ function align(box: Box, size: Size, alignment?: string): Pos { return { x: box.x + xShift, y: box.y + yShift }; } -function createImageObject(image: Image, pos: Pos, size: Size): ImageObject { +function createImageObject(image: PDFImage, pos: Pos, size: Size): ImageObject { return { type: 'image', image, x: pos.x, y: pos.y, width: size.width, height: size.height }; } diff --git a/src/layout/layout-text.test.ts b/src/layout/layout-text.test.ts index 18a07bb..cf35f31 100644 --- a/src/layout/layout-text.test.ts +++ b/src/layout/layout-text.test.ts @@ -1,15 +1,16 @@ +import type { PDFFont } from '@ralfstx/pdf-core'; import { beforeEach, describe, expect, it } from 'vitest'; import type { Box } from '../box.ts'; import { rgb } from '../colors.ts'; import { FontStore } from '../font-store.ts'; -import type { Font, FontSelector } from '../fonts.ts'; +import type { FontSelector } from '../fonts.ts'; import type { MakerCtx } from '../maker-ctx.ts'; import { extractTextRows, fakeFont, range, span } from '../test/test-utils.ts'; import { layoutTextContent } from './layout-text.ts'; describe('layout-text', () => { - let defaultFont: Font; + let defaultFont: PDFFont; let box: Box; let ctx: MakerCtx; diff --git a/src/layout/layout-text.ts b/src/layout/layout-text.ts index 310e2b5..0c21e15 100644 --- a/src/layout/layout-text.ts +++ b/src/layout/layout-text.ts @@ -1,5 +1,6 @@ +import type { PDFFont } from '@ralfstx/pdf-core'; + import type { Box, Size } from '../box.ts'; -import type { Font } from '../fonts.ts'; import type { LinkObject, RenderObject, TextRowObject, TextSegmentObject } from '../frame.ts'; import { createRowGuides } from '../guides.ts'; import type { MakerCtx } from '../maker-ctx.ts'; @@ -136,8 +137,8 @@ function layoutTextRow(segments: TextSegment[], box: Box) { return { row, objects, remainder }; } -function getDescent(font: Font, fontSize: number) { - return Math.abs((font.pdfFont.descent * fontSize) / 1000); +function getDescent(font: PDFFont, fontSize: number) { + return Math.abs((font.descent * fontSize) / 1000); } /** diff --git a/src/maker-ctx.ts b/src/maker-ctx.ts index 3430f22..7dcfd48 100644 --- a/src/maker-ctx.ts +++ b/src/maker-ctx.ts @@ -1,8 +1,8 @@ import type { FontStore } from './font-store.ts'; -import type { ImageStore } from './image-store.ts'; +import type { ImageLoader } from './image-loader.ts'; export type MakerCtx = { fontStore: FontStore; - imageStore: ImageStore; + imageLoader: ImageLoader; guides?: boolean; }; diff --git a/src/read-document.ts b/src/read-document.ts index 1d9aad7..6f2a353 100644 --- a/src/read-document.ts +++ b/src/read-document.ts @@ -3,10 +3,6 @@ import type { PDFDocument } from '@ralfstx/pdf-core'; import type { FileRelationShip } from './api/document.ts'; import type { BoxEdges, Size } from './box.ts'; import { parseEdges } from './box.ts'; -import type { FontDef } from './fonts.ts'; -import { readFonts } from './fonts.ts'; -import type { ImageDef } from './images.ts'; -import { readImages } from './images.ts'; import type { Block, TextAttrs } from './read-block.ts'; import { readBlock, readInheritableAttrs } from './read-block.ts'; import { parseOrientation, readPageSize } from './read-page-size.ts'; @@ -14,8 +10,6 @@ import type { Obj } from './types.ts'; import { dynamic, optional, readAs, readObject, required, typeError, types } from './types.ts'; export type DocumentDefinition = { - fonts?: FontDef[]; - images?: ImageDef[]; pageSize?: Size; pageOrientation?: 'portrait' | 'landscape'; info?: Metadata; @@ -57,8 +51,6 @@ export type PageInfo = { export function readDocumentDefinition(input: unknown): DocumentDefinition { const def1 = readObject(input, { - fonts: optional(readFonts), - images: optional(readImages), pageSize: optional(readPageSize), pageOrientation: optional(parseOrientation), info: optional(readInfo), diff --git a/src/render/render-document.test.ts b/src/render/render-document.test.ts index 5cb6b41..4091e50 100644 --- a/src/render/render-document.test.ts +++ b/src/render/render-document.test.ts @@ -52,6 +52,16 @@ describe('renderDocument', () => { expect(dataString).toMatch(/\/bar /); }); + it('sets dates even without info', async () => { + const def = { content: [] }; + + const pdfData = await renderDocument(def, [], noObjectStreams); + const dataString = new TextDecoder().decode(pdfData); + + expect(dataString).toMatch(/\/CreationDate \(D:20250101000000Z\)/); + expect(dataString).toMatch(/\/ModDate \(D:20250101000000Z\)/); + }); + it('renders custom data', async () => { const def = { content: [], diff --git a/src/render/render-document.ts b/src/render/render-document.ts index 2a72fcd..7a70e6e 100644 --- a/src/render/render-document.ts +++ b/src/render/render-document.ts @@ -36,19 +36,17 @@ export async function renderDocument( function setMetadata(doc: PDFDocument, info?: Metadata) { const now = new Date(); - if (info !== undefined) { - doc.setInfo({ - creationDate: info.creationDate ?? now, - modDate: now, - title: info.title, - subject: info.subject, - keywords: info.keywords?.join(', '), - author: info.author, - creator: info.creator, - producer: info.producer, - ...info.custom, - }); - } + doc.setInfo({ + creationDate: info?.creationDate ?? now, + modDate: now, + title: info?.title, + subject: info?.subject, + keywords: info?.keywords?.join(', '), + author: info?.author, + creator: info?.creator, + producer: info?.producer, + ...info?.custom, + }); } function setCustomData(data: Record, doc: PDFDocument) { diff --git a/src/render/render-image.test.ts b/src/render/render-image.test.ts index bff1230..b669d7a 100644 --- a/src/render/render-image.test.ts +++ b/src/render/render-image.test.ts @@ -1,9 +1,8 @@ -import { PDFPage } from '@ralfstx/pdf-core'; +import { type PDFImage, PDFPage } from '@ralfstx/pdf-core'; import { beforeEach, describe, expect, it } from 'vitest'; import type { Size } from '../box.ts'; import type { ImageObject } from '../frame.ts'; -import type { Image } from '../images.ts'; import type { Page } from '../page.ts'; import { fakeImage, getContentStream } from '../test/test-utils.ts'; import { renderImage } from './render-image.ts'; @@ -12,13 +11,13 @@ describe('renderImage', () => { const pos = { x: 10, y: 20 }; let page: Page; let size: Size; - let image: Image; + let image: PDFImage; beforeEach(() => { size = { width: 500, height: 800 }; const pdfPage = new PDFPage(size.width, size.height); page = { size, pdfPage } as Page; - image = fakeImage('test-image.jpg', 100, 150); + image = fakeImage(100, 150); }); it('renders single image object', () => { diff --git a/src/render/render-image.ts b/src/render/render-image.ts index eda24c9..1fb6b64 100644 --- a/src/render/render-image.ts +++ b/src/render/render-image.ts @@ -11,6 +11,6 @@ export function renderImage(object: ImageObject, page: Page, base: Pos) { .saveGraphicsState() .translate(x, y) .scale(width, height) - .drawImage(object.image.pdfImage) + .drawImage(object.image) .restoreGraphicsState(); } diff --git a/src/render/render-page.test.ts b/src/render/render-page.test.ts index c82cc36..b0d86d7 100644 --- a/src/render/render-page.test.ts +++ b/src/render/render-page.test.ts @@ -1,9 +1,8 @@ -import type { PDFDocument } from '@ralfstx/pdf-core'; +import type { PDFDocument, PDFFont } from '@ralfstx/pdf-core'; import { PDFPage } from '@ralfstx/pdf-core'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { Size } from '../box.ts'; -import type { Font } from '../fonts.ts'; import type { Frame } from '../frame.ts'; import type { Page } from '../page.ts'; import { fakeFont, getContentStream } from '../test/test-utils.ts'; @@ -79,7 +78,7 @@ describe('render-page', () => { describe('renderFrame', () => { let page: Page; let size: Size; - let font: Font; + let font: PDFFont; beforeEach(() => { size = { width: 500, height: 800 }; diff --git a/src/render/render-text.test.ts b/src/render/render-text.test.ts index c5c2670..3bfed13 100644 --- a/src/render/render-text.test.ts +++ b/src/render/render-text.test.ts @@ -1,8 +1,8 @@ +import type { PDFFont } from '@ralfstx/pdf-core'; import { PDFPage } from '@ralfstx/pdf-core'; import { beforeEach, describe, expect, it } from 'vitest'; import type { Size } from '../box.ts'; -import type { Font } from '../fonts.ts'; import type { TextObject } from '../frame.ts'; import type { Page } from '../page.ts'; import { fakeFont, getContentStream } from '../test/test-utils.ts'; @@ -11,7 +11,7 @@ import { renderText } from './render-text.ts'; describe('render-text', () => { let page: Page; let size: Size; - let font: Font; + let font: PDFFont; beforeEach(() => { size = { width: 500, height: 800 }; diff --git a/src/render/render-text.ts b/src/render/render-text.ts index 50d9496..b4f7217 100644 --- a/src/render/render-text.ts +++ b/src/render/render-text.ts @@ -18,12 +18,10 @@ export function renderText(object: TextObject, page: Page, base: Pos) { object.rows?.forEach((row) => { cs.setTextMatrix(1, 0, 0, 1, x + row.x, y - row.y - row.baseline); row.segments?.forEach((seg) => { - const pdfFont = seg.font.pdfFont; - if (!pdfFont) throw new Error('PDF font not initialized'); - const glyphRun = pdfFont.shapeText(seg.text, { defaultFeatures: false }); + const glyphRun = seg.font.shapeText(seg.text, { defaultFeatures: false }); setTextColorOp(cs, state, seg.color); - setTextFontAndSizeOp(cs, state, pdfFont, seg.fontSize); + setTextFontAndSizeOp(cs, state, seg.font, seg.fontSize); setTextRiseOp(cs, state, seg.rise); setLetterSpacingOp(cs, state, seg.letterSpacing); cs.showPositionedText(glyphRun); diff --git a/src/test/test-utils.ts b/src/test/test-utils.ts index 8363d34..b80cda1 100644 --- a/src/test/test-utils.ts +++ b/src/test/test-utils.ts @@ -1,35 +1,20 @@ import type { PDFFont } from '@ralfstx/pdf-core'; import { PDFImage, PDFRef } from '@ralfstx/pdf-core'; -import type { Font } from '../fonts.ts'; +import type { FontWeight } from '../api/text.ts'; import { weightToNumber } from '../fonts.ts'; import type { Frame } from '../frame.ts'; -import type { Image } from '../images.ts'; import type { Page } from '../page.ts'; import type { TextAttrs, TextSpan } from '../read-block.ts'; -export function fakeFont(name: string, opts?: Partial>): Font { - const key = `${name}-${opts?.style ?? 'normal'}-${opts?.weight ?? 400}`; - const font: Font = { - key, - name, - style: opts?.style ?? 'normal', - weight: weightToNumber(opts?.weight ?? 'normal'), - pdfFont: fakePdfFont(key), - }; - return font; +export function fakeFont(name: string, opts?: { style?: string; weight?: FontWeight }): PDFFont { + const key = `${name}-${opts?.style ?? 'normal'}-${weightToNumber(opts?.weight ?? 'normal')}`; + return fakePdfFont(key); } -export function fakeImage(name: string, width: number, height: number): Image { +export function fakeImage(width: number, height: number): PDFImage { const data = createTestJpeg(width, height); - return { - name, - width, - height, - format: 'jpeg', - url: 'test', - pdfImage: PDFImage.fromJpeg(data), - } as Image; + return PDFImage.fromJpeg(data); } /** diff --git a/src/text.test.ts b/src/text.test.ts index 3feee9c..9dc31a9 100644 --- a/src/text.test.ts +++ b/src/text.test.ts @@ -1,8 +1,8 @@ +import type { PDFFont } from '@ralfstx/pdf-core'; import { beforeEach, describe, expect, it } from 'vitest'; import { rgb } from './colors.ts'; import { FontStore } from './font-store.ts'; -import type { Font } from './fonts.ts'; import { fakeFont } from './test/test-utils.ts'; import type { TextSegment } from './text.ts'; import { @@ -14,7 +14,7 @@ import { } from './text.ts'; describe('text', () => { - let normalFont: Font; + let normalFont: PDFFont; let fontStore: FontStore; beforeEach(() => { diff --git a/src/text.ts b/src/text.ts index 3a42af3..ac6ae42 100644 --- a/src/text.ts +++ b/src/text.ts @@ -1,7 +1,8 @@ +import type { PDFFont } from '@ralfstx/pdf-core'; + import type { FontStyle, FontWeight } from './api/text.ts'; import { getTextHeight, getTextWidth } from './font-metrics.ts'; import type { FontStore } from './font-store.ts'; -import type { Font } from './fonts.ts'; import type { TextSpan } from './read-block.ts'; import type { Color } from './read-color.ts'; @@ -13,7 +14,7 @@ export type TextSegment = { width: number; height: number; lineHeight: number; - font: Font; + font: PDFFont; fontSize: number; fontFamily: string; fontStyle?: FontStyle; @@ -43,13 +44,13 @@ export async function extractTextSegments( letterSpacing, } = attrs; const font = await fontStore.selectFont({ fontFamily, fontStyle, fontWeight }); - const height = getTextHeight(font.pdfFont, fontSize); + const height = getTextHeight(font, fontSize); return splitChunks(text).map( (text) => ({ text, - width: getTextWidth(text, font.pdfFont, fontSize) + text.length * (letterSpacing ?? 0), + width: getTextWidth(text, font, fontSize) + text.length * (letterSpacing ?? 0), height, lineHeight, font,