diff --git a/README.md b/README.md index 7e9ec81..eae8b48 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,7 @@ import { CreateUrlImageRequest } from '@html-css-to-image/client'; const request = new CreateUrlImageRequest({ url: 'https://example.com', + css: '.cookie-banner { display: none; }', full_screen: true }); @@ -83,7 +84,7 @@ These methods are handy when you have a lot of content that may never be rendere ### Signed Template URLs ```typescript -const signedUrl = client.createTemplatedImageUrl('your_template_id', { +const signedUrl = client.generateTemplatedImageUrl('your_template_id', { title: 'Dynamic Title' }); @@ -102,11 +103,11 @@ const this_item = { id: 123, updated_at: new Date(2025, 7, 15) }; -const url_request = new CreateUrlImageRequest({ - url: `https://website.com/_social/${this_tem_id}?updated_at=${updated_at.valueOf()}`, - viewport_width: 600, +const url_request = new CreateUrlImageRequest({ + url: `https://website.com/_social/${this_item.id}?updated_at=${this_item.updated_at.valueOf()}`, + viewport_width: 600, viewport_height: 200}); -const signedUrl = client.generateCreateAndRenderUr(url_request); +const signedUrl = client.generateCreateAndRenderUrl(url_request); // now it's safe to use signedUrl on the frontend in a meta or img tag, without exposing your API key, and it will be kept updated as the updated_at changes. ``` @@ -116,13 +117,15 @@ const signedUrl = client.generateCreateAndRenderUr(url_request); If you have created a template for your account, you can generate an image by passing your template ID and the data for your variables. ```typescript -const result = await client.createImage({ +import { CreateTemplatedImageRequest } from '@html-css-to-image/client'; + +const result = await client.createImage(new CreateTemplatedImageRequest({ template_id: 'your_template_id', template_values: { title: 'Hello from Template', subtitle: 'This is a dynamic value' } -}); +})); ``` ### PDF Generation @@ -200,4 +203,4 @@ const client = new HtmlCssToImageClient(apiId, apiKey, fetch); > Check out the [HTML/CSS To Image Docs](https://docs.htmlcsstoimage.com) for more details on the API's capabilities. > [!TIP] -> Get started for free at [htmlcsstoimage.com](https://htmlcsstoimage.com). \ No newline at end of file +> Get started for free at [htmlcsstoimage.com](https://htmlcsstoimage.com). diff --git a/package.json b/package.json index 4e927cc..92f15ea 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,17 @@ { "name": "@html-css-to-image/client", - "version": "0.2.0", + "version": "0.3.0", "type": "module", "description": "Lightweight TypeScript client for the HTML/CSS to Image API. Generate images from HTML/CSS, URLs, or templates in your TypeScript or JavaScript projects.", "main": "dist/index.js", - "module": "dist/index.mjs", "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js", + "default": "./dist/index.js" + } + }, "repository": { "type": "git", "url": "git+https://github.com/htmlcsstoimage/ts-client.git" @@ -18,7 +24,8 @@ ], "scripts": { "test": "tsx --test tests/*.test.ts", - "build": "tsc", + "clean": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\"", + "build": "npm run clean && tsc", "prepublishOnly": "npm run build" }, "keywords": [ diff --git a/src/HtmlCssToImageClient.ts b/src/HtmlCssToImageClient.ts index 1de3afb..171aac5 100644 --- a/src/HtmlCssToImageClient.ts +++ b/src/HtmlCssToImageClient.ts @@ -1,11 +1,49 @@ -import {IHtmlCssToImageClient} from "./IHtmlCssToImageClient.js"; -import {CreateHtmlCssImageRequest, CreateTemplatedImageRequest, CreateUrlImageRequest, PDFOptions, PdfValueInput} from "./types/request.js"; -import {CreateImageBatchResponse, CreateImageBatchSuccessResponse, CreateImageResponse} from "./types/response.js"; -import {InternalCreateHtmlCssImageRequest, InternalCreateHtmlCssImageRequestWithOptionalHtml, InternalCreateUrlImageRequest, InternalCreateUrlImageRequestWithOptionalUrl, InternalPDFOptions} from "./types/internals.js"; +import type {IHtmlCssToImageClient} from "./IHtmlCssToImageClient.js"; +import {CreateTemplatedImageRequest} from "./types/request.js"; +import type {BaseCreateImageRequest, CreateHtmlCssImageRequest, CreateUrlImageRequest, PDFOptions, PdfValueInput} from "./types/request.js"; +import type {CreateImageBatchResponse, CreateImageBatchSuccessResponse, CreateImageResponse} from "./types/response.js"; import * as crypto from 'node:crypto'; export type FetchFunction = (input: string | URL | Request, init?: RequestInit) => Promise; +interface InternalPDFOptions { + print_background?: boolean; + scale?: number; + margins?: string[]; + page_height?: string; + page_width?: string; +} + +type InternalBaseCreateRequest = Omit & { + pdf_options?: InternalPDFOptions; +}; + +type InternalCreateHtmlCssImageRequest = InternalBaseCreateRequest & { + html: string; + css?: string; + google_fonts?: string; +}; + +type InternalCreateHtmlCssImageRequestWithOptionalHtml = InternalBaseCreateRequest & { + html?: string; + css?: string; + google_fonts?: string; +}; + +type InternalCreateUrlImageRequest = InternalBaseCreateRequest & { + url: string; + css?: string; + full_screen?: boolean; + block_consent_banners?: boolean; +}; + +type InternalCreateUrlImageRequestWithOptionalUrl = InternalBaseCreateRequest & { + url?: string; + css?: string; + full_screen?: boolean; + block_consent_banners?: boolean; +}; + export class HtmlCssToImageClient implements IHtmlCssToImageClient { private readonly apiId: string; @@ -65,7 +103,10 @@ export class HtmlCssToImageClient implements IHtmlCssToImageClient { case 'url': return this.mapUrlToInternalRequest(request, in_batch); case 'templated': - return request; + { + const {__type, ...templatedRequest} = request; + return templatedRequest; + } default: throw new Error("Unsupported request type"); } @@ -232,6 +273,7 @@ export class HtmlCssToImageClient implements IHtmlCssToImageClient { } return new_pdf_options; } + return undefined; } @@ -251,4 +293,4 @@ export class HtmlCssToImageClient implements IHtmlCssToImageClient { } -} \ No newline at end of file +} diff --git a/src/IHtmlCssToImageClient.ts b/src/IHtmlCssToImageClient.ts index 7de08d8..3e1f71a 100644 --- a/src/IHtmlCssToImageClient.ts +++ b/src/IHtmlCssToImageClient.ts @@ -1,5 +1,5 @@ -import {CreateHtmlCssImageRequest, CreateTemplatedImageRequest, CreateUrlImageRequest} from "./types/request.js"; -import {CreateImageBatchResponse, CreateImageErrorResponse, CreateImageResponse, CreateImageSuccessResponse} from "./types/response.js"; +import type {CreateHtmlCssImageRequest, CreateTemplatedImageRequest, CreateUrlImageRequest} from "./types/request.js"; +import type {CreateImageBatchResponse, CreateImageResponse} from "./types/response.js"; export interface IHtmlCssToImageClient { createImage(request: CreateHtmlCssImageRequest|CreateUrlImageRequest|CreateTemplatedImageRequest) : Promise; @@ -7,4 +7,4 @@ export interface IHtmlCssToImageClient { generateCreateAndRenderUrl(request: CreateUrlImageRequest): string; generateTemplatedImageUrl>(template_id: string, template_values: T, template_version?: number):string; generateTemplatedImageUrl(request: CreateTemplatedImageRequest):string; -} \ No newline at end of file +} diff --git a/src/index.ts b/src/index.ts index b744534..7095d78 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,16 +1,28 @@ export { BaseCreateImageRequest, CreateHtmlCssImageRequest, + CreateTemplatedImageRequest, CreateUrlImageRequest, - PDFOptions, + PDFOptions +} from './types/request.js'; + +export type { + ColorSchemeType, + MediaType, PdfMargins, + PdfValueInput, PdfValueWithUnits, PdfUnit } from './types/request.js'; -export { +export type { + CreateImageBatchResponse, + CreateImageBatchSuccessResponse, + CreateImageErrorResponse, CreateImageResponse, - CreateImageBatchResponse + CreateImageSuccessResponse, + ValidationError } from './types/response.js'; export { HtmlCssToImageClient } from './HtmlCssToImageClient.js'; -export { IHtmlCssToImageClient } from './IHtmlCssToImageClient.js'; \ No newline at end of file +export type { FetchFunction } from './HtmlCssToImageClient.js'; +export type { IHtmlCssToImageClient } from './IHtmlCssToImageClient.js'; diff --git a/src/types/internals.ts b/src/types/internals.ts deleted file mode 100644 index 36253ac..0000000 --- a/src/types/internals.ts +++ /dev/null @@ -1,156 +0,0 @@ -import {ColorSchemeType, MediaType} from "./request.js"; - -export abstract class BaseCreateRequestWithoutVariableOptions { - /** - * A CSS selector to target a specific element on the page. - * The API will crop the image to the dimensions of this element. - */ - selector?: string; - - /** - * Adjusts the pixel ratio for the screenshot. - * The default is 2 which is equivalent to a 4K monitor. - */ - device_scale?: number; - - /** - * Set the height of Chrome's viewport. This will disable automatic cropping. - */ - viewport_height?: number; - - /** - * Set the width of Chrome's viewport. This will disable automatic cropping. - */ - viewport_width?: number; - - /** - * Sets a limit on time to wait until the screenshot is taken. - * Use this if your page loads a lot of extra irrelevant content. - */ - max_wait_ms?: number; - - /** - * Adds extra time before taking the screenshot, - * like if you need to wait for Javascript to execute. - */ - ms_delay?: number; - - /** - * This will wait until 'ScreenshotReady()' is called from Javascript to take the screenshot. - */ - render_when_ready?: boolean; - - /** - * Ensure the image is only ever rendered and saved one time. - * This is an advanced option not applicable to most scenarios. - */ - max_render_once?: boolean; - - /** - * Twemoji is used to render emoji as a fallback for native emoji fonts. - * This option will disable that behavior. - */ - disable_twemoji?: boolean; - /** - * Set Chrome to render assuming the user has explicitly set their browser to Light or Dark mode. - */ - color_scheme?: ColorSchemeType; - - /** - * Sets the timezone for the browser instance. Use IANA timezone format (e.g. 'America/New_York'). - */ - timezone?: string; - - /** - Sets a value indicating whether the viewport should be rendered as if it's being viewed from a mobile device. - */ - viewport_mobile?: boolean; - - /** - Sets a value indicating whether touch interactions are enabled within the viewport. - */ - viewport_touch?: boolean; - - /** - * sets a value indicating whether the viewport should be in landscape orientation. - */ - viewport_landscape?: boolean; - - /** - * Sets the media type to use for rendering, 'print' or 'screen'. - */ - media_type?: MediaType; - - /** - * Sets which proxy_id for your organization to use when rendering. - */ - proxy_id?: string; - - /** - * Sets the Maximum width of the rendered image in jumbo mode. Consumes extra renders, jumbo_max_height to be defined as well. - */ - jumbo_max_width?: number; - - /** - * Sets the Maximum height of the rendered image in jumbo mode. Consumes extra renders, jumbo_max_width to be defined as well. - */ - jumbo_max_height?: number; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} - -export abstract class BasePDFOptions { - /** - * Whether the background graphics should be printed in the PDF output. - */ - print_background?: boolean; - /** - * The scale factor to be applied when generating the PDF output (e.g., 1.0). - */ - scale?: number; - - constructor(init?: Partial) { - Object.assign(this, init); - } -} - -/** - * Internal interface representing how PDFOptions looks over the wire. - * All units are flattened to strings (e.g., "10in"). - * @internal - */ -export interface InternalPDFOptions extends BasePDFOptions { - margins?: string[]; - page_height?: string; - page_width?: string; -} - -export interface InternalBaseCreateRequest extends BaseCreateRequestWithoutVariableOptions { - pdf_options?: InternalPDFOptions; -} - -export interface InternalCreateHtmlCssImageRequest extends InternalBaseCreateRequest { - html: string; - css?: string; - google_fonts?: string; -} - -export interface InternalCreateHtmlCssImageRequestWithOptionalHtml extends InternalBaseCreateRequest { - html?: string; - css?: string; - google_fonts?: string; -} - -export interface InternalCreateUrlImageRequest extends InternalBaseCreateRequest { - url: string; - full_screen?: boolean; - block_consent_banners?: boolean; -} - -export interface InternalCreateUrlImageRequestWithOptionalUrl extends InternalBaseCreateRequest { - url?: string; - full_screen?: boolean; - block_consent_banners?: boolean; -} \ No newline at end of file diff --git a/src/types/request.ts b/src/types/request.ts index 0843573..91af0c3 100644 --- a/src/types/request.ts +++ b/src/types/request.ts @@ -1,5 +1,3 @@ -import {BaseCreateRequestWithoutVariableOptions, BasePDFOptions} from "./internals.js"; - export type ColorSchemeType = 'light' | 'dark'; export type PdfUnit = 'px' | 'in' | 'cm' | 'mm'; export type MediaType = 'print' | 'screen'; @@ -26,7 +24,17 @@ export interface PdfMargins { left: PdfValueInput; } -export class PDFOptions extends BasePDFOptions { +export class PDFOptions { + /** + * Whether background graphics should be printed in the PDF output. + */ + print_background?: boolean; + + /** + * The scale factor applied when generating the PDF output. + */ + scale?: number; + /** * Gets or sets the margins to be applied to the PDF output. * Specifies the top, right, bottom, and left margins. @@ -44,7 +52,6 @@ export class PDFOptions extends BasePDFOptions { page_width?: PdfValueInput; constructor(init?: Partial) { - super(); Object.assign(this, init); } @@ -52,14 +59,105 @@ export class PDFOptions extends BasePDFOptions { } -export abstract class BaseCreateImageRequest extends BaseCreateRequestWithoutVariableOptions { +export abstract class BaseCreateImageRequest { + /** + * A CSS selector to target a specific element on the page. + * The API will crop the image to the dimensions of this element. + */ + selector?: string; + + /** + * Adjusts the pixel ratio for the screenshot. + * The default is 2, which is equivalent to a 4K monitor. + */ + device_scale?: number; + + /** + * Set the height of Chrome's viewport. This will disable automatic cropping. + */ + viewport_height?: number; + + /** + * Set the width of Chrome's viewport. This will disable automatic cropping. + */ + viewport_width?: number; + + /** + * Sets a limit on time to wait until the screenshot is taken. + */ + max_wait_ms?: number; + + /** + * Adds extra time before taking the screenshot, such as when waiting for JavaScript to execute. + */ + ms_delay?: number; + + /** + * Wait until ScreenshotReady() is called from JavaScript before taking the screenshot. + */ + render_when_ready?: boolean; + + /** + * Ensure the image is only ever rendered and saved one time. + */ + max_render_once?: boolean; + + /** + * Disable Twemoji fallback rendering. + */ + disable_twemoji?: boolean; + + /** + * Render as if the user has selected light or dark mode. + */ + color_scheme?: ColorSchemeType; + + /** + * Set the browser timezone using an IANA timezone name. + */ + timezone?: string; + + /** + * Render as if the viewport is a mobile device. + */ + viewport_mobile?: boolean; + + /** + * Enable touch interactions within the viewport. + */ + viewport_touch?: boolean; + + /** + * Render the viewport in landscape orientation. + */ + viewport_landscape?: boolean; + + /** + * Set the rendering media type. + */ + media_type?: MediaType; + + /** + * Select an organization proxy for rendering. + */ + proxy_id?: string; + + /** + * Set the maximum width in jumbo mode. jumbo_max_height must also be defined. + */ + jumbo_max_width?: number; + + /** + * Set the maximum height in jumbo mode. jumbo_max_width must also be defined. + */ + jumbo_max_height?: number; + /** * Options for generating a PDF from the HTML/CSS or Url. */ pdf_options?: PDFOptions; protected constructor(init?: Partial) { - super(); Object.assign(this, init); } } @@ -100,6 +198,13 @@ export class CreateUrlImageRequest extends BaseCreateImageRequest { * This variable is expected to contain a valid URL string. */ url!: string; + + /** + * Custom CSS rules to inject into the target webpage before rendering. + * Use this to override existing styles or customize specific elements. + */ + css?: string; + /** * Indicates whether the screenshot should capture the entire webpage in full height. * @@ -140,4 +245,4 @@ export class CreateTemplatedImageRequest> { constructor(init?: Partial>) { Object.assign(this, init); } -} \ No newline at end of file +} diff --git a/tests/client.test.ts b/tests/client.test.ts index 69a24f1..ca187e2 100644 --- a/tests/client.test.ts +++ b/tests/client.test.ts @@ -1,8 +1,8 @@ -import { test, describe, before, after } from 'node:test'; +import { test, describe } from 'node:test'; import assert from 'node:assert'; import { HtmlCssToImageClient } from '../src/HtmlCssToImageClient.js'; -import {CreateHtmlCssImageRequest, CreateUrlImageRequest, PDFOptions} from '../src/types/request.js'; -import {CreateImageSuccessResponse} from "../src/types/response.js"; +import {CreateHtmlCssImageRequest, CreateTemplatedImageRequest, CreateUrlImageRequest, PDFOptions} from '../src/types/request.js'; +import type {CreateImageSuccessResponse} from "../src/types/response.js"; describe('HtmlCssToImageClient', () => { @@ -16,7 +16,7 @@ describe('HtmlCssToImageClient', () => { // Verify the mapping logic (internal serialization) assert.strictEqual(body.html, '

