Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion apps/juxtaposition-ui/src/api/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ export type PostDto = {
app_data?: string; // nintendo base64

painting?: string; // base64 or '', undef for PMs
painting_img?: string; // URL frag (leading /) or '', undef for PMs
painting_big?: string; // URL frag (leading /) or '', undef for PMs
screenshot?: string; // URL frag (leading /) or '', undef for PMs
screenshot_big?: string; // URL frag (leading /) or '', undef for PMs/old posts
screenshot_thumb?: string; // URL frag (leading /) or '', undef for PMs/old posts
screenshot_length?: number;
screenshot_aspect?: string; // '4:3' '5:4' '16:9'
screenshot_aspect?: string; // '4:3' '5:3' '16:9'

search_key?: string[]; // can be []
topic_tag?: string; // can be ''
Expand Down
114 changes: 94 additions & 20 deletions apps/juxtaposition-ui/src/images.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { IMagickImage } from '@imagemagick/magick-wasm';

export type Painting = {
png: Buffer;
big: Buffer;
tgaz: Buffer;
};

Expand All @@ -27,6 +28,12 @@ function processPainting(image: IMagickImage): Painting {

return {
png: image.write('PNG', Buffer.from),
big: image.clone((image) => {
image.filterType = FilterType.Point;
image.resize(640, 240);

return image.write('PNG', Buffer.from);
}),
tgaz: image.clone((image) => {
// Ingame TGA decoders don't support all the fun stuff.
image.colorType = ColorType.TrueColorAlpha;
Expand Down Expand Up @@ -82,6 +89,11 @@ export type ProcessPaintingOptions = {
pid: number;
postId: string;
};
export type PaintingUrls = {
blob: string;
img: string;
big: string;
};
/**
* Processes and uploads a new painting to the CDN - to paintings/${pid}/${postID}.png.
* @param paintingBlob base64 TGAZ or BMP blob from the client request body.
Expand All @@ -90,7 +102,7 @@ export type ProcessPaintingOptions = {
* @param postID Post ID.
* @returns base64 TGAZ blob, sanitised.
*/
export async function uploadPainting(opts: ProcessPaintingOptions): Promise<string | null> {
export async function uploadPainting(opts: ProcessPaintingOptions): Promise<PaintingUrls | null> {
const paintingBuf = Buffer.from(opts.blob.replace(/\0/g, '').trim(), 'base64');
const paintings = ((): Painting => {
if (opts.autodetectFormat) {
Expand All @@ -102,44 +114,66 @@ export async function uploadPainting(opts: ProcessPaintingOptions): Promise<stri
}
})();

if (!await uploadCDNAsset(`paintings/${opts.pid}/${opts.postId}.png`, paintings.png, 'public-read')) {
const imgKey = `paintings/${opts.pid}/${opts.postId}.png`;
const bigKey = `paintings/${opts.pid}/${opts.postId}-big.png`;

if (!await uploadCDNAsset(imgKey, paintings.png, 'public-read')) {
return null;
}

return paintings.tgaz.toString('base64');
if (!await uploadCDNAsset(bigKey, paintings.big, 'public-read')) {
return null;
}

return {
blob: paintings.tgaz.toString('base64'),
img: `/${imgKey}`,
big: `/${bigKey}`
};
}

export type Aspect = '16:9' | '5:4' | '4:3';
export type Aspect = '16:9' | '5:3' | '4:3';

export type Screenshot = {
// Normal image
jpg: Buffer;
// Tiny image (for ctr)
thumb: Buffer;
// 2x image (for wiiu)
big: Buffer | null;

aspect: Aspect;
};

type ScreenshotRes = {
// full res
type Res = {
w: number;
h: number;
// thumbnail res
tw: number;
th: number;
// other
};
function res(w: number, h: number): Res {
return { w, h };
}

type ScreenshotRes = {
full: Res;
thumb: Res;
big: Res | null;

aspect: Aspect;
};

const validResolutions: ScreenshotRes[] = [
{ w: 800, h: 450, tw: 320, th: 180, aspect: '16:9' },
{ w: 400, h: 240, tw: 320, th: 192, aspect: '5:4' },
{ w: 320, h: 240, tw: 320, th: 240, aspect: '4:3' },
{ w: 640, h: 480, tw: 320, th: 240, aspect: '4:3' }
{ full: res(800, 450), thumb: res(320, 180), big: null, aspect: '16:9' },
{ full: res(400, 240), thumb: res(320, 192), big: res(800, 480), aspect: '5:3' },
{ full: res(320, 240), thumb: res(320, 240), big: res(640, 480), aspect: '4:3' },
{ full: res(640, 480), thumb: res(320, 240), big: null, aspect: '4:3' }
];

function processScreenshot(image: IMagickImage): Screenshot | null {
const res = validResolutions.find(({ w, h }) => w === image.width && h === image.height);
const res = validResolutions.find(({ full }) => full.w === image.width && full.h === image.height);
if (res === undefined) {
return null;
}
const { thumb, big, aspect } = res;
// Remove EXIF whatever
image.strip();

Expand All @@ -151,12 +185,25 @@ function processScreenshot(image: IMagickImage): Screenshot | null {
jpg: image.write('JPEG', Buffer.from),
thumb: image.clone((image) => {
image.filterType = FilterType.Lanczos2;
image.resize(res.tw, res.th);
image.extent(res.tw, res.th, Gravity.Center);
image.quality = 80; // smash 'em
image.resize(thumb.w, thumb.h);
image.extent(thumb.w, thumb.h, Gravity.Center);

image.quality = 85; // smash 'em
return image.write('JPEG', Buffer.from);
}),
big: image.clone((image) => {
if (!big) {
return null;
}

// the entire purpose of doing this is to get sharp pixels
image.filterType = FilterType.Point;
image.resize(big.w, big.h);
image.extent(big.w, big.h, Gravity.Center);

return image.write('JPEG', Buffer.from);
}),
aspect: res.aspect
aspect
};
}

Expand All @@ -168,6 +215,7 @@ export type ScreenshotUrls = {
full: string;
fullLength: number;
thumb: string;
big: string | null;
aspect: Aspect;
};

Expand All @@ -185,6 +233,7 @@ export async function uploadScreenshot(opts: UploadScreenshotOptions): Promise<S

const fullKey = `screenshots/${opts.pid}/${opts.postId}.jpg`;
const thumbKey = `screenshots/${opts.pid}/${opts.postId}-thumb.jpg`;
const bigKey = `screenshots/${opts.pid}/${opts.postId}-big.jpg`;

const fullLength = screenshots.jpg.byteLength;

Expand All @@ -196,15 +245,22 @@ export async function uploadScreenshot(opts: UploadScreenshotOptions): Promise<S
return null;
}

if (screenshots.big && !await uploadCDNAsset(bigKey, screenshots.big, 'public-read')) {
return null;
}

const full = `/${fullKey}`;
const thumb = `/${thumbKey}`;
const big = screenshots.big ? `/${bigKey}` : null;

return { full, fullLength, thumb, aspect: screenshots.aspect };
return { full, fullLength, thumb, big, aspect: screenshots.aspect };
}

type Icon = {
icon32: Buffer;
icon48: Buffer;
icon64: Buffer;
icon96: Buffer;
icon128: Buffer;
tga: Buffer;
};
Expand All @@ -225,10 +281,18 @@ function processIcon(image: IMagickImage): Icon | null {
image.resize(32, 32);
return image.write('PNG', Buffer.from);
}),
icon48: image.clone((image) => {
image.resize(48, 48);
return image.write('PNG', Buffer.from);
}),
icon64: image.clone((image) => {
image.resize(64, 64);
return image.write('PNG', Buffer.from);
}),
icon96: image.clone((image) => {
image.resize(96, 96);
return image.write('PNG', Buffer.from);
}),
icon128: image.clone((image) => {
image.resize(128, 128);
return image.write('PNG', Buffer.from);
Expand All @@ -251,7 +315,9 @@ function processIcon(image: IMagickImage): Icon | null {

export type IconUrls = {
icon32: string;
icon48: string;
icon64: string;
icon96: string;
icon128: string;
tgaBlob: string;
};
Expand All @@ -267,22 +333,30 @@ export async function uploadIcons(opts: UploadIconsOptions): Promise<IconUrls |
}

const icon32Key = `icons/${opts.communityId}/32.png`;
const icon48Key = `icons/${opts.communityId}/48.png`;
const icon64Key = `icons/${opts.communityId}/64.png`;
const icon96Key = `icons/${opts.communityId}/96.png`;
const icon128Key = `icons/${opts.communityId}/128.png`;

if (!await uploadCDNAsset(icon32Key, icons.icon32, 'public-read') ||
!await uploadCDNAsset(icon48Key, icons.icon48, 'public-read') ||
!await uploadCDNAsset(icon64Key, icons.icon64, 'public-read') ||
!await uploadCDNAsset(icon96Key, icons.icon96, 'public-read') ||
!await uploadCDNAsset(icon128Key, icons.icon128, 'public-read')) {
return null;
}

const icon32 = `/${icon32Key}`;
const icon48 = `/${icon48Key}`;
const icon64 = `/${icon64Key}`;
const icon96 = `/${icon96Key}`;
const icon128 = `/${icon128Key}`;

return {
icon32,
icon48,
icon64,
icon96,
icon128,
tgaBlob: icons.tga.toString('base64')
};
Expand Down
3 changes: 3 additions & 0 deletions apps/juxtaposition-ui/src/models/post.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,10 @@ export const PostSchema = new Schema({
body: String,
app_data: String,
painting: String,
painting_img: String,
painting_big: String,
screenshot: String,
screenshot_big: String,
screenshot_thumb: String,
screenshot_length: Number,
screenshot_aspect: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { WebMessagesView } from '@/services/juxt-web/views/web/messages';
import { WebMessageThreadView } from '@/services/juxt-web/views/web/messageThread';
import { getInvalidPostRegex, getUserAccountData, getUserFriendPIDs } from '@/util';
import { parseReq } from '@/services/juxt-web/routes/routeUtils';
import type { ScreenshotUrls } from '@/images';
import type { PaintingUrls, ScreenshotUrls } from '@/images';

export const messagesRouter = express.Router();

Expand Down Expand Up @@ -77,16 +77,16 @@ messagesRouter.post('/new', async function (req, res) {
res.status(422);
return res.redirect(`/friend_messages/${conversation.id}`);
}
let paintingBlob: string | null = null;
let paintings: PaintingUrls | null = null;
if (req.body._post_type === 'painting' && req.body.painting) {
paintingBlob = await uploadPainting({
paintings = await uploadPainting({
blob: req.body.painting,
autodetectFormat: false,
isBmp: req.body.bmp === 'true',
pid: authCtx.pid,
postId
});
if (paintingBlob === null) {
if (paintings === null) {
res.status(422);
return res.renderError({
code: 422,
Expand Down Expand Up @@ -146,8 +146,11 @@ messagesRouter.post('/new', async function (req, res) {
community_id: conversation.id,
screen_name: authCtx.user.mii.name,
body: body,
painting: paintingBlob ?? '',
painting: paintings?.blob ?? '',
painting_img: paintings?.img ?? '',
painting_big: paintings?.big ?? '',
screenshot: screenshots?.full ?? '',
screenshot_big: screenshots?.big ?? '',
screenshot_length: screenshots?.fullLength ?? 0,
screenshot_thumb: screenshots?.thumb ?? '',
screenshot_aspect: screenshots?.aspect ?? '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { PortalPostPageView } from '@/services/juxt-web/views/portal/postPageVie
import type { Request, Response } from 'express';
import type { InferSchemaType } from 'mongoose';
import type { GetUserDataResponse } from '@pretendonetwork/grpc/account/get_user_data_rpc';
import type { PaintingUrls } from '@/images';
import type { PostPageViewProps } from '@/services/juxt-web/views/web/postPageView';
import type { PostSchema } from '@/models/post';
import type { CommunitySchema } from '@/models/communities';
Expand Down Expand Up @@ -357,16 +358,16 @@ async function newPost(req: Request, res: Response): Promise<void> {
return res.redirect(`/titles/${community.olive_community_id}/new`);
}

let paintingBlob = null;
let paintings: PaintingUrls | null = null;
if (body._post_type === 'painting' && body.painting) {
paintingBlob = await uploadPainting({
paintings = await uploadPainting({
blob: body.painting,
autodetectFormat: false,
isBmp: body.bmp,
pid: auth().pid,
postId
});
if (paintingBlob === null) {
if (paintings === null) {
res.status(422);
res.renderError({
code: 422,
Expand Down Expand Up @@ -430,8 +431,11 @@ async function newPost(req: Request, res: Response): Promise<void> {
community_id: community.olive_community_id,
screen_name: userSettings.screen_name,
body: postBody,
painting: paintingBlob ?? '',
painting: paintings?.blob ?? '',
painting_img: paintings?.img ?? '',
painting_big: paintings?.big ?? '',
screenshot: screenshots?.full ?? '',
screenshot_big: screenshots?.big ?? '',
screenshot_length: screenshots?.fullLength ?? 0,
screenshot_thumb: screenshots?.thumb ?? '',
screenshot_aspect: screenshots?.aspect ?? '',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export function PortalCommunityItem(props: CommunityItemProps): ReactNode {

export function PortalCommunityListView(props: CommunityListViewProps): ReactNode {
return (
<PortalRoot title={props.ctx.lang.all_communities.text} onLoad="stopLoading();">
<PortalRoot ctx={props.ctx} title={props.ctx.lang.all_communities.text} onLoad="stopLoading();">
<PortalNavBar ctx={props.ctx} selection={2} />
<PortalPageBody>
<header id="header">
Expand All @@ -50,7 +50,7 @@ export function PortalCommunityListView(props: CommunityListViewProps): ReactNod

export function PortalCommunityOverviewView(props: CommunityOverviewViewProps): ReactNode {
return (
<PortalRoot title={props.ctx.lang.global.communities} onLoad="stopLoading();">
<PortalRoot ctx={props.ctx} title={props.ctx.lang.global.communities} onLoad="stopLoading();">
<PortalNavBar ctx={props.ctx} selection={2} />
<PortalPageBody>
<header id="header">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function PortalCommunityView(props: CommunityViewProps): ReactNode {
: utils.cdn(props.ctx, `/headers/${imageId}/WiiU.png`);

return (
<PortalRoot title={community.name}>
<PortalRoot ctx={props.ctx} title={community.name}>
<PortalNavBar ctx={props.ctx} selection={2} />
<PortalPageBody>
<header id="header">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export function PortalErrorView(props: ErrorViewProps): ReactNode {
const title = `Error: ${props.code}`;

return (
<PortalRoot title={title} onLoad="wiiuBrowser.endStartUp();">
<PortalRoot ctx={props.ctx} title={title} onLoad="wiiuBrowser.endStartUp();">
<PortalNavBar ctx={props.ctx} selection={-1} />
<PortalPageBody>
<header id="header">
Expand Down
Loading