image.ts 2.5 KB

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