fix(backend/DriveService): convert WebP/AVIF to WebP (#10239)
* fix(backend/DriveService): convert transparent WebP/AVIF to PNG * webpにする その希望が複数ありましたので * Update packages/backend/src/core/DriveService.ts Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> * update test * webpはwebpublicにできる --------- Co-authored-by: Acid Chicken (硫酸鶏) <root@acid-chicken.com> Co-authored-by: tamaina <tamaina@hotmail.co.jp>
This commit is contained in:
parent
e0b7633a7a
commit
3f53cbd8f6
|
@ -299,7 +299,7 @@ export class DriveService {
|
||||||
}
|
}
|
||||||
|
|
||||||
satisfyWebpublic = !!(
|
satisfyWebpublic = !!(
|
||||||
type !== 'image/svg+xml' && type !== 'image/webp' && type !== 'image/avif' &&
|
type !== 'image/svg+xml' && type !== 'image/avif' &&
|
||||||
!(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) &&
|
!(metadata.exif ?? metadata.iptc ?? metadata.xmp ?? metadata.tifftagPhotoshop) &&
|
||||||
metadata.width && metadata.width <= 2048 &&
|
metadata.width && metadata.width <= 2048 &&
|
||||||
metadata.height && metadata.height <= 2048
|
metadata.height && metadata.height <= 2048
|
||||||
|
@ -319,11 +319,11 @@ export class DriveService {
|
||||||
this.registerLogger.info('creating web image');
|
this.registerLogger.info('creating web image');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (['image/jpeg', 'image/webp', 'image/avif'].includes(type)) {
|
if (type === 'image/jpeg') {
|
||||||
webpublic = await this.imageProcessingService.convertSharpToJpeg(img, 2048, 2048);
|
webpublic = await this.imageProcessingService.convertSharpToJpeg(img, 2048, 2048);
|
||||||
} else if (['image/png'].includes(type)) {
|
} else if (['image/webp', 'image/avif'].includes(type)) {
|
||||||
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
|
webpublic = await this.imageProcessingService.convertSharpToWebp(img, 2048, 2048);
|
||||||
} else if (['image/svg+xml'].includes(type)) {
|
} else if (['image/png', 'image/svg+xml'].includes(type)) {
|
||||||
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
|
webpublic = await this.imageProcessingService.convertSharpToPng(img, 2048, 2048);
|
||||||
} else {
|
} else {
|
||||||
this.registerLogger.debug('web image not created (not an required image)');
|
this.registerLogger.debug('web image not created (not an required image)');
|
||||||
|
@ -749,7 +749,7 @@ export class DriveService {
|
||||||
}: UploadFromUrlArgs): Promise<DriveFile> {
|
}: UploadFromUrlArgs): Promise<DriveFile> {
|
||||||
// Create temp file
|
// Create temp file
|
||||||
const [path, cleanup] = await createTemp();
|
const [path, cleanup] = await createTemp();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// write content at URL to temp file
|
// write content at URL to temp file
|
||||||
const { filename: name } = await this.downloadService.downloadUrl(url, path);
|
const { filename: name } = await this.downloadService.downloadUrl(url, path);
|
||||||
|
|
|
@ -116,7 +116,7 @@ export class ApRendererService {
|
||||||
if (block.blockee?.uri == null) {
|
if (block.blockee?.uri == null) {
|
||||||
throw new Error('renderBlock: missing blockee uri');
|
throw new Error('renderBlock: missing blockee uri');
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type: 'Block',
|
type: 'Block',
|
||||||
id: `${this.config.url}/blocks/${block.id}`,
|
id: `${this.config.url}/blocks/${block.id}`,
|
||||||
|
@ -134,10 +134,10 @@ export class ApRendererService {
|
||||||
published: note.createdAt.toISOString(),
|
published: note.createdAt.toISOString(),
|
||||||
object,
|
object,
|
||||||
} as ICreate;
|
} as ICreate;
|
||||||
|
|
||||||
if (object.to) activity.to = object.to;
|
if (object.to) activity.to = object.to;
|
||||||
if (object.cc) activity.cc = object.cc;
|
if (object.cc) activity.cc = object.cc;
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -155,7 +155,7 @@ export class ApRendererService {
|
||||||
public renderDocument(file: DriveFile): IApDocument {
|
public renderDocument(file: DriveFile): IApDocument {
|
||||||
return {
|
return {
|
||||||
type: 'Document',
|
type: 'Document',
|
||||||
mediaType: file.type,
|
mediaType: file.webpublicType ?? file.type,
|
||||||
url: this.driveFileEntityService.getPublicUrl(file),
|
url: this.driveFileEntityService.getPublicUrl(file),
|
||||||
name: file.comment,
|
name: file.comment,
|
||||||
};
|
};
|
||||||
|
@ -297,16 +297,16 @@ export class ApRendererService {
|
||||||
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
const items = await this.driveFilesRepository.findBy({ id: In(ids) });
|
||||||
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
|
return ids.map(id => items.find(item => item.id === id)).filter(item => item != null) as DriveFile[];
|
||||||
};
|
};
|
||||||
|
|
||||||
let inReplyTo;
|
let inReplyTo;
|
||||||
let inReplyToNote: Note | null;
|
let inReplyToNote: Note | null;
|
||||||
|
|
||||||
if (note.replyId) {
|
if (note.replyId) {
|
||||||
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
inReplyToNote = await this.notesRepository.findOneBy({ id: note.replyId });
|
||||||
|
|
||||||
if (inReplyToNote != null) {
|
if (inReplyToNote != null) {
|
||||||
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
|
const inReplyToUser = await this.usersRepository.findOneBy({ id: inReplyToNote.userId });
|
||||||
|
|
||||||
if (inReplyToUser != null) {
|
if (inReplyToUser != null) {
|
||||||
if (inReplyToNote.uri) {
|
if (inReplyToNote.uri) {
|
||||||
inReplyTo = inReplyToNote.uri;
|
inReplyTo = inReplyToNote.uri;
|
||||||
|
@ -322,24 +322,24 @@ export class ApRendererService {
|
||||||
} else {
|
} else {
|
||||||
inReplyTo = null;
|
inReplyTo = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
let quote;
|
let quote;
|
||||||
|
|
||||||
if (note.renoteId) {
|
if (note.renoteId) {
|
||||||
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
|
const renote = await this.notesRepository.findOneBy({ id: note.renoteId });
|
||||||
|
|
||||||
if (renote) {
|
if (renote) {
|
||||||
quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`;
|
quote = renote.uri ? renote.uri : `${this.config.url}/notes/${renote.id}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const attributedTo = `${this.config.url}/users/${note.userId}`;
|
const attributedTo = `${this.config.url}/users/${note.userId}`;
|
||||||
|
|
||||||
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
|
const mentions = (JSON.parse(note.mentionedRemoteUsers) as IMentionedRemoteUsers).map(x => x.uri);
|
||||||
|
|
||||||
let to: string[] = [];
|
let to: string[] = [];
|
||||||
let cc: string[] = [];
|
let cc: string[] = [];
|
||||||
|
|
||||||
if (note.visibility === 'public') {
|
if (note.visibility === 'public') {
|
||||||
to = ['https://www.w3.org/ns/activitystreams#Public'];
|
to = ['https://www.w3.org/ns/activitystreams#Public'];
|
||||||
cc = [`${attributedTo}/followers`].concat(mentions);
|
cc = [`${attributedTo}/followers`].concat(mentions);
|
||||||
|
@ -352,44 +352,44 @@ export class ApRendererService {
|
||||||
} else {
|
} else {
|
||||||
to = mentions;
|
to = mentions;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({
|
const mentionedUsers = note.mentions.length > 0 ? await this.usersRepository.findBy({
|
||||||
id: In(note.mentions),
|
id: In(note.mentions),
|
||||||
}) : [];
|
}) : [];
|
||||||
|
|
||||||
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
const hashtagTags = (note.tags ?? []).map(tag => this.renderHashtag(tag));
|
||||||
const mentionTags = mentionedUsers.map(u => this.renderMention(u));
|
const mentionTags = mentionedUsers.map(u => this.renderMention(u));
|
||||||
|
|
||||||
const files = await getPromisedFiles(note.fileIds);
|
const files = await getPromisedFiles(note.fileIds);
|
||||||
|
|
||||||
const text = note.text ?? '';
|
const text = note.text ?? '';
|
||||||
let poll: Poll | null = null;
|
let poll: Poll | null = null;
|
||||||
|
|
||||||
if (note.hasPoll) {
|
if (note.hasPoll) {
|
||||||
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
poll = await this.pollsRepository.findOneBy({ noteId: note.id });
|
||||||
}
|
}
|
||||||
|
|
||||||
let apText = text;
|
let apText = text;
|
||||||
|
|
||||||
if (quote) {
|
if (quote) {
|
||||||
apText += `\n\nRE: ${quote}`;
|
apText += `\n\nRE: ${quote}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
const summary = note.cw === '' ? String.fromCharCode(0x200B) : note.cw;
|
||||||
|
|
||||||
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
const content = this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
||||||
text: apText,
|
text: apText,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const emojis = await this.getEmojis(note.emojis);
|
const emojis = await this.getEmojis(note.emojis);
|
||||||
const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
|
const apemojis = emojis.map(emoji => this.renderEmoji(emoji));
|
||||||
|
|
||||||
const tag = [
|
const tag = [
|
||||||
...hashtagTags,
|
...hashtagTags,
|
||||||
...mentionTags,
|
...mentionTags,
|
||||||
...apemojis,
|
...apemojis,
|
||||||
];
|
];
|
||||||
|
|
||||||
const asPoll = poll ? {
|
const asPoll = poll ? {
|
||||||
type: 'Question',
|
type: 'Question',
|
||||||
content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
content: this.apMfmService.getNoteHtml(Object.assign({}, note, {
|
||||||
|
@ -601,7 +601,7 @@ export class ApRendererService {
|
||||||
if (typeof x === 'object' && x.id == null) {
|
if (typeof x === 'object' && x.id == null) {
|
||||||
x.id = `${this.config.url}/${uuid()}`;
|
x.id = `${this.config.url}/${uuid()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Object.assign({
|
return Object.assign({
|
||||||
'@context': [
|
'@context': [
|
||||||
'https://www.w3.org/ns/activitystreams',
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
@ -634,18 +634,18 @@ export class ApRendererService {
|
||||||
],
|
],
|
||||||
}, x as T & { id: string; });
|
}, x as T & { id: string; });
|
||||||
}
|
}
|
||||||
|
|
||||||
@bindThis
|
@bindThis
|
||||||
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
public async attachLdSignature(activity: any, user: { id: User['id']; host: null; }): Promise<IActivity> {
|
||||||
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
const keypair = await this.userKeypairStoreService.getUserKeypair(user.id);
|
||||||
|
|
||||||
const ldSignature = this.ldSignatureService.use();
|
const ldSignature = this.ldSignatureService.use();
|
||||||
ldSignature.debug = false;
|
ldSignature.debug = false;
|
||||||
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
|
activity = await ldSignature.signRsaSignature2017(activity, keypair.privateKey, `${this.config.url}/users/${user.id}#main-key`);
|
||||||
|
|
||||||
return activity;
|
return activity;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render OrderedCollectionPage
|
* Render OrderedCollectionPage
|
||||||
* @param id URL of self
|
* @param id URL of self
|
||||||
|
@ -686,11 +686,11 @@ export class ApRendererService {
|
||||||
type: 'OrderedCollection',
|
type: 'OrderedCollection',
|
||||||
totalItems,
|
totalItems,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (first) page.first = first;
|
if (first) page.first = first;
|
||||||
if (last) page.last = last;
|
if (last) page.last = last;
|
||||||
if (orderedItems) page.orderedItems = orderedItems;
|
if (orderedItems) page.orderedItems = orderedItems;
|
||||||
|
|
||||||
return page;
|
return page;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,7 @@ import * as assert from 'assert';
|
||||||
// node-fetch only supports it's own Blob yet
|
// node-fetch only supports it's own Blob yet
|
||||||
// https://github.com/node-fetch/node-fetch/pull/1664
|
// https://github.com/node-fetch/node-fetch/pull/1664
|
||||||
import { Blob } from 'node-fetch';
|
import { Blob } from 'node-fetch';
|
||||||
import { startServer, signup, post, api, uploadFile } from '../utils.js';
|
import { startServer, signup, post, api, uploadFile, simpleGet } from '../utils.js';
|
||||||
import type { INestApplicationContext } from '@nestjs/common';
|
import type { INestApplicationContext } from '@nestjs/common';
|
||||||
|
|
||||||
describe('Endpoints', () => {
|
describe('Endpoints', () => {
|
||||||
|
@ -439,6 +439,45 @@ describe('Endpoints', () => {
|
||||||
assert.strictEqual(res.body.name, 'image.svg');
|
assert.strictEqual(res.body.name, 'image.svg');
|
||||||
assert.strictEqual(res.body.type, 'image/svg+xml');
|
assert.strictEqual(res.body.type, 'image/svg+xml');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
for (const type of ['webp', 'avif']) {
|
||||||
|
const mediaType = `image/${type}`;
|
||||||
|
|
||||||
|
const getWebpublicType = async (user: any, fileId: string): Promise<string> => {
|
||||||
|
// drive/files/create does not expose webpublicType directly, so get it by posting it
|
||||||
|
const res = await post(user, {
|
||||||
|
text: mediaType,
|
||||||
|
fileIds: [fileId],
|
||||||
|
});
|
||||||
|
const apRes = await simpleGet(`notes/${res.id}`, 'application/activity+json');
|
||||||
|
assert.strictEqual(apRes.status, 200);
|
||||||
|
assert.ok(Array.isArray(apRes.body.attachment));
|
||||||
|
return apRes.body.attachment[0].mediaType;
|
||||||
|
};
|
||||||
|
|
||||||
|
test(`透明な${type}ファイルを作成できる`, async () => {
|
||||||
|
const path = `with-alpha.${type}`;
|
||||||
|
const res = await uploadFile(alice, { path });
|
||||||
|
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.body.name, path);
|
||||||
|
assert.strictEqual(res.body.type, mediaType);
|
||||||
|
|
||||||
|
const webpublicType = await getWebpublicType(alice, res.body.id);
|
||||||
|
assert.strictEqual(webpublicType, 'image/webp');
|
||||||
|
});
|
||||||
|
|
||||||
|
test(`透明じゃない${type}ファイルを作成できる`, async () => {
|
||||||
|
const path = `without-alpha.${type}`;
|
||||||
|
const res = await uploadFile(alice, { path });
|
||||||
|
assert.strictEqual(res.status, 200);
|
||||||
|
assert.strictEqual(res.body.name, path);
|
||||||
|
assert.strictEqual(res.body.type, mediaType);
|
||||||
|
|
||||||
|
const webpublicType = await getWebpublicType(alice, res.body.id);
|
||||||
|
assert.strictEqual(webpublicType, 'image/webp');
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('drive/files/update', () => {
|
describe('drive/files/update', () => {
|
||||||
|
|
BIN
packages/backend/test/resources/with-alpha.avif
Normal file
BIN
packages/backend/test/resources/with-alpha.avif
Normal file
Binary file not shown.
BIN
packages/backend/test/resources/with-alpha.webp
Normal file
BIN
packages/backend/test/resources/with-alpha.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.9 KiB |
BIN
packages/backend/test/resources/without-alpha.avif
Normal file
BIN
packages/backend/test/resources/without-alpha.avif
Normal file
Binary file not shown.
BIN
packages/backend/test/resources/without-alpha.webp
Normal file
BIN
packages/backend/test/resources/without-alpha.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
|
@ -204,7 +204,12 @@ export const simpleGet = async (path: string, accept = '*/*'): Promise<{ status:
|
||||||
redirect: 'manual',
|
redirect: 'manual',
|
||||||
});
|
});
|
||||||
|
|
||||||
const body = res.headers.get('content-type') === 'application/json; charset=utf-8'
|
const jsonTypes = [
|
||||||
|
'application/json; charset=utf-8',
|
||||||
|
'application/activity+json; charset=utf-8',
|
||||||
|
];
|
||||||
|
|
||||||
|
const body = jsonTypes.includes(res.headers.get('content-type') ?? '')
|
||||||
? await res.json()
|
? await res.json()
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue