1+ <?php namespace Le \ImgMan ;
2+
3+ use GdImage ;
4+ use Throwable , Exception ;
5+
6+ class ImgMan
7+ {
8+
9+ private string $ string ;
10+ private string $ dataUrl ;
11+ private string $ filename ;
12+
13+ private array $ exif ;
14+
15+ private ImageFormat $ outputFormat ;
16+ private ?int $ outputQuality ;
17+
18+ /**
19+ * @param GdImage|null $image optionally provide an image object to be
20+ * manipulated with instead of using the from methods
21+ */
22+ public function __construct (
23+ private ?GdImage $ image = null
24+ )
25+ {
26+ }
27+
28+ /**
29+ * @param string $string raw image byte stream
30+ * @return $this a new instance
31+ */
32+ public function fromString (string $ string ): static
33+ {
34+ $ instance = new static ;
35+ $ instance ->string = $ string ;
36+ return $ instance ;
37+ }
38+
39+ /**
40+ * @param string $dataUrl data URL or data URI that can be provided
41+ * by FileReader method readAsDataURL in Javascript
42+ * @return $this a new instance
43+ */
44+ public function fromDataUrl (string $ dataUrl ): static
45+ {
46+ $ instance = new static ;
47+ $ instance ->dataUrl = $ dataUrl ;
48+ return $ instance ;
49+
50+ }
51+
52+ /**
53+ * @param string $filename relative path to the image file
54+ * @return $this a new instance
55+ */
56+ public function fromFile (string $ filename ): static
57+ {
58+ $ instance = new static ;
59+ $ instance ->filename = $ filename ;
60+ return $ instance ;
61+ }
62+
63+ /**
64+ * reads exif data (if it exists) from the provided source
65+ * if used, this method must be called before any manipulation methods
66+ *
67+ * @throws Exception if object is not initialized or has already been manipulated
68+ * @return $this
69+ */
70+ public function cacheExif (): static
71+ {
72+ if (!isset ($ this ->dataUrl ) && !isset ($ this ->filename ) && !isset ($ this ->string ))
73+ throw new Exception ('original source missing ' );
74+
75+ $ prefix = 'data://image/jpeg;base64, ' ; // because only jpegs can have exif, right?
76+
77+ try {
78+ $ this ->exif = match (true ) {
79+ isset ($ this ->dataUrl ) => exif_read_data ($ this ->dataUrl ),
80+ isset ($ this ->filename ) => exif_read_data ($ this ->filename ),
81+ isset ($ this ->string ) => exif_read_data ($ prefix . base64_encode ($ this ->string ))
82+ };
83+ }
84+ catch (Throwable ){ $ this ->exif = []; }
85+
86+ return $ this ;
87+ }
88+
89+ /**
90+ * rotates the image to match the exif data (if existent or cached)
91+ *
92+ * recommended way to use is after downscaling the image so less memory usage occurs
93+ * to achieve this use cacheExif() method directly after providing the image source
94+ *
95+ * @throws Exception if object is not initialized, use one of the "from" methods first
96+ * or if the image format is not supported or damaged
97+ * @return $this
98+ */
99+ public function rotateFromExif (): static
100+ {
101+ if (!isset ($ this ->exif )) $ this ->cacheExif ();
102+ if (empty ($ this ->exif ['Orientation ' ])) return $ this ;
103+
104+ $ image = match ($ this ->exif ['Orientation ' ]){
105+ 3 , 4 => imagerotate ($ this ->getImage (), 180 , 0 ),
106+ 5 , 6 => imagerotate ($ this ->getImage (), 270 , 0 ),
107+ 7 , 8 => imagerotate ($ this ->getImage (), 90 , 0 ),
108+ default => $ this ->getImage ()
109+ };
110+
111+ if (in_array ($ this ->exif ['Orientation ' ], [4 , 5 , 7 ]))
112+ imageflip ($ image , IMG_FLIP_HORIZONTAL );
113+
114+ $ this ->image = $ image ;
115+ return $ this ;
116+ }
117+
118+
119+ /**
120+ * downscales the image while preserving aspect ratio
121+ *
122+ * if image is smaller than provided max dimensions, no downscaling will occur
123+ *
124+ * @param int $maxWidth new image width
125+ * @param int|null $maxHeight new image height, optional: will be equal to width if not provided
126+ * @throws Exception if object is not initialized, use one of the "from" methods first
127+ * or if the image format is not supported or damaged
128+ * @return $this
129+ */
130+ public function downscale (int $ maxWidth , ?int $ maxHeight = null ): static
131+ {
132+ if ($ maxHeight === null ) $ maxHeight = $ maxWidth ;
133+
134+ $ this ->image = $ this ->getImage ();
135+ $ x = imagesx ($ this ->image );
136+ $ y = imagesy ($ this ->image );
137+ $ ratio = $ x / $ y ;
138+
139+ if ($ x <= $ maxWidth && $ y <= $ maxHeight )
140+ return $ this ; // image is already within limits
141+
142+ if ( $ ratio > 1 ) { // landscape
143+ $ width = $ maxWidth ;
144+ $ height = $ width /$ ratio ;
145+ }
146+ else { // portrait
147+ $ height = $ maxHeight ;
148+ $ width = $ height *$ ratio ;
149+ }
150+
151+ $ scaledImage = imagecreatetruecolor ($ width , $ height );
152+ imagecopyresampled ($ scaledImage , $ this ->image , 0 , 0 , 0 , 0 , $ width , $ height , $ x , $ y );
153+ $ this ->image = $ scaledImage ;
154+
155+ return $ this ;
156+ }
157+
158+ /**
159+ * @param ImageFormat $format specify the output format that will be used
160+ * @param int|null $quality this parameter will be passed to the image____() function's quality parameter
161+ * @return $this
162+ */
163+ public function output (ImageFormat $ format , ?int $ quality = null ): static
164+ {
165+ $ this ->outputFormat = $ format ;
166+ $ this ->outputQuality = $ quality ;
167+
168+ return $ this ;
169+ }
170+
171+ /**
172+ * @param string|null $filename image will be written to this file;
173+ * if you wish to overwrite the existing image,
174+ * leave this parameter out (only if initialized using
175+ * the fromFile() method)
176+ * @throws Exception if no output method was specified using the output() method
177+ */
178+ public function toFile (?string $ filename = null ): void
179+ {
180+ if (is_null ($ filename )) $ filename = $ this ->filename ;
181+ $ this ->toFormat ($ filename );
182+ }
183+
184+ /**
185+ * @return string raw image bytes
186+ * @throws Exception if no output method was specified using the output() method
187+ * or the image was not initialized, or is not supported or damaged
188+ */
189+ public function toString (): string
190+ {
191+ return $ this ->toFormat ();
192+ }
193+
194+ /**
195+ * @return string data URL or data URI
196+ * @throws Exception if no output method was specified using the output() method
197+ * or the image was not initialized, or is not supported or damaged
198+ */
199+ public function toDataUrl (): string
200+ {
201+ $ prefix = 'data:// ' . $ this ->outputFormat ->value . ';base64, ' ;
202+ return $ prefix . base64_encode ($ this ->toFormat ());
203+ }
204+
205+ /**
206+ * @throws Exception
207+ */
208+ private function getImage (): GdImage
209+ {
210+ if (isset ($ this ->image )) return $ this ->image ;
211+
212+ if (!isset ($ this ->dataUrl ) && !isset ($ this ->filename ) && !isset ($ this ->string ))
213+ throw new Exception ('object not initialized ' );
214+
215+
216+ if (isset ($ this ->string ))
217+ $ image = @imagecreatefromstring ($ this ->string );
218+ else
219+ if (isset ($ this ->dataUrl ))
220+ $ image = @imagecreatefromstring (
221+ base64_decode (
222+ explode (', ' , $ this ->dataUrl , 2 )[1 ]
223+ )
224+ );
225+ else {
226+ $ image = match (getimagesize ($ this ->filename )['mime ' ]) {
227+ 'image/bmp ' => @imagecreatefrombmp ($ this ->filename ),
228+ 'image/gif ' => @imagecreatefromgif ($ this ->filename ),
229+ 'image/png ' => @imagecreatefrompng ($ this ->filename ),
230+ 'image/jpeg ' => @imagecreatefromjpeg ($ this ->filename ),
231+ 'image/webp ' => @imagecreatefromwebp ($ this ->filename ),
232+ default => throw new Exception ('not a supported image file extension ' )
233+ };
234+ }
235+
236+ if ($ image === false )
237+ throw new Exception ('unsuccessful image loading - could be damaged or an unsupported format ' );
238+
239+ unset($ this ->string );
240+ unset($ this ->dataUrl );
241+
242+ return $ image ;
243+ }
244+
245+ /**
246+ * @throws Exception
247+ */
248+ private function toFormat (string $ filename = null ): string
249+ {
250+ if (!isset ($ this ->outputFormat )) throw new Exception ('no output format specified (output() method) ' );
251+
252+ ob_start ();
253+ match ($ this ->outputFormat ){
254+ ImageFormat::jpeg => imagejpeg ($ this ->getImage (), $ filename , $ this ->outputQuality ),
255+ ImageFormat::png => imagepng ($ this ->getImage (), $ filename , $ this ->outputQuality ?? -1 ),
256+ ImageFormat::webp => imagewebp ($ this ->getImage (), $ filename , $ this ->outputQuality )
257+ };
258+ return ob_get_clean ();
259+ }
260+
261+
262+
263+ }
0 commit comments