Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integrated support for writing PhotoShop layered PSD files #240

Open
9 of 13 tasks
andersmelander opened this issue Apr 17, 2023 · 30 comments
Open
9 of 13 tasks

Add integrated support for writing PhotoShop layered PSD files #240

andersmelander opened this issue Apr 17, 2023 · 30 comments
Assignees

Comments

@andersmelander
Copy link
Member

andersmelander commented Apr 17, 2023

Based on the PSD Export example from #239 standard functionality should be added for writing PSD files.
The PSD files should be layered when created from TCustomImage32 (with layers) and non-layered when created from TBitmap32.

Tasks

Some of these tasks are already covered by the existing PSD Export example. The functionality simply needs to be refactored into the existing framework.

@andersmelander
Copy link
Member Author

@lamdalili FYI

andersmelander pushed a commit that referenced this issue Apr 17, 2023
…ts.PSD.*

TPsdBuilder and TCustomPsdLayer is now TPhotoshopDocument and TCustomPhotoshopLayer.
PSD read/write functionality has been decoupled from PSD model classes.
Refs #240
@holgerflick
Copy link

holgerflick commented Apr 17, 2023

First: Thank you for all your hard work on this. I recently looked into the new saving/loading mechanism and it is amazing.

I struggle using streams instead of files though. So, with regards to the examples already in the repo and with creating these examples that you are creating for PSD. Is it possible with the new mechanism, to receive "any" stream and load it as a TBitmap32 with Graphics32 without knowing what format it is? Further, if I have a Bitmap32, it would be awesome if the examples would contain something how to write in a certain format to a stream - again, without using files. I always had to look for alternatives to TPicture, for example, as its mechanism is based on file extension which fails with streams and you need to know the concrete file format. Especially, in web times when people can "upload" multiple formats, it would be great if Graphics32 offered a solution. Back in the early 2000s, I used GraphicEx as that determined file formats using the header of a file or stream.

I would be great if the new examples answered these questions as I am sure I am not the only one having these issues. I would gladly participate to create these, but right now, I simply do not know how.

Thank you and I hope you don't mind that I chime in here. But it seemed the right place to comment as you are working on a new format and it could be included at some point in this process.

@andersmelander
Copy link
Member Author

Is it possible with the new mechanism, to receive "any" stream and load it as a TBitmap32 with Graphics32 without knowing what format it is?

Yes; Bitmap.LoadFromStream(Stream) should work. It will use the file signatures to detect the format of the data in the stream.

Further, if I have a Bitmap32, it would be awesome if the examples would contain something how to write in a certain format to a stream - again, without using files.

I think that might be too specialized for the examples, but it's really easy though. If you look in TCustomBitmap32.SaveToFile then you can see how it's done: Find a file format writer based on a file extension and then just use that to write the bitmap to the stream.

I have considered extending the image format registration system to also support registration against mime type. It just doesn't "feel" right to have to use file types when no files are involved.

Thank you and I hope you don't mind that I chime in here.

Not at all. I guess I could enable the repo discussion section. I've never used it.

@holgerflick
Copy link

I think that might be too specialized for the examples, but it's really easy though. If you look in TCustomBitmap32.SaveToFile then you can see how it's done: Find a file format writer based on a file extension and then just use that to write the bitmap to the stream.

Thank you for your reply. I have actually tried finding my way into this and maybe a description why I was not able to proceed can help others. My difficulty was to get a list of all registered writers, readers and their associated file extensions.

I was able to iterate all the writers and readers. You have an excellent architecture there. However, the reference to the interface then did not allow me to "print" the file extension. I guess, I am missing the base interface type of all the writers or readers that feature these attributes. With Delphi 11 code navigation in the state it is in right now, I was not able to find my way to the correct type.

@andersmelander
Copy link
Member Author

andersmelander commented Apr 17, 2023

However, the reference to the interface then did not allow me to "print" the file extension. I guess, I am missing the base interface type of all the writers or readers that feature these attributes.

Once you have a reader or a writer you can query it for the IImageFormatFileInfo interface:

type
  TFileTypes = array of string;

  IImageFormatFileInfo = interface
    ['{EC7037E2-DE93-43A8-AD5D-7BDD91E59E04}']
    function ImageFormatDescription: string;
    function ImageFormatFileTypes: TFileTypes;
  end;

