image2ppt 0.5.3 → 0.7.0
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/README.md +246 -13
- package/dist/client.d.ts +80 -17
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +433 -55
- package/dist/client.js.map +1 -1
- package/dist/compress.d.ts +3 -3
- package/dist/compress.d.ts.map +1 -1
- package/dist/compress.js +7 -5
- package/dist/compress.js.map +1 -1
- package/dist/errors.d.ts +121 -8
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +154 -18
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/limits.d.ts +23 -5
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +51 -10
- package/dist/limits.js.map +1 -1
- package/dist/pages.d.ts +47 -0
- package/dist/pages.d.ts.map +1 -0
- package/dist/pages.js +90 -0
- package/dist/pages.js.map +1 -0
- package/dist/types.d.ts +163 -2
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +125 -2
- package/dist/types.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/webhook.d.ts +75 -0
- package/dist/webhook.d.ts.map +1 -0
- package/dist/webhook.js +182 -0
- package/dist/webhook.js.map +1 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -45,6 +45,40 @@ const done = await client.wait(job.jobId, { pollIntervalMs: 5000, timeoutMs: 1_8
|
|
|
45
45
|
await client.download(done.jobId, "out.pptx");
|
|
46
46
|
```
|
|
47
47
|
|
|
48
|
+
Show progress to your own users — `onProgress` is called after every status poll
|
|
49
|
+
(the last one included) with the fresh `Job` and the milliseconds elapsed since
|
|
50
|
+
`wait` started. `convert` and `convertAll` take it too.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
const done = await client.wait(job.jobId, {
|
|
54
|
+
onProgress: (job) => {
|
|
55
|
+
if (job.queueAhead != null) {
|
|
56
|
+
console.log(`queued, ${job.queueAhead} job(s) ahead`);
|
|
57
|
+
} else if (job.estimatedRemainingSeconds != null) {
|
|
58
|
+
console.log(`${job.progress}% — about ${job.estimatedRemainingSeconds}s left`);
|
|
59
|
+
} else {
|
|
60
|
+
console.log(`${job.progress ?? 0}% — estimating…`);
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
While a job is running, each snapshot carries:
|
|
67
|
+
|
|
68
|
+
- `progress` — the whole job, 0–100. It moves within a page, so a one-page job
|
|
69
|
+
climbs steadily instead of jumping from 0 to 100.
|
|
70
|
+
- `pageProgress` — one `{ pageNumber, progress, stepLabel }` per page.
|
|
71
|
+
`stepLabel` is a short line about what that page is doing right now, in the
|
|
72
|
+
language of `acceptLanguage` (English by default), or `null`. Show it as-is and
|
|
73
|
+
never branch on it: its wording changes as the service changes.
|
|
74
|
+
- `estimatedRemainingSeconds` — the service's estimate of the time left. It can be
|
|
75
|
+
`null` while the job runs, when no estimate is available yet.
|
|
76
|
+
- `queueAhead` — jobs ahead of this one while it waits to start.
|
|
77
|
+
|
|
78
|
+
`pageProgress`, `estimatedRemainingSeconds` and `queueAhead` are `null` once
|
|
79
|
+
the job has ended. A returned promise is
|
|
80
|
+
awaited before the next poll, and anything the callback throws ends the wait.
|
|
81
|
+
|
|
48
82
|
Cancel a job you no longer need:
|
|
49
83
|
|
|
50
84
|
```ts
|
|
@@ -72,10 +106,195 @@ subclass of `JobFailedError`). `JobAlreadyFinishedError` covers both a job that
|
|
|
72
106
|
finished on its own and one already past the point where cancelling could change the
|
|
73
107
|
outcome.
|
|
74
108
|
|
|
109
|
+
## Task names — `name`
|
|
110
|
+
|
|
111
|
+
```typescript
|
|
112
|
+
const uploaded = await client.submit(["report.pdf"], { name: "Quarterly report" });
|
|
113
|
+
const linked = await client.submitUrls(["https://example.com/report.pdf"], { name: "Quarterly report" });
|
|
114
|
+
const converted = await client.convert(["report.pdf"], "local.pptx", { name: "Quarterly report" });
|
|
115
|
+
console.log(converted.name); // resolved name, possibly "Quarterly report (2)"
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`name` is optional on submissions and conversion calls, including `submitAll()` / `convertAll()`.
|
|
119
|
+
Batch calls reuse it for each job; the service resolves each collision. It is the
|
|
120
|
+
task name and the service's download filename, without `.pptx`. After invisible
|
|
121
|
+
characters are removed and whitespace collapsed, it must be non-empty, at most
|
|
122
|
+
60 Unicode characters and 180 UTF-8 bytes. Control characters and `/ \ : * ? " < > |`
|
|
123
|
+
are forbidden, as are leading/trailing dots and Windows reserved names. An invalid
|
|
124
|
+
name raises `InvalidNameError` with code `INVALID_NAME`. The service validates
|
|
125
|
+
and normalizes name contents; the SDK sends the supplied string unchanged.
|
|
126
|
+
|
|
127
|
+
Names already used by undeleted jobs in the same account get ` (2)`, ` (3)`, etc.
|
|
128
|
+
The resolved name is on `Job.name` in create responses, `getJob()` and job lists;
|
|
129
|
+
completion webhook payloads also include it. A rename on the website appears in
|
|
130
|
+
later snapshots. Older servers that omit it give `null`. Without `name`, one
|
|
131
|
+
input uses its cleaned filename without extension; multiple inputs or an unusable
|
|
132
|
+
filename use `Slides YYYY-MM-DD HHMMSS UTC` at creation time.
|
|
133
|
+
|
|
134
|
+
`name` participates in the `Idempotency-Key` fingerprint, compared after the
|
|
135
|
+
service's cleanup. Reusing a key with a name that cleans up differently raises
|
|
136
|
+
`IdempotencyKeyMismatchError`, just like changing other submission options; a name
|
|
137
|
+
that differs only in what cleanup removes (extra spaces, invisible characters)
|
|
138
|
+
replays the original job.
|
|
139
|
+
|
|
140
|
+
`download()` writes to the caller's path and never uses the server name, so a task
|
|
141
|
+
name cannot cause local filename collisions or overwrite other files. Existing
|
|
142
|
+
files at your explicitly chosen path can still be replaced. `convertAll()` keeps
|
|
143
|
+
`part-01.pptx`, `part-02.pptx`, ... naming regardless of task names.
|
|
144
|
+
|
|
145
|
+
## Convert only some pages — `pages`
|
|
146
|
+
|
|
147
|
+
```ts
|
|
148
|
+
const job = await client.submit(["report.pdf"], { pages: "1-3, 7" });
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
Single pages and `start-end` ranges, separated by commas, counting from 1. Page numbers
|
|
152
|
+
run across the **whole submission in order** — an image is one page, a PDF is its page
|
|
153
|
+
count — so with a single PDF they are simply the PDF's own page numbers. Overlaps are
|
|
154
|
+
merged, and the deck keeps the pages in ascending order.
|
|
155
|
+
|
|
156
|
+
Only the selected pages are charged, and only they count towards the 50-page limit: a
|
|
157
|
+
200-page PDF with `pages: "1-50"` is fine. The spelling is checked locally before
|
|
158
|
+
anything is uploaded (`InvalidPagesError`); a page past the end comes back
|
|
159
|
+
`PagesOutOfRangeError`.
|
|
160
|
+
|
|
161
|
+
With a selection, `pageResults[i].pageNumber` is the page's place **in the deck**, not
|
|
162
|
+
the number you selected: `pages: "3,7"` gives entries 1 and 2.
|
|
163
|
+
|
|
164
|
+
## Submit by URL — `submitUrls`
|
|
165
|
+
|
|
166
|
+
Instead of uploading, hand the service `https` links and it downloads them itself:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
const job = await client.submitUrls(
|
|
170
|
+
["https://example.com/slides/1.png", "https://example.com/report.pdf"],
|
|
171
|
+
{ pages: "1-4" },
|
|
172
|
+
);
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Up to 50 links, converted in order into one deck, held to the same limits as uploads
|
|
176
|
+
(35MB per file, 90MB together, 50 pages). File types are judged by content. Links must
|
|
177
|
+
be `https` and resolve to public addresses; a link that is refused throws
|
|
178
|
+
`InvalidUrlError`, a download that fails throws `UrlFetchFailedError` (worth retrying
|
|
179
|
+
later — the other side may have been down), and for both `e.index` says which link,
|
|
180
|
+
counting from 0.
|
|
181
|
+
|
|
182
|
+
Downloading takes time, so `submitUrls` waits up to 180 seconds for an answer whatever
|
|
183
|
+
the client's `timeoutMs`. One submission by URL per account can be in flight at a time;
|
|
184
|
+
another gets `RateLimitedError`.
|
|
185
|
+
|
|
186
|
+
## Get told when a job ends — `callbackUrl` and `verifyWebhook`
|
|
187
|
+
|
|
188
|
+
Pass `callbackUrl` to any submit call and the service `POST`s to it when the job ends,
|
|
189
|
+
instead of you polling:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
const job = await client.submit(["slide1.png"], {
|
|
193
|
+
callbackUrl: "https://example.com/hooks/image2ppt",
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Every delivery is signed ([Standard Webhooks](https://www.standardwebhooks.com)). Check
|
|
198
|
+
it with the **raw** request body and the signing secret from the Developer / API page:
|
|
199
|
+
|
|
200
|
+
```ts
|
|
201
|
+
import express from "express";
|
|
202
|
+
import { verifyWebhook, WebhookVerificationError } from "image2ppt";
|
|
203
|
+
|
|
204
|
+
// express.raw(), not express.json(): the signature is over the exact bytes received.
|
|
205
|
+
app.post("/hooks/image2ppt", express.raw({ type: "application/json" }), (req, res) => {
|
|
206
|
+
let event;
|
|
207
|
+
try {
|
|
208
|
+
event = verifyWebhook(req.body, req.headers, process.env.IMAGE2PPT_WEBHOOK_SECRET!);
|
|
209
|
+
} catch (e) {
|
|
210
|
+
if (e instanceof WebhookVerificationError) return res.sendStatus(400);
|
|
211
|
+
throw e;
|
|
212
|
+
}
|
|
213
|
+
if (event.type === "job.completed") {
|
|
214
|
+
const job = event.job; // same shape as getJob()
|
|
215
|
+
// ...
|
|
216
|
+
} else if (event.type === "job.failed") {
|
|
217
|
+
// ...
|
|
218
|
+
}
|
|
219
|
+
// Ignore types you don't recognise — more may be added. Answer 2xx either way.
|
|
220
|
+
res.sendStatus(204);
|
|
221
|
+
});
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
- **Pass the body exactly as received** — a `Buffer`, `Uint8Array` or string. Parsing
|
|
225
|
+
the JSON and serialising it again changes the bytes, and the signature will not match.
|
|
226
|
+
`headers` can be Node's `req.headers` or a fetch `Headers`.
|
|
227
|
+
- **Answer 2xx within 10 seconds.** Anything else counts as a failure, and the service
|
|
228
|
+
retries: 6 attempts over about 8.5 hours (right away, then +1 min, +5 min, +30 min,
|
|
229
|
+
+2 h, +6 h). A retry of one delivery keeps its `event.id`, so use that to skip
|
|
230
|
+
duplicates.
|
|
231
|
+
- **Timestamps more than 5 minutes off are refused**, either way, as protection against
|
|
232
|
+
replays (`toleranceSeconds` to change it).
|
|
233
|
+
- A malformed secret, or a body your framework already parsed into an object, throws a
|
|
234
|
+
`TypeError` — those are mistakes in your receiver, so they are kept apart from a bad
|
|
235
|
+
delivery.
|
|
236
|
+
- Rotating the secret is safe: for 24 hours the service signs with both the old and the
|
|
237
|
+
new one, and either verifies.
|
|
238
|
+
|
|
239
|
+
`getJob()` reports how delivery is going in `job.callback` — `status` is `pending`,
|
|
240
|
+
`delivered` or `failed` (all 6 attempts failed), with `attempts`, `lastResponseStatus`
|
|
241
|
+
and the times of the last and next attempt.
|
|
242
|
+
|
|
243
|
+
## Resubmitting safely — `idempotencyKey`
|
|
244
|
+
|
|
245
|
+
Every submission carries an `Idempotency-Key`. If the same key comes back with the same
|
|
246
|
+
request within 24 hours, the service returns the job it already created —
|
|
247
|
+
`job.replayed` is `true` — instead of creating and charging for a second one.
|
|
248
|
+
|
|
249
|
+
That is what lets this client **resend a submission whose outcome is unknown** — a
|
|
250
|
+
dropped connection, a timeout, a 5xx — without risking a double charge. It does so by
|
|
251
|
+
itself, always under the same key (see *How it works*). By default the key is a random
|
|
252
|
+
UUID per call. Pass your own to extend the protection across calls, processes or
|
|
253
|
+
restarts:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
const job = await client.submit(paths, { idempotencyKey: `order-${order.id}` });
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
If a submission still fails, the key it used is on the error:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
import { APIConnectionError, ServerError } from "image2ppt";
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
job = await client.submit(paths);
|
|
266
|
+
} catch (e) {
|
|
267
|
+
if (!(e instanceof APIConnectionError || e instanceof ServerError)) throw e;
|
|
268
|
+
// Later, or from another process: safe for 24 hours, because the same files,
|
|
269
|
+
// options and key can never create a second job.
|
|
270
|
+
job = await client.submit(paths, { idempotencyKey: e.idempotencyKey });
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
"The same request" means the same file bytes in the same order (or the same `urls`), and
|
|
275
|
+
the same `locale`, `aspectRatio`, `pages` and `callbackUrl`. Reusing a key for a
|
|
276
|
+
different request throws `IdempotencyKeyMismatchError`. Keys are 1–255 printable ASCII
|
|
277
|
+
characters, shared by all API keys of the account.
|
|
278
|
+
|
|
279
|
+
## List your jobs — `listJobs` / `iterJobs`
|
|
280
|
+
|
|
281
|
+
```ts
|
|
282
|
+
for await (const job of client.iterJobs({ createdFrom: "2026-10-01", createdTo: "2026-10-31" })) {
|
|
283
|
+
console.log(job.jobId, job.status, job.creditsUsed, job.creditsRefunded);
|
|
284
|
+
}
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Jobs submitted through the API, newest first; jobs deleted on the website are not
|
|
288
|
+
listed. Dates are `YYYY-MM-DD` in UTC and both inclusive. `iterJobs` follows the pages
|
|
289
|
+
for you; `listJobs` returns one page (`page.data`, `page.nextCursor`) if you want to page
|
|
290
|
+
yourself — pass `nextCursor` back as `cursor`, `limit` is 1–100 (default 20). Each job
|
|
291
|
+
has `getJob()`'s fields except `pageResults`. For reconciliation, add up `creditsUsed`
|
|
292
|
+
(charged) and `creditsRefunded`.
|
|
293
|
+
|
|
75
294
|
## Which pages made it
|
|
76
295
|
|
|
77
296
|
Once a job is terminal, `job.pageResults` says what happened to each page, in page
|
|
78
|
-
order, one entry per page:
|
|
297
|
+
order, one entry per page of the deck (with `pages`, per *selected* page):
|
|
79
298
|
|
|
80
299
|
```ts
|
|
81
300
|
for (const page of done.pageResults ?? []) {
|
|
@@ -139,12 +358,12 @@ console.log(email, "credits:", credits);
|
|
|
139
358
|
- **Async.** `submit` resolves with a job id immediately; conversion runs in the background. A single page typically takes ~2 minutes; 90% of jobs finish within 3.
|
|
140
359
|
- **One job = one PPTX.** All files in a submission are merged into a single deck, in upload order.
|
|
141
360
|
- **Billed per page.** 1 page = 1 credit, reserved at submit and settled on completion. If some pages fail but others succeed, the job still completes with the good pages and the failed pages' credits are refunded (`creditsRefunded`).
|
|
142
|
-
- **Limits.** Each file ≤ 35MB; **the files in one request ≤
|
|
143
|
-
- **The check is never stricter than the documented limit.**
|
|
361
|
+
- **Limits.** Each file ≤ 35MB; **the files in one request ≤ 90MB in total**; ≤ 50 pages per job (images count as 1, PDFs as their page count — or, with `pages`, the pages selected). All three are checked locally before upload — note the per-file limit is the *stricter* one, so a 40MB PDF is refused even though it fits a request. **The sizes counted are the ones that actually go on the wire**: image preparation happens first, then the final payload sizes drive pre-flight and batching. PDFs keep their on-disk size.
|
|
362
|
+
- **The check is never stricter than the documented limit.** 90MB of file content is meant to be usable, so a submission sitting exactly on it goes through. Auto-batching is the one place that is deliberately conservative — it fills a batch only to 40MB, because starting one more batch costs nothing while refusing something the server would have accepted does not.
|
|
144
363
|
- **Only the formats the API accepts.** `png`, `jpg`/`jpeg`, `webp`, `gif`, `pdf`. Anything else throws `InvalidFileError` locally — the batch calls check every file before submitting the first one, so an unsupported file at the end of the pile cannot leave you paying for the batches ahead of it.
|
|
145
|
-
- **The local page check is a lower bound.** The client does not parse PDFs, so it counts each one as *at least* 1 page. That is enough to refuse combinations that can never work (50 images plus any PDF is already 51 pages), but a submission that passes locally can still come back `TOO_MANY_SLIDES` — a 30-page PDF counts as 1 here and 30 on the server.
|
|
364
|
+
- **The local page check is a lower bound.** The client does not parse PDFs, so it counts each one as *at least* 1 page. That is enough to refuse combinations that can never work (50 images plus any PDF is already 51 pages), but a submission that passes locally can still come back `TOO_MANY_SLIDES` — a 30-page PDF counts as 1 here and 30 on the server. With `pages`, the check counts the pages selected instead, and a PDF longer than 50 pages is fine.
|
|
146
365
|
- **Going over the request limit is not a polite error.** Past that the connection is cut before the API can answer, so the caller sees a write timeout or a broken pipe instead of a status code. The client therefore checks locally *before* uploading and throws `InvalidFileError` (`code: "PAYLOAD_TOO_LARGE"`) without sending a byte.
|
|
147
|
-
- **A
|
|
366
|
+
- **A submission whose outcome is unknown is resent, under the same `Idempotency-Key`.** A network error only tells you the exchange broke — not whether the request body arrived; the job may exist with credits already reserved and only the response lost. Because every attempt carries the same key, a resend of that case gets the existing job back rather than a second charge. So `submit()` resends after a dropped connection, an idle timeout or a 5xx (up to 2 more attempts, 1s and 2s apart), and waits out `IdempotencyKeyInProgressError` — an earlier attempt the service is still working on — for up to 10 attempts or 3 minutes. Any other error is thrown straight away. Whatever finally escapes carries `e.idempotencyKey`; see *Resubmitting safely*.
|
|
148
367
|
- **Downloads are all-or-nothing.** `download()` writes to a temporary file next to the destination and renames it into place at the end, so a dropped connection cannot leave a truncated `.pptx` behind — or destroy a good deck already sitting at that path.
|
|
149
368
|
- **Every request identifies the client** with a `User-Agent` of `image2ppt-node/<version>`. The service uses this to tell SDK versions apart — it is not part of authentication and never changes a request's outcome.
|
|
150
369
|
- **A deprecated SDK version logs one warning.** If this version is below the lowest the service still supports, the response carries a `Deprecation` header and the client warns once (`console.warn`). Pass `warnOnDeprecated: false` to `Image2PPTClient` to silence it.
|
|
@@ -152,7 +371,7 @@ console.log(email, "credits:", credits);
|
|
|
152
371
|
- **The 60-second request timeout is idle time, not total time.** `timeoutMs` (default 60000) is how long one request may go with **no data moving in either direction** — it is not a cap on how long a request may take. A 40MB upload or a large PPTX download that keeps making progress runs as long as it needs to; only a transfer that actually stalls is given up on, as `APITimeoutError`. A request that never gets a response at all is covered by the same clock. This matches the Python client's read timeout, so the two SDKs behave the same way on a slow link.
|
|
153
372
|
- **Every failure of the request is an `Image2PPTError`.** A refused connection, a reset mid-download, a 2xx that comes back as a proxy's HTML login page, a job body missing its own id — all of them arrive as an SDK error with the original kept on `.cause`, never as a raw `TypeError: fetch failed` or `SyntaxError`. **Your own filesystem is the exception, deliberately:** if `download` cannot write where you asked it to, you get the operating system's error — `ENOSPC`, `EACCES`, `ENOENT` — because that names the thing you have to go and fix, and no error of ours would say it better. The Python client draws the same line.
|
|
154
373
|
|
|
155
|
-
> Both the Node and Python SDKs pre-compress images that need processing before upload. PNG/JPEG files already at most
|
|
374
|
+
> Both the Node and Python SDKs pre-compress images that need processing before upload. PNG/JPEG files already at most 2MiB with a longest edge at most 2000px upload byte-for-byte unchanged. Other PNG/JPEG files, and all WebP/GIF files, may be resized, flattened onto white, and sent as full-colour (4:4:4) JPEG; PDFs are never compressed or decoded and are streamed unchanged.
|
|
156
375
|
|
|
157
376
|
## More files than one request can hold
|
|
158
377
|
|
|
@@ -163,7 +382,9 @@ const files = await client.convertAll(imagePaths, "decks/");
|
|
|
163
382
|
console.log(files); // ['decks/part-01.pptx', 'decks/part-02.pptx']
|
|
164
383
|
```
|
|
165
384
|
|
|
166
|
-
Batches hold at most
|
|
385
|
+
Batches hold at most 80MB of file content and at most 50 images; every PDF goes in a batch of its own, because the client does not parse PDFs and only the server knows their page count. `submitAll()` does the same splitting and hands back the jobs if you want to drive polling yourself. To see the plan without uploading anything, use `planBatches()`.
|
|
386
|
+
|
|
387
|
+
Both take `callbackUrl` — each batch's job calls it. They do not take `pages` or `idempotencyKey` (passing either throws a `TypeError`): page numbers run across one submission and a key names one job, so neither can span several batches. Each batch gets its own key, kept across that batch's retries.
|
|
167
388
|
|
|
168
389
|
**Rate limits are waited out, not thrown.** A pile big enough to need batching will hit the account's per-minute page quota (and its cap on concurrently active jobs). Both arrive as a `429` with a `Retry-After`; both are handled the same way — sleep that long, retry the same batch. Retrying a 429 is free: the server is saying it did *not* take the submission, so nothing was created and nothing was charged. Total waiting is capped by `rateLimitMaxWaitMs` (default 30 min) — and **only waiting counts against it**, not the time the uploads themselves take, so a slow link cannot quietly turn the cap into "do not wait at all". A single batch is also retried at most 10 times, whatever the budget says: every retry re-uploads the whole batch, and a service still refusing after ten tries will not be talked round by more of them.
|
|
169
390
|
|
|
@@ -187,14 +408,15 @@ try {
|
|
|
187
408
|
|
|
188
409
|
Per account (all keys share the budget): ≤ 10 concurrent jobs, ≤ 60 pages/minute submitted. Over the limit returns `429` with a `Retry-After` hint. **Only submissions are rate limited — polling job status is not.**
|
|
189
410
|
|
|
190
|
-
`submitAll()` / `convertAll()` wait these out for you: a pile big enough to need batching is a pile big enough to hit the quota, so a 429 mid-pile is the normal path, not an error. `submit()` and `convert()` do not
|
|
411
|
+
`submitAll()` / `convertAll()` wait these out for you: a pile big enough to need batching is a pile big enough to hit the quota, so a 429 mid-pile is the normal path, not an error. `submit()`, `submitUrls()` and `convert()` do not, so catch `RateLimitedError` and honor `retryAfter` (seconds) yourself — resending with the same key keeps it safe:
|
|
191
412
|
|
|
192
413
|
```ts
|
|
193
414
|
import { RateLimitedError } from "image2ppt";
|
|
194
415
|
|
|
416
|
+
const idempotencyKey = "order-42";
|
|
195
417
|
for (;;) {
|
|
196
418
|
try {
|
|
197
|
-
job = await client.submit(paths);
|
|
419
|
+
job = await client.submit(paths, { idempotencyKey });
|
|
198
420
|
break;
|
|
199
421
|
} catch (e) {
|
|
200
422
|
if (e instanceof RateLimitedError) {
|
|
@@ -206,11 +428,11 @@ for (;;) {
|
|
|
206
428
|
|
|
207
429
|
## Errors
|
|
208
430
|
|
|
209
|
-
Every error this client raises about a *request* subclasses `Image2PPTError` and carries `statusCode`, `code`, and `message`. Branch on `code`, not `message`. The one thing that reaches you unwrapped is a failure of your own disk while `download` is writing — see the note above.
|
|
431
|
+
Every error this client raises about a *request* subclasses `Image2PPTError` and carries `statusCode`, `code`, and `message` — plus `index` (which of `urls` it is about, else `undefined`) and, out of a submit call, `idempotencyKey`. Branch on `code`, not `message`. The one thing that reaches you unwrapped is a failure of your own disk while `download` is writing — see the note above.
|
|
210
432
|
|
|
211
|
-
Every `Image2PPTError` also carries **`isTransient`** — whether **repeating this exact read** later could plausibly succeed. (A raw filesystem error from `download` has no such field; that is the same exception as above, and it is never transient — free the space or fix the permission first.) It is what `wait()` uses to decide whether a failed status poll should be backed off and retried or should end the wait: `true` for a 5xx, a dropped connection and a stalled request, `false` for a bad key, a job that does not exist, or a response this client cannot parse.
|
|
433
|
+
Every `Image2PPTError` also carries **`isTransient`** — whether **repeating this exact read** later could plausibly succeed. (A raw filesystem error from `download` has no such field; that is the same exception as above, and it is never transient — free the space or fix the permission first.) It is what `wait()` uses to decide whether a failed status poll should be backed off and retried or should end the wait: `true` for a 5xx, a rate limit, `IdempotencyKeyInProgressError`, `UrlFetchFailedError`, a dropped connection and a stalled request, `false` for a bad key, a job that does not exist, or a response this client cannot parse.
|
|
212
434
|
|
|
213
|
-
**
|
|
435
|
+
**Resubmitting is only safe with the same key.** A lost response cannot be told apart from a submission the server accepted, so resending under a *new* key can create the same job twice and **charge you twice**. Resend with `e.idempotencyKey`, as `submit()` itself does.
|
|
214
436
|
|
|
215
437
|
| Class | HTTP | code |
|
|
216
438
|
|---|---|---|
|
|
@@ -220,7 +442,17 @@ Every `Image2PPTError` also carries **`isTransient`** — whether **repeating th
|
|
|
220
442
|
| `MalformedUploadError` | 400 | `MALFORMED_UPLOAD` — the body was not valid `multipart/form-data`; **resending identical bytes will not help** |
|
|
221
443
|
| `NoFilesError` | 400 | `NO_FILES` — no files reached the server |
|
|
222
444
|
| `InvalidAspectRatioError` | 400 | `INVALID_ASPECT_RATIO` — use `auto`, `16:9`, or `4:3` |
|
|
223
|
-
| `TooManySlidesError` | 400 | `TOO_MANY_SLIDES` |
|
|
445
|
+
| `TooManySlidesError` | 400 | `TOO_MANY_SLIDES` — also for more than 50 pages selected |
|
|
446
|
+
| `InvalidPagesError` | 400 | `INVALID_PAGES` — `pages` is misspelled (also checked locally) |
|
|
447
|
+
| `PagesOutOfRangeError` | 400 | `PAGES_OUT_OF_RANGE` — page 0, or past the end; subclasses `InvalidPagesError` |
|
|
448
|
+
| `InvalidCallbackUrlError` | 400 | `INVALID_CALLBACK_URL` — not `https`, has credentials, over 2048 characters, or not a public address |
|
|
449
|
+
| `InvalidIdempotencyKeyError` | 400 | `INVALID_IDEMPOTENCY_KEY` — not 1–255 printable ASCII characters (also checked locally) |
|
|
450
|
+
| `IdempotencyKeyMismatchError` | 422 | `IDEMPOTENCY_KEY_MISMATCH` — the key was used for a different request |
|
|
451
|
+
| `IdempotencyKeyInProgressError` | 409 | `IDEMPOTENCY_KEY_IN_PROGRESS` (has `retryAfter`) — an earlier request with this key is still being processed |
|
|
452
|
+
| `InvalidUrlError` | 400 | `INVALID_URL` — a link was refused before downloading (`e.index` says which) |
|
|
453
|
+
| `UrlFetchFailedError` | 400 | `URL_FETCH_FAILED` — downloading a link failed; may be temporary (`e.index` says which) |
|
|
454
|
+
| `InvalidNameError` | 400 | `INVALID_NAME` — invalid task name |
|
|
455
|
+
| `InvalidParameterError` | 400 | `INVALID_PARAMETER`, `INVALID_JSON` — e.g. more than 50 `urls`, or a bad `listJobs` argument |
|
|
224
456
|
| `PageRateExceededError` | 400 | `PAGE_RATE_EXCEEDED` — this one submission has more pages than a minute's quota, so waiting will not help; split it |
|
|
225
457
|
| `InsufficientCreditsError` | 402 | `INSUFFICIENT_CREDITS` |
|
|
226
458
|
| `RateLimitedError` | 429 | `RATE_LIMITED` (has `retryAfter`) |
|
|
@@ -235,6 +467,7 @@ Every `Image2PPTError` also carries **`isTransient`** — whether **repeating th
|
|
|
235
467
|
| `APITimeoutError` | — | `REQUEST_TIMEOUT` — one request went `timeoutMs` with **no data moving**; subclasses `APIConnectionError` |
|
|
236
468
|
| `MalformedResponseError` | — | — (the server answered with something this client cannot read: a 2xx that is not JSON, or a body missing a field the contract guarantees) |
|
|
237
469
|
| `Image2PPTTimeoutError` | — | — (`wait()` exceeded its own `timeoutMs`; job may still be running — **not** a transport failure, and not the same as `APITimeoutError`) |
|
|
470
|
+
| `WebhookVerificationError` | — | — (`verifyWebhook` refused a delivery: missing header, stale timestamp, or no matching signature) |
|
|
238
471
|
|
|
239
472
|
> **5xx now lands on `ServerError` rather than the base class.** It still subclasses `Image2PPTError`, so `catch (e) { if (e instanceof Image2PPTError) }` is unaffected — only code matching on the exact class or on `e.name` needs updating.
|
|
240
473
|
|
package/dist/client.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** The image2ppt API client. */
|
|
2
|
-
import type { Account, CancellationResult, ClientOptions, ConvertOptions, SubmitOptions, WaitOptions } from "./types.js";
|
|
3
|
-
import { Job } from "./types.js";
|
|
2
|
+
import type { Account, CancellationResult, ClientOptions, ConvertAllOptions, ConvertOptions, ListJobsOptions, SubmitAllOptions, SubmitOptions, WaitOptions } from "./types.js";
|
|
3
|
+
import { Job, JobList } from "./types.js";
|
|
4
4
|
export declare const DEFAULT_BASE_URL = "https://image2ppt.com";
|
|
5
5
|
/**
|
|
6
6
|
* Client for the image2ppt API. Server-side only — keep your key off the browser.
|
|
@@ -28,30 +28,67 @@ export declare class Image2PPTClient {
|
|
|
28
28
|
* Submit a batch of files and create a conversion job.
|
|
29
29
|
*
|
|
30
30
|
* Checked locally before anything is uploaded: the files must add up to at most
|
|
31
|
-
*
|
|
31
|
+
* 90MB and at most 50 pages (the pages *selected*, when `pages` is given), and
|
|
32
|
+
* `pages` must be spelled correctly. Over a limit this throws without opening a
|
|
32
33
|
* connection — going over the size cap on the wire does not come back as a clean
|
|
33
34
|
* error, it comes back as a dead connection.
|
|
34
35
|
*
|
|
35
|
-
* **
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
36
|
+
* **Every submission carries an `Idempotency-Key`**, and that is what makes a
|
|
37
|
+
* failed one safe to resend: if an earlier attempt did create the job, the service
|
|
38
|
+
* hands that job back (`job.replayed` is true) instead of creating and charging for
|
|
39
|
+
* a second one. So this call resends by itself, always with the same key, when the
|
|
40
|
+
* outcome is unknown — a dropped connection, an idle timeout, or a 5xx (up to 2
|
|
41
|
+
* more attempts, 1s and 2s apart) — and waits out `IDEMPOTENCY_KEY_IN_PROGRESS`, an
|
|
42
|
+
* earlier attempt the service is still working on (up to 10 attempts / 3 minutes).
|
|
43
|
+
* A 429 is not retried here; `submitAll` waits those out.
|
|
44
|
+
*
|
|
45
|
+
* If it still fails, the key is on the error as `err.idempotencyKey`. Calling
|
|
46
|
+
* `submit` again with that key and the same files and options is safe for 24
|
|
47
|
+
* hours; calling it without the key may create a second job.
|
|
41
48
|
*
|
|
42
49
|
* @param paths Local file paths (one or more). png/jpeg/webp/gif/pdf, each ≤ 35MB,
|
|
43
|
-
* and ≤
|
|
50
|
+
* and ≤ 90MB of file content per request. An image is 1 page, a PDF is its page
|
|
44
51
|
* count; the total must be ≤ 50 pages. For more files than one request can hold,
|
|
45
52
|
* use `submitAll` / `convertAll`.
|
|
46
|
-
* @returns A `Job` with status `pending
|
|
53
|
+
* @returns A `Job` with status `pending` (for a replay: the job's current status),
|
|
54
|
+
* plus `slideCount` and `creditsReserved`.
|
|
47
55
|
* @throws InvalidFileError Including the local `PAYLOAD_TOO_LARGE` pre-flight failure.
|
|
56
|
+
* @throws InvalidPagesError / PagesOutOfRangeError / TooManySlidesError A selection
|
|
57
|
+
* that is certain to be refused, before anything is sent.
|
|
58
|
+
* @throws IdempotencyKeyMismatchError The key was used for a different request.
|
|
59
|
+
* @throws APIConnectionError / ServerError / IdempotencyKeyInProgressError Once the
|
|
60
|
+
* retries above run out.
|
|
48
61
|
*/
|
|
49
62
|
submit(paths: string[], options?: SubmitOptions): Promise<Job>;
|
|
63
|
+
/**
|
|
64
|
+
* Create a conversion job from files the service downloads itself.
|
|
65
|
+
*
|
|
66
|
+
* Instead of uploading, hand over `https` links; the service fetches them in order
|
|
67
|
+
* and converts them as one deck. The downloaded files are held to the same limits
|
|
68
|
+
* as uploads (35MB each, 90MB together, 50 pages). File types are judged by
|
|
69
|
+
* content, not by the link or its `Content-Type`.
|
|
70
|
+
*
|
|
71
|
+
* Downloading takes time — up to 60 seconds a link and 120 for the request — so
|
|
72
|
+
* this call waits at least 180 seconds for an answer, whatever the client's
|
|
73
|
+
* `timeoutMs`. Retries and `idempotencyKey` work exactly as in `submit`. Only one
|
|
74
|
+
* submission by URL per account can be in flight at a time; another gets a
|
|
75
|
+
* `RateLimitedError`.
|
|
76
|
+
*
|
|
77
|
+
* @param urls 1–50 `https` links. Each must resolve to a public address.
|
|
78
|
+
* @param options As `submit`. With links the page total is not known here, so
|
|
79
|
+
* `pages` is only checked for spelling and for selecting more than 50 pages.
|
|
80
|
+
* @throws InvalidUrlError A link was refused before downloading.
|
|
81
|
+
* @throws UrlFetchFailedError A download failed; may be temporary. For these two —
|
|
82
|
+
* and `InvalidFileError` about a downloaded file — `err.index` says which link,
|
|
83
|
+
* counting from 0.
|
|
84
|
+
* @throws InvalidParameterError More than 50 links, thrown locally.
|
|
85
|
+
*/
|
|
86
|
+
submitUrls(urls: string[], options?: SubmitOptions): Promise<Job>;
|
|
50
87
|
/**
|
|
51
88
|
* Split files into submittable batches and create **one job per batch**.
|
|
52
89
|
*
|
|
53
90
|
* For a pile of files too big or too numerous for a single request. Batching
|
|
54
|
-
* rules live in `planBatches`: at most
|
|
91
|
+
* rules live in `planBatches`: at most 80MB of file content and at most 50
|
|
55
92
|
* images per batch, and every PDF in a batch of its own (the SDK does not parse
|
|
56
93
|
* PDFs, so only the server knows their page count). Input order is preserved.
|
|
57
94
|
*
|
|
@@ -66,8 +103,11 @@ export declare class Image2PPTClient {
|
|
|
66
103
|
* the same batch again. Waiting is the normal path here. Total waiting is capped
|
|
67
104
|
* by the client's `rateLimitMaxWaitMs`.
|
|
68
105
|
*
|
|
69
|
-
* **
|
|
70
|
-
*
|
|
106
|
+
* **Each batch is its own submission with its own `Idempotency-Key`**, and is
|
|
107
|
+
* retried exactly as `submit` retries — plus the 429s above, with the same key.
|
|
108
|
+
* There is no `pages` or `idempotencyKey` option here: page numbers run across one
|
|
109
|
+
* submission and a key names one job, so neither can span several batches. Submit
|
|
110
|
+
* batches yourself with `submit` if you need them.
|
|
71
111
|
*
|
|
72
112
|
* **If it does give up, the jobs already created are handed back on the error**,
|
|
73
113
|
* in `err.submittedJobs`. Those jobs are running on the server with credits
|
|
@@ -79,7 +119,7 @@ export declare class Image2PPTClient {
|
|
|
79
119
|
* batching can carry it.
|
|
80
120
|
* @throws RateLimitedError Still rate limited after `rateLimitMaxWaitMs`.
|
|
81
121
|
*/
|
|
82
|
-
submitAll(paths: string[], options?:
|
|
122
|
+
submitAll(paths: string[], options?: SubmitAllOptions): Promise<Job[]>;
|
|
83
123
|
/** Fetch the current job state as a `Job` snapshot. Throws JobNotFoundError. */
|
|
84
124
|
getJob(jobId: string): Promise<Job>;
|
|
85
125
|
/**
|
|
@@ -118,7 +158,7 @@ export declare class Image2PPTClient {
|
|
|
118
158
|
/**
|
|
119
159
|
* One-shot: submit → wait for completion → download to `destPath`.
|
|
120
160
|
*
|
|
121
|
-
* One job, one PPTX — the files must fit in a single submission (
|
|
161
|
+
* One job, one PPTX — the files must fit in a single submission (90MB of file
|
|
122
162
|
* content, 50 pages). For more than that, `convertAll` splits the pile and writes
|
|
123
163
|
* one PPTX per batch.
|
|
124
164
|
*/
|
|
@@ -147,7 +187,30 @@ export declare class Image2PPTClient {
|
|
|
147
187
|
* before anything is submitted**, so an unusable destination costs nothing.
|
|
148
188
|
* @returns The written file paths, in batch order.
|
|
149
189
|
*/
|
|
150
|
-
convertAll(paths: string[], destDir: string, options?:
|
|
190
|
+
convertAll(paths: string[], destDir: string, options?: ConvertAllOptions): Promise<string[]>;
|
|
191
|
+
/**
|
|
192
|
+
* One page of the jobs this account submitted through the API, newest first.
|
|
193
|
+
*
|
|
194
|
+
* Jobs deleted on the website are not listed. Each job has `getJob`'s shape minus
|
|
195
|
+
* `pageResults`; for reconciliation, add up `creditsUsed` and `creditsRefunded`.
|
|
196
|
+
* To walk every page, use `iterJobs`.
|
|
197
|
+
*
|
|
198
|
+
* @throws InvalidParameterError A parameter is invalid; `message` says which.
|
|
199
|
+
*/
|
|
200
|
+
listJobs(options?: ListJobsOptions): Promise<JobList>;
|
|
201
|
+
/**
|
|
202
|
+
* Every job `listJobs` would list, following `nextCursor` to the end.
|
|
203
|
+
*
|
|
204
|
+
* `limit` is the page size, not a cap on how many jobs come back. A 429 is waited
|
|
205
|
+
* out (`Retry-After`, else 5s), and a page that fails in a way worth repeating
|
|
206
|
+
* (`isTransient`) is fetched again after a short backoff — at most 10 attempts per
|
|
207
|
+
* page before the error is thrown.
|
|
208
|
+
*
|
|
209
|
+
* ```ts
|
|
210
|
+
* for await (const job of client.iterJobs({ createdFrom: "2026-10-01" })) { ... }
|
|
211
|
+
* ```
|
|
212
|
+
*/
|
|
213
|
+
iterJobs(options?: Omit<ListJobsOptions, "cursor">): AsyncGenerator<Job>;
|
|
151
214
|
/** Return account info: `{ email, credits }` (available credits). */
|
|
152
215
|
account(): Promise<Account>;
|
|
153
216
|
}
|
package/dist/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,gCAAgC;
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAoChC,OAAO,KAAK,EACV,OAAO,EACP,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,gBAAgB,EAChB,aAAa,EACb,WAAW,EACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,GAAG,EAAE,OAAO,EAAwC,MAAM,YAAY,CAAC;AAGhF,eAAO,MAAM,gBAAgB,0BAA0B,CAAC;AAmhBxD;;;;;;;GAOG;AACH,qBAAa,eAAe;;IAC1B,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,QAAQ,CAAC,gBAAgB,EAAE,OAAO,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;gBAKrB,OAAO,EAAE,aAAa;IAiBlC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,GAAG,CAAC;IAgBxE;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,GAAG,CAAC;IA+B3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAkCG;IACG,SAAS,CAAC,KAAK,EAAE,MAAM,EAAE,EAAE,OAAO,GAAE,gBAAqB,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;IAqChF,gFAAgF;IAC1E,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAQzC;;;;;;;;;;;OAWG;IACG,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC;IAQxD;;;;;;OAMG;IACG,IAAI,CAAC,KAAK,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,OAAO,CAAC,GAAG,CAAC;IAqDlE;;;;;;;;;;OAUG;IACG,QAAQ,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAoEhE;;;;;;OAMG;IACG,OAAO,CACX,KAAK,EAAE,MAAM,EAAE,EACf,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,cAAmB,GAC3B,OAAO,CAAC,GAAG,CAAC;IAsBf;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACG,UAAU,CACd,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,iBAAsB,GAC9B,OAAO,CAAC,MAAM,EAAE,CAAC;IA4BpB;;;;;;;;OAQG;IACG,QAAQ,CAAC,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,OAAO,CAAC;IAoB/D;;;;;;;;;;;OAWG;IACI,QAAQ,CAAC,OAAO,GAAE,IAAI,CAAC,eAAe,EAAE,QAAQ,CAAM,GAAG,cAAc,CAAC,GAAG,CAAC;IAUnF,qEAAqE;IAC/D,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;CA2elC"}
|