Test

'); - assert.deepStrictEqual(body.pdf_options.margins, ['10px', '20px', '10px', '20in']); + assert.deepStrictEqual(body.pdf_options.margins, ['10px', '20px', '5mm', '20in']); assert.strictEqual(body.google_fonts, 'Roboto|Open+Sans'); return { @@ -30,7 +30,12 @@ describe('HtmlCssToImageClient', () => { html: '

Test

', google_fonts: ['Roboto', 'Open Sans','Open Sans'], pdf_options: new PDFOptions({ - margins: { top: 10, bottom: 10, right: 20, left: {value: 20, unit: 'in'} } + margins: { + top: 10, + bottom: {value: 5, unit: 'mm'}, + right: 20, + left: {value: 20, unit: 'in'} + } }) }); const client = new HtmlCssToImageClient(apiId, apiKey, mockFetch as any); @@ -44,6 +49,60 @@ describe('HtmlCssToImageClient', () => { } }); + test('createImage strips the internal template discriminator', async () => { + const mockFetch = async (_url: string, options: any) => { + const body = JSON.parse(options.body); + assert.strictEqual(body.__type, undefined); + assert.strictEqual(body.template_id, 'template-id'); + assert.deepStrictEqual(body.template_values, {title: 'Hello'}); + + return { + ok: true, + json: async () => ({id: '123', url: 'https://hcti.io/v1/image/123'}) + }; + }; + + const client = new HtmlCssToImageClient(apiId, apiKey, mockFetch as any); + const result = await client.createImage(new CreateTemplatedImageRequest({ + template_id: 'template-id', + template_values: {title: 'Hello'} + })); + + assert.strictEqual(result.success, true); + }); + + test('createImage sends CSS with URL screenshot requests', async () => { + const mockFetch = async (_url: string, options: any) => { + const body = JSON.parse(options.body); + assert.strictEqual(body.url, 'https://example.com'); + assert.strictEqual(body.css, 'body { background: black; }'); + + return { + ok: true, + json: async () => ({id: '123', url: 'https://hcti.io/v1/image/123'}) + }; + }; + + const client = new HtmlCssToImageClient(apiId, apiKey, mockFetch as any); + const result = await client.createImage(new CreateUrlImageRequest({ + url: 'https://example.com', + css: 'body { background: black; }' + })); + + assert.strictEqual(result.success, true); + }); + + test('generateCreateAndRenderUrl includes CSS for URL screenshots', () => { + const client = new HtmlCssToImageClient(apiId, apiKey); + const url = client.generateCreateAndRenderUrl(new CreateUrlImageRequest({ + url: 'https://example.com', + css: 'body { background: black; }' + })); + + const parsedUrl = new URL(url); + assert.strictEqual(parsedUrl.searchParams.get('css'), 'body { background: black; }'); + }); + test('createImageBatch correctly maps and sends batch request', async () => { const mockFetch = (async (url: string, options: any) => { @@ -57,8 +116,6 @@ describe('HtmlCssToImageClient', () => { assert.strictEqual(body.variations.length, 2); assert.strictEqual(body.variations[0].html, '

V1

'); assert.strictEqual(body.variations[1].html, '

V2

'); - console.log(options.body); - return { ok: true, json: async () => ({ images: [{ id: '1', url: 'u1' }, { id: '2', url: 'u2' }] }) @@ -92,8 +149,6 @@ describe('HtmlCssToImageClient', () => { assert.strictEqual(body.variations[0].html, '

V1

'); assert.strictEqual(body.variations[1].html, undefined); assert.strictEqual(body.default_options.html, '

BASE

'); - console.log(options.body); - return { ok: true, json: async () => ({ images: [{ id: '1', url: 'u1' }, { id: '2', url: 'u2' }] }) @@ -178,4 +233,4 @@ describe('HtmlCssToImageClient', () => { assert.strictEqual(result.error, 'Internal Server Error'); } }); -}); \ No newline at end of file +});