Originally I had IImageFormatReader and IImageFormatWriter inherit from IImageFormatFileInfo but it turned out that for some formats (I forget which ones) it should be possible to stream them but not to load and save from a file.

@lamdalili
Copy link
Contributor

An other task to add to the todo list :
The code for RLE isn't optimal, it starts packing from a sequence of two pixels the correct algo should start from three pixels and more, this impacts the output size and the output needs to be aligned by 2 as indicated in the specification.

Create layered PSD based on TCustomImage32 with layers.

The example shows how to export a TBitmap32 based layer , but how to deal with the others having a simple Paint Hanlder which can't be used to create a temporary bitmap bc the drawing is shifted by the location Top and Left

@lamdalili
Copy link
Contributor

lamdalili commented Apr 17, 2023

Is it possible with the new mechanism, to receive "any" stream and load it as a TBitmap32 with Graphics32 without knowing what format it is?

The initial code was written based on abstract TStream to work on any stream, but then I saw the operation is very slow when using TFileStream so I had to force the use of TMemoryStream.

@andersmelander
Copy link
Member Author

An other task to add to the todo list :
The code for RLE isn't optimal, it starts packing from a sequence of two pixels the correct algo should start from three pixels and more, this impacts the output size and the output needs to be aligned by 2 as indicated in the specification.
Anyway; Task added.

Yes, I noticed that some of the alignment values didn't quite match the specs. This is probably the reason some of the readers complain about the file format but still manage to load the files.

The example shows how to export a TBitmap32 based layer , but how to deal with the others having a simple Paint Hanlder which can't be used to create a temporary bitmap bc the drawing is shifted by the location Top and Left

It should be possible to handle that within the current framework. It's just not built in out-of-the-box and I think it's a reasonable limitation. Very little code would be required to implement it but since it is impossible to cover all cases I don't think we should even try (with the standard functionality I mean).

The initial code was written based on abstract TStream to work on any stream

Holger is talking about the Image Format Adapter framework in general.

I had to force the use of TMemoryStream

Yes, and I removed that and made it work with the generic TStream API. That way you can use any stream type you like. Not just TMemoryStream.

andersmelander pushed a commit that referenced this issue Apr 17, 2023
…ough.

Added PSD Image Format Adapter. TPhotoshopDocument is now assignment compatible with TCustomBitmap32.
Refs #240
andersmelander pushed a commit that referenced this issue Apr 17, 2023
@lamdalili
Copy link
Contributor

PSDPlanarOrder: array[0..PSD_CHANNELS-1] of TColor32Component = (ccRed, ccGreen, ccBlue, ccAlpha);

I'm not sure I understand your code but are you sure changing channels order is supported by all readers ?

I think FILL_RLE is superfluous, the background should be generated in all situations for two reasons

  1. to generate thumbnail image which is its miniature
  2. for simple readers that don't fully support PSD, they just load the background since the operation is pretty easy.

@andersmelander
Copy link
Member Author

I'm not sure I understand your code but are you sure changing channels order is supported by all readers ?

The purpose of that constant is to handle the difference in source channel order on different platforms. For example, on Windows, the channel order inside a TColor32 is BGRA while on Android the channel order is RGBA. Since the channel order in the PSD background is fixed to RGBA we need to handle the potential difference and that is what PSDPlanarOrder does. It maps between the required PSD channel order and the native channel order.

RGBA_FORMAT hasn't been tested very well so there are probably many places in Graphics32 that assume the Windows channel layout.

I think FILL_RLE is superfluous, the background should be generated in all situations

Yes, as far as I can tell from the specs, the background (i.e. the Image Data), isn't optional but I think it's nice that one doesn't have to supply a background source layer. Since it's mandatory how can FILL_RLE be superfluous? FILL_RLE ensures that there is a background image even if one isn't supplied...

  1. to generate thumbnail image which is its miniature

I had that thought too but as far as I can tell, the background must have the size of the image so it cannot be a thumbnail.
Besides, there is a spec for adding an actual thumbnail (in JPEG format if desired) in the file format. It's already on the task list.

for simple readers that don't fully support PSD, they just load the background since the operation is pretty easy.

Sure. I'm not sure what you're arguing here.
The background image is saved if it is supplied. Otherwise, a blank image is saved. What else can we do?

