image.ts 2.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081
  1. export * as Image from "./image"
  2. import { makeLocationNode } from "./effect/app-node"
  3. import { Context, Effect, Layer, Schema } from "effect"
  4. import { Config } from "./config"
  5. import { FileSystem } from "./filesystem"
  6. export class ResizerUnavailableError extends Schema.TaggedErrorClass<ResizerUnavailableError>()(
  7. "Image.ResizerUnavailableError",
  8. {},
  9. ) {}
  10. export class DecodeError extends Schema.TaggedErrorClass<DecodeError>()("Image.DecodeError", {
  11. resource: Schema.String,
  12. }) {
  13. override get message() {
  14. return `Image could not be decoded: ${this.resource}`
  15. }
  16. }
  17. export class SizeError extends Schema.TaggedErrorClass<SizeError>()("Image.SizeError", {
  18. resource: Schema.String,
  19. width: Schema.Number,
  20. height: Schema.Number,
  21. bytes: Schema.Number,
  22. maxWidth: Schema.Number,
  23. maxHeight: Schema.Number,
  24. maxBytes: Schema.Number,
  25. }) {
  26. override get message() {
  27. return `Image ${this.resource} is ${this.width}x${this.height} with base64 size ${this.bytes}, exceeding configured limits ${this.maxWidth}x${this.maxHeight}/${this.maxBytes} bytes`
  28. }
  29. }
  30. export interface Interface {
  31. readonly normalize: (
  32. resource: string,
  33. content: FileSystem.Content & { readonly encoding: "base64" },
  34. ) => Effect.Effect<
  35. FileSystem.Content & { readonly encoding: "base64" },
  36. ResizerUnavailableError | DecodeError | SizeError
  37. >
  38. }
  39. export class Service extends Context.Service<Service, Interface>()("@kirincode/Image") {}
  40. const layer = Layer.effect(
  41. Service,
  42. Effect.gen(function* () {
  43. const config = yield* Config.Service
  44. const loadAdapter = yield* Effect.cached(
  45. Effect.tryPromise({
  46. try: () => import("./image/photon"),
  47. catch: () => new ResizerUnavailableError(),
  48. }).pipe(Effect.flatMap((adapter) => adapter.make)),
  49. )
  50. const normalize = Effect.fn("Image.normalize")(function* (
  51. resource: string,
  52. content: FileSystem.Content & { readonly encoding: "base64" },
  53. ) {
  54. const image = Object.assign(
  55. {},
  56. ...(yield* config.entries()).flatMap((entry) =>
  57. entry.type === "document" && entry.info.attachments?.image ? [entry.info.attachments.image] : [],
  58. ),
  59. )
  60. const normalize = yield* loadAdapter
  61. return yield* normalize(resource, content, {
  62. autoResize: image.auto_resize ?? true,
  63. maxWidth: image.max_width ?? 2_000,
  64. maxHeight: image.max_height ?? 2_000,
  65. maxBase64Bytes: image.max_base64_bytes ?? 5 * 1024 * 1024,
  66. })
  67. })
  68. return Service.of({ normalize })
  69. }),
  70. )
  71. export const locationLayer = layer.pipe(Layer.provide(Config.locationLayer))
  72. export const node = makeLocationNode({ service: Service, layer, deps: [Config.node] })