Agamnentzar / ag-psd

Javascript library for reading and writing PSD files
Other
489 stars 66 forks source link

how to split all the layers (nodes in psd file) to render independently, I want to use recursive function to get the children of psd file but falsely rendered. #165

Closed AvailableForTheWorld closed 6 months ago

AvailableForTheWorld commented 6 months ago

here is my code using vue3:

// index.vue
<template>
  <div>psdddddd!!!!!!!!!!</div>
</template>

<script lang="ts" setup>
import { readPsd } from 'ag-psd'
import MyPsd from '../assets/image/视觉1.psd'
import { onMounted } from 'vue'

function drawLayer(layer, context) {
  if (layer.children) {
    layer.children.forEach(child => drawLayer(child, context))
  }
  if (layer.canvas && !layer.hidden) {
    context.drawImage(layer.canvas, layer.left, layer.top)
  }
}

onMounted(async () => {
  const response = await fetch(MyPsd)
  const arrayBuffer = await response.arrayBuffer()
  const psd = await readPsd(arrayBuffer)
  console.log('psd: ', psd)
  const canvas = document.createElement('canvas')
  canvas.width = psd.width
  canvas.height = psd.height
  const ctx = canvas.getContext('2d')
  psd.children.forEach(layer => {
    drawLayer(layer, ctx)
  })
  document.body.appendChild(canvas)
})
</script>

<style lang="scss" scoped></style>

here is the psd file: psd.zip

and here is the result I render: psd

in this case the problem is that: some layer has mask, I cannot render the mask well for it, here is my render-mask-fix:

function drawLayer(layer, context) {
  if (layer.children && !layer.hidden) {
    layer.children.forEach(child => drawLayer(child, context))
  }
  const compoundCanvas = document.createElement('canvas')
  const compoundCtx = compoundCanvas.getContext('2d')
  if (layer.canvas && !layer.hidden) {
    console.log('layer: ', layer)
    compoundCanvas.width = layer.right - layer.left
    compoundCanvas.height = layer.bottom - layer.top
    compoundCtx.globalAlpha = layer.opacity
    compoundCtx.drawImage(layer.canvas, 0, 0)
    if (layer.mask && !layer.hidden) {
      const offsetX = (layer.canvas.width - layer.mask.canvas.width) / 2
      const offsetY = (layer.canvas.height - layer.mask.canvas.height) / 2
      compoundCtx.globalCompositeOperation = 'destination-in'
      console.log('layer.mask: ', layer)
      compoundCtx.drawImage(layer.mask.canvas, offsetX, offsetY)
    }
    compoundCtx.globalCompositeOperation = 'source-over'
    compoundCtx.globalAlpha = 1
    context.drawImage(compoundCanvas, layer.left, layer.top)
  }
}

the code for mask rendering (the reflection of objects on the table ) (locate in the red circle of the last picture) cannot work, instead the compoundCtx.opacity works well, and the result generated image : psd (3) psd (4)

and I found the wordart on the top of the picture (locate in the blue circle of the last picture) don't show the color due to the effects, I don't know how to render the effects independently, could you please tell me? Thanks a lot.

Agamnentzar commented 6 months ago

I think you have to reset globalAlpha to 1 before drawing the mask, and you're also centering the mask on the layer canvas, instead you should use difference between mask.left and mask.top and layer.left and layer.top to correctly position the mask.

Even better way to do it is to draw the layer.canvas with globalAlpha = 1 but then draw compoundCanvas with globalAlpha = layer.alpha. That way masking will have correct calculation, as the mask is applied before the layer.opacity.

AvailableForTheWorld commented 6 months ago

Thank you for answering me. I try to fix it with the code:

function drawLayer(layer, context) {
  if (layer.children && !layer.hidden) {
    layer.children.forEach(child => drawLayer(child, context))
  }
  const compoundCanvas = document.createElement('canvas')
  const compoundCtx = compoundCanvas.getContext('2d')
  if (layer.canvas && !layer.hidden) {
    compoundCanvas.width = layer.right - layer.left
    compoundCanvas.height = layer.bottom - layer.top
    compoundCtx.globalAlpha = layer.opacity
    compoundCtx.drawImage(layer.canvas, 0, 0)
    if (layer.mask && !layer.hidden) {
      // const offsetX = (layer.canvas.width - layer.mask.canvas.width) / 2
      // const offsetY = (layer.canvas.height - layer.mask.canvas.height) / 2
      const maskX = layer.mask.left - layer.left
      const maskY = layer.mask.top - layer.top
      compoundCtx.globalCompositeOperation = 'destination-in'
      compoundCtx.drawImage(layer.mask.canvas, maskX, maskY)
    }
    // compoundCtx.globalCompositeOperation = 'source-over'
    compoundCtx.globalAlpha = 1
    context.drawImage(compoundCanvas, layer.left, layer.top)
  }
}