andersmelander pushed a commit that referenced this issue Apr 18, 2023
@andersmelander
Copy link
Member Author

The code for RLE isn't optimal, it starts packing from a sequence of two pixels the correct algo should start from three pixels and more, this impacts the output siz

Fixed. I've completely rewritten the PackBits encoder.

@lamdalili
Copy link
Contributor

Since the channel order in the PSD background is fixed to RGBA we need to handle the potential difference and that is what PSDPlanarOrder does. It maps between the required PSD channel order and the native channel order.

In PSD the IDs of the channels are placed in Layer Record their order fixes the storage order of the channels, in theory it's possible to mix them (ID) as you want this has no effect since each channel has its ID (-1, 0, 1, 2) .

@lamdalili
Copy link
Contributor

Sure. I'm not sure what you're arguing here.
The background image is saved if it is supplied. Otherwise, a blank image is saved. What else can we do?

If you want more compatibilty you should put the final renedring in the image, a simple appli can skip easily to the last section and load the final image decompress it without have to repruduce all complex features.

@andersmelander
Copy link
Member Author

In PSD the IDs of the channels are placed in Layer Record their order fixes the storage order of the channels, in theory it's possible to mix them (ID) as you want this has no effect since each channel has its ID (-1, 0, 1, 2) .

Yes, but the same isn't true for the Image Data (what you call the background):
https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/#50577409_89817

The last section of a Photoshop file contains the image pixel data. Image data is stored in planar order: first all the red data, then all the green data, etc. Each plane is stored in scan-line order, with no pad bytes,


If you want more compatibilty you should put the final renedring in the image, a simple appli can skip easily to the last section and load the final image decompress it without have to repruduce all complex features.

If a background layer/bitmap is included in TPhotoshopDocument then it is written to the PSD. The standard TCustomImage32 to TPhotoshopDocument converter does include a background generated from a composite of all the bitmap layers. Just like in your original example.

If you are arguing that the background should somehow be mandatory in the PSD object model layer (TPhotoshopDocument and friends) then I see no reason for that. If you want a background then supply one. If you don't want a background then don't. Also, it isn't the responsibility of the PSD object model layer to render stuff. It's just an object model.

andersmelander pushed a commit that referenced this issue Apr 19, 2023
Moved big endian I/O write functions to GR32.BigEndian, added corresponding read functions and wrapped them all in a record type for scope.
Wrapped PSD compression functions in a record type for scope.
Added tile rect view to psd_bitmap_export  example.
Refs #240
Refs #241
andersmelander pushed a commit that referenced this issue Apr 19, 2023
@andersmelander
Copy link
Member Author

I think we're just about "feature complete" with regard to PSD write support. The remaining tasks are nice-to-haves that can be implemented at a later stage.

I will create a pull request for the PSD branch but I will leave this issue open until the remaining tasks have been implemented or rejected. I expect to merge the PR in a few days unless there are objections.

@lamdalili
Copy link
Contributor

lamdalili commented Apr 20, 2023

(what you call the background):

Photoshop app uses that name , if you think it's wrong to call it so, you're free to correct it.

Fixed. I've completely rewritten the PackBits encoder.

I've done some tests with this new code, and compared with previous:

I removed the jpg image from the test since compressed to avoid confusion
the generated PSD file with no compression gives size of 1.3 M.
the new code PackBits gives 838 k.
previous algo gives 78 k.

@andersmelander
Copy link
Member Author

Photoshop app uses that name , if you think it's wrong to call it so, you're free to correct it.

No, it's fine. I do think it's pretty misleading but I don't really have a better term to use. FWIW, it's called the Image Data in the specs.

the new code PackBits gives 838 k.
previous algo gives 78 k.

That's strange. Theoretically, it should compress better. I did a lot of tests and never once saw the size increase.
Anyway, I'll look into it.

@lamdalili
Copy link
Contributor

lamdalili commented Apr 23, 2023

Potentiel bug in TImage32.PaintTo in the code of background generation ,

AImage.PaintTo(BackgroundBitmap, BackgroundBitmap.BoundsRect);

As I know transparency tiles are painted to mark presence of alpha channel, and reseved for purly internal use and shouldn't be painted in the external Bitmap32 :

I think it's important to disable dmBlend DrawMode in TImage32.Bitmap before exportation

TransTiles

