bookish-press 0.11.0 → 0.11.2

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.
@@ -15,7 +15,13 @@ async function addEditor() {
15
15
  const { createUser } = await import("../../models/CRUD");
16
16
  const uid = await createUser(email);
17
17
  if (uid === null) newEditorError = "couldn't add editor";
18
- else change([...uids, uid]);
18
+ else {
19
+ // The server only reveals the emails of people who already
20
+ // share a saved book, and this one isn't saved yet. We know
21
+ // the email anyway, so show it now.
22
+ emails.update((known) => new Map(known).set(uid, email));
23
+ change([...uids, uid]);
24
+ }
19
25
  } finally {
20
26
  loading = undefined;
21
27
  }
@@ -74,6 +74,27 @@ export declare function resolveImageURL(url: string, base: string): string;
74
74
  * keeping its caption.
75
75
  */
76
76
  export declare function prepareImage(url: string, size: ImageSize, grayscale?: boolean): Promise<PreparedImage | undefined>;
77
+ /**
78
+ * Whether to package the re-encoded image rather than the original.
79
+ *
80
+ * Keep whichever is smaller, since re-encoding can inflate a file: resizing a
81
+ * diagram resamples its hairlines into shades of grey, and the result can be
82
+ * larger than the crisp original. Three things override that:
83
+ *
84
+ * - An image far past the budget is shrunk regardless, because decoding it
85
+ * costs a small reader memory it may not have.
86
+ * - Greyscale was asked for, and an image small enough to pass through would
87
+ * otherwise come out in colour while everything around it was grey.
88
+ * - WebP is always converted. EPUB 3.3 allows it, but many e-readers still in
89
+ * use predate that and show nothing, and bound books serve WebP by default.
90
+ */
91
+ export declare function shouldUseReencoded(image: {
92
+ mediaType: string;
93
+ oversize: boolean;
94
+ grayscale: boolean;
95
+ originalBytes: number;
96
+ reencodedBytes: number;
97
+ }): boolean;
77
98
  /**
78
99
  * Run tasks with a bounded number in flight, so a book with a hundred images
79
100
  * doesn't open a hundred connections at once.
@@ -138,20 +138,37 @@ export async function prepareImage(url, size, grayscale = false) {
138
138
  const reencoded = await reencode(bytes, mediaType, size, grayscale);
139
139
  if (reencoded === undefined)
140
140
  return passThrough(bytes, mediaType);
141
- // Keep whichever is smaller, since re-encoding can inflate a file: resizing
142
- // a diagram resamples its hairlines into shades of grey, and the result can
143
- // be larger than the crisp original. But an image far past the budget is
144
- // shrunk regardless, because decoding it costs a small reader memory it may
145
- // not have, and a few extra kilobytes are the cheaper price.
146
- //
147
- // Greyscale overrides the size comparison too: it was asked for, and an
148
- // image small enough to pass through untouched would otherwise come out in
149
- // colour while everything around it was grey.
150
141
  const { oversize, ...prepared } = reencoded;
151
- return oversize || grayscale || prepared.bytes.length < bytes.length
142
+ return shouldUseReencoded({
143
+ mediaType,
144
+ oversize,
145
+ grayscale,
146
+ originalBytes: bytes.length,
147
+ reencodedBytes: prepared.bytes.length,
148
+ })
152
149
  ? prepared
153
150
  : passThrough(bytes, mediaType);
154
151
  }
152
+ /**
153
+ * Whether to package the re-encoded image rather than the original.
154
+ *
155
+ * Keep whichever is smaller, since re-encoding can inflate a file: resizing a
156
+ * diagram resamples its hairlines into shades of grey, and the result can be
157
+ * larger than the crisp original. Three things override that:
158
+ *
159
+ * - An image far past the budget is shrunk regardless, because decoding it
160
+ * costs a small reader memory it may not have.
161
+ * - Greyscale was asked for, and an image small enough to pass through would
162
+ * otherwise come out in colour while everything around it was grey.
163
+ * - WebP is always converted. EPUB 3.3 allows it, but many e-readers still in
164
+ * use predate that and show nothing, and bound books serve WebP by default.
165
+ */
166
+ export function shouldUseReencoded(image) {
167
+ return (image.mediaType === 'image/webp' ||
168
+ image.oversize ||
169
+ image.grayscale ||
170
+ image.reencodedBytes < image.originalBytes);
171
+ }
155
172
  /**
156
173
  * Draw the image at its budgeted size and encode it, reporting whether the
157
174
  * original was far enough past the budget that it must be shrunk regardless of
@@ -1,5 +1,5 @@
1
1
  import { expect, test } from 'vitest';
2
- import { DEFAULT_SIZE, exceedsBudget, fitWithin, mapWithLimit, resolveImageURL, SIZES, } from './images';
2
+ import { DEFAULT_SIZE, exceedsBudget, fitWithin, mapWithLimit, resolveImageURL, shouldUseReencoded, SIZES, } from './images';
3
3
  test('shrinks a landscape image to the width budget', () => {
4
4
  expect(fitWithin(2400, 1200, 1200, 1600)).toEqual({
5
5
  width: 1200,
@@ -129,3 +129,35 @@ test('a preset says nothing about colour', () => {
129
129
  for (const size of Object.values(SIZES))
130
130
  expect(size).not.toHaveProperty('grayscale');
131
131
  });
132
+ const plain = {
133
+ oversize: false,
134
+ grayscale: false,
135
+ originalBytes: 1000,
136
+ reencodedBytes: 2000,
137
+ };
138
+ test('keeps a PNG or JPEG original when re-encoding would make it larger', () => {
139
+ expect(shouldUseReencoded({ ...plain, mediaType: 'image/png' })).toBe(false);
140
+ expect(shouldUseReencoded({ ...plain, mediaType: 'image/jpeg' })).toBe(false);
141
+ });
142
+ test('uses the re-encoded image when it is smaller', () => {
143
+ expect(shouldUseReencoded({
144
+ ...plain,
145
+ mediaType: 'image/png',
146
+ reencodedBytes: 500,
147
+ })).toBe(true);
148
+ });
149
+ test('always converts WebP, even when the conversion is larger', () => {
150
+ expect(shouldUseReencoded({ ...plain, mediaType: 'image/webp' })).toBe(true);
151
+ });
152
+ test('oversize and greyscale images are always re-encoded', () => {
153
+ expect(shouldUseReencoded({
154
+ ...plain,
155
+ mediaType: 'image/png',
156
+ oversize: true,
157
+ })).toBe(true);
158
+ expect(shouldUseReencoded({
159
+ ...plain,
160
+ mediaType: 'image/png',
161
+ grayscale: true,
162
+ })).toBe(true);
163
+ });
@@ -1,4 +1,4 @@
1
- import { db, functions } from './Firebase';
1
+ import { auth, db, functions } from './Firebase';
2
2
  import { collection, getDocs, getDoc, setDoc, doc, addDoc, query, where, deleteDoc, onSnapshot, } from 'firebase/firestore';
3
3
  import { httpsCallable } from 'firebase/functions';
4
4
  import Edition from './book/Edition';
@@ -78,6 +78,11 @@ export function listenToChapters(bookID, editionID, react) {
78
78
  export async function getUserEmails(uids) {
79
79
  if (!functions)
80
80
  throw Error("Can't publish, no connection to Firebase functions.");
81
+ // The function only answers signed-in users, and a page load can get here
82
+ // before Firebase has restored the sign-in, so wait for it.
83
+ await auth?.authStateReady();
84
+ if (!auth?.currentUser)
85
+ return null;
81
86
  const getUserEmails = httpsCallable(functions, 'getUserEmailsGen2');
82
87
  const result = await getUserEmails({
83
88
  uids,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bookish-press",
3
3
  "private": false,
4
- "version": "0.11.0",
4
+ "version": "0.11.2",
5
5
  "type": "module",
6
6
  "description": "A platform for reading and writing readable, accessible, beautiful online books.",
7
7
  "license": "MIT",