@@ -118,6 +118,9 @@ export class AvatarControlComponent implements OnInit, ControlValueAccessor {
118118 /** Состояние загрузки файла */
119119 loading = false ;
120120
121+ /** Исправленное изображение в формате base64 для кроппера */
122+ correctedImageBase64 = "" ;
123+
121124 /**
122125 * Обработчик выбора файла - открывает кроппер
123126 */
@@ -128,8 +131,169 @@ export class AvatarControlComponent implements OnInit, ControlValueAccessor {
128131 return ;
129132 }
130133
131- this . imageChangedEvent = event ;
132- this . showCropperModal = true ;
134+ // Обрабатываем EXIF ориентацию перед открытием кроппера
135+ this . fixImageOrientation ( files [ 0 ] , ( ) => {
136+ // Используем исходное событие, но imageChangedEvent уже содержит исправленное изображение
137+ this . imageChangedEvent = event ;
138+ this . showCropperModal = true ;
139+ } ) ;
140+ }
141+
142+ /**
143+ * Исправляет EXIF ориентацию изображения
144+ * Решает проблему с повернутыми фотографиями со смартфонов
145+ */
146+ private fixImageOrientation ( file : File , onComplete : ( ) => void ) {
147+ const reader = new FileReader ( ) ;
148+
149+ reader . onload = e => {
150+ const img = new Image ( ) ;
151+ img . onload = ( ) => {
152+ // Читаем EXIF данные для определения ориентации
153+ this . getImageOrientation ( file , orientation => {
154+ // Если ориентация нормальная (1), просто используем исходное изображение
155+ if ( orientation === 1 ) {
156+ this . correctedImageBase64 = "" ;
157+ onComplete ( ) ;
158+ return ;
159+ }
160+
161+ // Ротируем изображение на Canvas
162+ const canvas = this . rotateImage ( img , orientation ) ;
163+ this . correctedImageBase64 = canvas . toDataURL ( file . type ) ;
164+ onComplete ( ) ;
165+ } ) ;
166+ } ;
167+ img . src = e . target ?. result as string ;
168+ } ;
169+
170+ reader . readAsDataURL ( file ) ;
171+ }
172+
173+ /**
174+ * Определяет EXIF ориентацию изображения
175+ */
176+ private getImageOrientation ( file : File , onOrientationDetected : ( orientation : number ) => void ) {
177+ const reader = new FileReader ( ) ;
178+
179+ reader . onload = event => {
180+ const view = new DataView ( event . target ?. result as ArrayBuffer ) ;
181+ // Проверяем JPEG маркер
182+ if ( view . byteLength < 2 || view . getUint16 ( 0 ) !== 0xffd8 ) {
183+ onOrientationDetected ( 1 ) ; // Не JPEG, используем нормальную ориентацию
184+ return ;
185+ }
186+
187+ let offset = 2 ;
188+ // Ищем EXIF данные
189+ while ( offset < view . byteLength - 9 ) {
190+ if ( view . getUint16 ( offset ) === 0xffe1 ) {
191+ const length = view . getUint16 ( offset + 2 ) + 2 ;
192+ // Проверяем EXIF идентификатор
193+ if ( view . getUint32 ( offset + 4 ) === 0x45786966 && view . getUint16 ( offset + 8 ) === 0x0000 ) {
194+ const orientation = this . getExifOrientation ( view , offset + 10 ) ;
195+ onOrientationDetected ( orientation ) ;
196+ return ;
197+ }
198+ offset += length ;
199+ } else {
200+ offset += 2 ;
201+ }
202+ }
203+ onOrientationDetected ( 1 ) ; // EXIF не найден, используем нормальную ориентацию
204+ } ;
205+
206+ reader . readAsArrayBuffer ( file ) ;
207+ }
208+
209+ /**
210+ * Извлекает значение ориентации из EXIF данных
211+ */
212+ private getExifOrientation ( view : DataView , offset : number ) : number {
213+ try {
214+ const littleEndian = view . getUint16 ( offset ) === 0x4949 ;
215+ const ifdOffset = view . getUint32 ( offset + 4 , littleEndian ) ;
216+ const entries = view . getUint16 ( offset + ifdOffset , littleEndian ) ;
217+
218+ for ( let i = 0 ; i < entries ; i ++ ) {
219+ const entryOffset = offset + ifdOffset + 2 + i * 12 ;
220+ const tag = view . getUint16 ( entryOffset , littleEndian ) ;
221+ // 0x0112 это тег для ориентации (Orientation tag)
222+ if ( tag === 0x0112 ) {
223+ const value = view . getUint32 ( entryOffset + 8 , littleEndian ) ;
224+ return value > 1 && value <= 8 ? value : 1 ;
225+ }
226+ }
227+ } catch ( e ) {
228+ console . warn ( "Ошибка при чтении EXIF ориентации:" , e ) ;
229+ }
230+ return 1 ;
231+ }
232+
233+ /**
234+ * Ротирует изображение на Canvas в зависимости от EXIF ориентации
235+ */
236+ private rotateImage ( img : HTMLImageElement , orientation : number ) : HTMLCanvasElement {
237+ const canvas = document . createElement ( "canvas" ) ;
238+ const ctx = canvas . getContext ( "2d" ) ;
239+ if ( ! ctx ) {
240+ return canvas ;
241+ }
242+
243+ const [ newWidth , newHeight ] = [ img . width , img . height ] ;
244+
245+ switch ( orientation ) {
246+ case 2 :
247+ canvas . width = newWidth ;
248+ canvas . height = newHeight ;
249+ ctx . scale ( - 1 , 1 ) ;
250+ ctx . drawImage ( img , - newWidth , 0 ) ;
251+ break ;
252+ case 3 :
253+ canvas . width = newWidth ;
254+ canvas . height = newHeight ;
255+ ctx . rotate ( Math . PI ) ;
256+ ctx . drawImage ( img , - newWidth , - newHeight ) ;
257+ break ;
258+ case 4 :
259+ canvas . width = newWidth ;
260+ canvas . height = newHeight ;
261+ ctx . scale ( 1 , - 1 ) ;
262+ ctx . drawImage ( img , 0 , - newHeight ) ;
263+ break ;
264+ case 5 :
265+ canvas . width = newHeight ;
266+ canvas . height = newWidth ;
267+ ctx . rotate ( Math . PI / 2 ) ;
268+ ctx . scale ( - 1 , 1 ) ;
269+ ctx . drawImage ( img , - newHeight , 0 ) ;
270+ break ;
271+ case 6 :
272+ canvas . width = newHeight ;
273+ canvas . height = newWidth ;
274+ ctx . rotate ( Math . PI / 2 ) ;
275+ ctx . drawImage ( img , 0 , - newWidth ) ;
276+ break ;
277+ case 7 :
278+ canvas . width = newHeight ;
279+ canvas . height = newWidth ;
280+ ctx . rotate ( - Math . PI / 2 ) ;
281+ ctx . scale ( - 1 , 1 ) ;
282+ ctx . drawImage ( img , - newHeight , - newWidth ) ;
283+ break ;
284+ case 8 :
285+ canvas . width = newHeight ;
286+ canvas . height = newWidth ;
287+ ctx . rotate ( - Math . PI / 2 ) ;
288+ ctx . drawImage ( img , - newHeight , 0 ) ;
289+ break ;
290+ default :
291+ canvas . width = newWidth ;
292+ canvas . height = newHeight ;
293+ ctx . drawImage ( img , 0 , 0 ) ;
294+ }
295+
296+ return canvas ;
133297 }
134298
135299 /**
0 commit comments