but the position of the layer.mask is still not correct and for your advice I do assign the compoundCanvas with globalAlpha (not the context but the compoundCtx instead) but I suppose it works fine and does not impact the mask position. I am so confused of the mask position and don't know how to deal with the effects of psd layer, how can I access the knowledge of them? I'm really glad to receive your message, thanks.

AvailableForTheWorld commented 6 months ago

I have fix it by transforming the mask to alpha channel:

function drawLayer(layer, context) {
  if (layer.children && !layer.hidden) {
    layer.children.forEach(child => drawLayer(child, context))
  }
  const compoundCanvas = document.createElement('canvas')
  const compoundCtx = compoundCanvas.getContext('2d')
  if (layer.canvas && !layer.hidden) {
    compoundCanvas.width = layer.right - layer.left
    compoundCanvas.height = layer.bottom - layer.top
    compoundCtx.globalAlpha = layer.opacity
    compoundCtx.drawImage(layer.canvas, 0, 0)
    if (layer.mask && !layer.hidden) {
      // debugger
      console.log('layer mask: ', layer)
      // const maskX = layer.mask.left - layer.left
      // const maskY = layer.mask.top - layer.top
      // compoundCtx.globalCompositeOperation = 'destination-in'
      // compoundCtx.drawImage(layer.mask.canvas, maskX, maskY)
      // Prepare mask
      const maskCanvas = document.createElement('canvas')
      const maskCtx = maskCanvas.getContext('2d')
      maskCanvas.width = layer.mask.right - layer.mask.left
      maskCanvas.height = layer.mask.bottom - layer.mask.top

      // Draw mask
      maskCtx.drawImage(layer.mask.canvas, 0, 0)

      // Convert grayscale to alpha
      const maskImageData = maskCtx.getImageData(0, 0, maskCanvas.width, maskCanvas.height)
      const data = maskImageData.data
      for (let i = 0; i < data.length; i += 4) {
        // Assuming the mask is grayscale, the R, G, and B values should be approximately equal.
        // The alpha value of each pixel is set based on the average of the RGB values.
        const avg = (data[i] + data[i + 1] + data[i + 2]) / 3
        data[i + 3] = avg // Set alpha channel to the average of R, G, and B
      }
      maskCtx.putImageData(maskImageData, 0, 0)

      // Apply mask
      const maskX = layer.mask.left - layer.left
      const maskY = layer.mask.top - layer.top
      compoundCtx.globalCompositeOperation = 'destination-in'
      compoundCtx.drawImage(maskCanvas, maskX, maskY)
      // document.body.appendChild(layer.mask.canvas)
      // document.body.appendChild(compoundCanvas)
      // document.body.appendChild(layer.canvas)
      // document.body.appendChild(maskCanvas)
    }
    compoundCtx.globalCompositeOperation = 'source-over'
    compoundCtx.globalAlpha = 1
    context.drawImage(compoundCanvas, layer.left, layer.top)
  }
}

but I want to know: how to handle the positionRelativeToLayer attribute in layer.mask. When positionRelativeToLayer is true, I try to reckon it as relative position: left/right/bottom/top but failed to render, I saw no specific info in doc. And I still want to figure out how to handle effects in a layer (except fill color effects). I found that there lack of info to render the dropShadow innerGlow and so on. Could you please tell me?

Agamnentzar commented 6 months ago

Oh you're right, I forgot that mask is by default a grayscale representation instead of alpha channel.

I'm not sure how mask positioning is calculated, I think it should be the same as layer, but maybe positionRelativeToLayer flag has impact on it, you'd have to experiment with it, I think for me it worked when I just used it the same as layer top/left.

Additionally you'll have to take into account how destination-in works, because you want to remove the parts of the image with low alpha channel value on mask image. But the parts that you don't draw the mask on will not get clipped, because the composite operation will not get invoked for those pixels. So you need to manually cut off those parts of the layer bitmap. Alternatively draw the mask to a temporary canvas that's the same size as layer so all pixels will be covered by mask.

All layer affects info is stored in layer.effects property. Unfortunately if you want to replicate them you'd have to research how photoshop is generating them and replicate that code yourself, the PSD file itself doesn't have any more information that what is in layer.effects.

AvailableForTheWorld commented 6 months ago

Thanks a lot for replying me. I asked UI designer for the range of mask, she said the mask contain all the section of the psd file so that I do not need to consider the outside mask section.