diff --git a/src/puter-js/src/modules/ai/ai.test.js b/src/puter-js/src/modules/ai/ai.test.js index c9ef15cb1..5e772a912 100644 --- a/src/puter-js/src/modules/ai/ai.test.js +++ b/src/puter-js/src/modules/ai/ai.test.js @@ -289,6 +289,26 @@ describe('ai.img2txt driver payloads', () => { it('img2txt rejects without a source', async () => { await expect(ai.img2txt({})).rejects.toMatchObject({ code: 'source_required' }); }); + + it('img2txt sizes data URIs by decoded bytes, not string length', async () => { + FakeXHR.respondWith = () => ({ success: true, result: { text: 'ok' } }); + const prefix = 'data:image/png;base64,'; + // String length just over 10MB; base64 still decodes under the 10MB limit (~7.5MB). + const targetLen = 10 * 1024 * 1024 + 4; + let base64Len = targetLen - prefix.length; + base64Len += (4 - (base64Len % 4)) % 4; + const uri = prefix + 'A'.repeat(base64Len); + expect(uri.length).toBeGreaterThan(10 * 1024 * 1024); + await expect(ai.img2txt(uri)).resolves.toBe('ok'); + }); + + it('img2txt rejects when decoded data-URI bytes exceed 10MB', async () => { + const prefix = 'data:image/png;base64,'; + let base64Len = Math.ceil(((10 * 1024 * 1024) + 1) * 4 / 3); + base64Len += (4 - (base64Len % 4)) % 4; + const uri = prefix + 'A'.repeat(base64Len); + await expect(ai.img2txt(uri)).rejects.toMatchObject({ code: 'input_too_large' }); + }); }); describe('ai.txt2speech driver payloads', () => { diff --git a/src/puter-js/src/modules/ai/ocr.js b/src/puter-js/src/modules/ai/ocr.js index 457e3bffd..3d4831ac6 100644 --- a/src/puter-js/src/modules/ai/ocr.js +++ b/src/puter-js/src/modules/ai/ocr.js @@ -1,5 +1,5 @@ import * as utils from '../../lib/utils.js'; -import { isBlobLike, isPlainObject } from './lib/args.js'; +import { dataUriByteLength, isBlobLike, isPlainObject } from './lib/args.js'; /** @typedef {import('../../../types/modules/ai').Img2TxtOptions} Img2TxtOptions */ @@ -124,7 +124,7 @@ export async function img2txt (sourceOrOptions, optionsOrTestMode, testModeOrOpt if ( typeof options.source === 'string' && options.source.startsWith('data:') && - options.source.length > MAX_INPUT_SIZE ) { + dataUriByteLength(options.source) > MAX_INPUT_SIZE ) { throw { message: `Input size cannot be larger than ${ MAX_INPUT_SIZE}`, code: 'input_too_large' }; }