@andersmelander
Copy link
Member Author

Potentiel bug in TImage32.PaintTo in the code of background generation ,

Moved to #247

andersmelander pushed a commit that referenced this issue Apr 23, 2023
@andersmelander
Copy link
Member Author

I'll merge the PR to master tomorrow evening (Tuesday, CET) unless there are objections.

@lamdalili
Copy link
Contributor

lamdalili commented Apr 25, 2023

The current CreatePhotoshopDocument needs revision, TImage32.Bitmap may be lost if containing valid image, currenty it's exported with background this can work with no layred TImage32 otherwise it should be exported in separate layer.

@andersmelander
Copy link
Member Author

Yes, that makes sense.

@andersmelander
Copy link
Member Author

Fixed.
Unfortunately, this means that we now create a layer with a fully transparent "empty" bitmap if the bitmap is "empty". I thought about scanning the bitmap to determine if it was "empty" and skipping it if so, but there's no way of knowing if the user actually want the empty layer or not. It's very easy to delete the empty layer after the PSD document has been created so I don't think it's worth it to try to be too clever.

@lamdalili
Copy link
Contributor

I thought about scanning the bitmap to determine if it was "empty" and skipping it if so

The problem is that PhotoShop ignores alpha channel in the background and displays it as solid image , the scan need also detect presence of any blending value (alpha <> 255) if so the Bitmap should be exported as PSD layer regardless of the Image32 is layred or not.

@andersmelander
Copy link
Member Author

the Bitmap should be exported as PSD layer regardless of the Image32 is layred or not.

Yes. That's how I've implemented it now.

It turned out that Photopea ignored the background even if the image didn't contain layers; An image with a background but no layers just displays as an empty, black bitmap.

@holgerflick
Copy link

holgerflick commented Apr 29, 2023

Once you have a reader or a writer you can query it for the IImageFormatFileInfo interface:

That works great! I also check if the file format that I enumerate supports it because not all do.
Further, is Photoshop not included in the master branch yet or why do I only get this list:

Bitmaps
bmp
dib
rle
----
PNG images
png
----
Icons
ico
----
Metafiles
wmf
emf
----
TIFF Images
tif
tiff
----

Compiler optimization because I do not use any of the classes?! Because JPEG is also missing.

@andersmelander
Copy link
Member Author

Further, is Photoshop not included in the master branch yet

Yes. It got merged a few days ago.
Note though that we're limited to PSD write-support for the moment. There's no PSD reader yet. I have some code that uses a modified version of the PSD reader in the GraphicEx library but it's not ready for release yet; Basically, I need to rip the code out of the GraphicEx unit and remove/replace the dependencies.

why do I only get this list

Only the ones you listed are included by default (see the GR32.ImageFormats.Default unit). This is done to avoid cluttering the file format list with whatever exotic formats we might add in the future - and to avoid linking in unwanted code.

To include more formats just add their units to a uses somewhere in your project.

@holgerflick
Copy link

To include more formats just add their units to a uses somewhere in your project.

Just thinking out loud here... could we create a unit that contains uses for all available file formats? Kinda what happens when you drop a certain component on the form with FireDAC that just adds units into the binary. So, we could add that unit and all file formats would be included. Obviously, I could create that myself for my project as well, but this way it would be part of GR32.

@andersmelander
Copy link
Member Author

could we create a unit that contains uses for all available file formats?

Yeah, well, I'm not too thrilled about that idea.
Because I've designed it so there are no dependencies between the individual file format units and the file format manager, there would be no guarantee that all file formats were included. We could only reference the units that were known at the time the "include all file formats" unit was written/updated. In other words: It wouldn't necessarily contain all available file formats and then what's the point?

I'm also not seeing a good use case for this. Personally, I would prefer to have fewer file formats included by default than is the case now. For example how often do you need to load a TIFF file nowadays? And what about wmf, emf, ico, rle and dib? Unless you're writing an image editor I don't really think there's much need for these or that the typical end-user even knows what they are.

Finally, the VCL TGraphic/TPicture format registration works the same way; You need to reference the image format unit to have them included.

@holgerflick
Copy link

It wouldn't necessarily contain all available file formats and then what's the point?

I was just seeing the consumer side too much. I agree it is fine if somebody wants to "use all", they can just add all the units. No argument here. I stand corrected.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants