Compare commits
	
		
			433 Commits
		
	
	
		
			b868e76a15
			...
			v1.0.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						3bea18708f
	
				 | 
					
					
						|||
| 
						
						
							
						
						ed8ba147ba
	
				 | 
					
					
						|||
| 
						
						
							
						
						c221a00c33
	
				 | 
					
					
						|||
| 
						
						
							
						
						192ffa0de9
	
				 | 
					
					
						|||
| 
						
						
							
						
						bac9fce4e0
	
				 | 
					
					
						|||
| 
						
						
							
						
						2e9ea9b4e2
	
				 | 
					
					
						|||
| 
						
						
							
						
						b34fe63198
	
				 | 
					
					
						|||
| 
						
						
							
						
						3c8ddcaf26
	
				 | 
					
					
						|||
| 
						
						
							
						
						e3ec07a19f
	
				 | 
					
					
						|||
| 
						
						
							
						
						e57364cd97
	
				 | 
					
					
						|||
| 
						
						
							
						
						7330f07dd7
	
				 | 
					
					
						|||
| 
						
						
							
						
						d68e09525c
	
				 | 
					
					
						|||
| 
						
						
							
						
						115a7bab0f
	
				 | 
					
					
						|||
| 
						
						
							
						
						91538aaba5
	
				 | 
					
					
						|||
| 
						
						
							
						
						c214e668d9
	
				 | 
					
					
						|||
| 
						
						
							
						
						a5ebc697ad
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ca18f52d5
	
				 | 
					
					
						|||
| 
						
						
							
						
						604594a8f1
	
				 | 
					
					
						|||
| 
						
						
							
						
						9acab00bcc
	
				 | 
					
					
						|||
| 
						
						
							
						
						ae8dc3070a
	
				 | 
					
					
						|||
| 
						
						
							
						
						3c8a280546
	
				 | 
					
					
						|||
| 
						
						
							
						
						96189b70b8
	
				 | 
					
					
						|||
| 
						
						
							
						
						67433f3776
	
				 | 
					
					
						|||
| 
						
						
							
						
						c1418c7462
	
				 | 
					
					
						|||
| 
						
						
							
						
						935506b120
	
				 | 
					
					
						|||
| 
						
						
							
						
						84269b2ba2
	
				 | 
					
					
						|||
| 
						
						
							
						
						51ca3f8e2e
	
				 | 
					
					
						|||
| 
						
						
							
						
						f196b03e97
	
				 | 
					
					
						|||
| 
						
						
							
						
						ee08565389
	
				 | 
					
					
						|||
| 
						
						
							
						
						c04c4063e4
	
				 | 
					
					
						|||
| 
						
						
							
						
						aed6ae6b83
	
				 | 
					
					
						|||
| 
						
						
							
						
						bae640a116
	
				 | 
					
					
						|||
| 
						
						
							
						
						52c17c8a16
	
				 | 
					
					
						|||
| 
						
						
							
						
						b07fba0c9c
	
				 | 
					
					
						|||
| 
						
						
							
						
						72bf913f3d
	
				 | 
					
					
						|||
| 
						
						
							
						
						e79574fd56
	
				 | 
					
					
						|||
| 
						
						
							
						
						93ad75eb35
	
				 | 
					
					
						|||
| 
						
						
							
						
						2d10aa8b61
	
				 | 
					
					
						|||
| 
						
						
							
						
						1ec41f7749
	
				 | 
					
					
						|||
| 
						
						
							
						
						d4b91d6260
	
				 | 
					
					
						|||
| 
						
						
							
						
						5ec5f5bdbd
	
				 | 
					
					
						|||
| 
						
						
							
						
						840e7f172c
	
				 | 
					
					
						|||
| 
						
						
							
						
						9b99de99bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						ab75d2b61d
	
				 | 
					
					
						|||
| 
						
						
							
						
						92deba3890
	
				 | 
					
					
						|||
| 
						
						
							
						
						668c5eb78a
	
				 | 
					
					
						|||
| 
						
						
							
						
						d713d5820c
	
				 | 
					
					
						|||
| 
						
						
							
						
						f05e66bfc1
	
				 | 
					
					
						|||
| 
						
						
							
						
						6ee5f69bfe
	
				 | 
					
					
						|||
| 
						
						
							
						
						4249898497
	
				 | 
					
					
						|||
| 
						
						
							
						
						117422ade5
	
				 | 
					
					
						|||
| 
						
						
							
						
						8ff33e6b63
	
				 | 
					
					
						|||
| 
						
						
							
						
						ce4a13ed38
	
				 | 
					
					
						|||
| 
						
						
							
						
						6a1b851130
	
				 | 
					
					
						|||
| 
						
						
							
						
						68245b55c9
	
				 | 
					
					
						|||
| 
						
						
							
						
						2869c656c1
	
				 | 
					
					
						|||
| 
						
						
							
						
						ec713b633e
	
				 | 
					
					
						|||
| 
						
						
							
						
						88234f8283
	
				 | 
					
					
						|||
| 
						
						
							
						
						49ee551b9b
	
				 | 
					
					
						|||
| 
						
						
							
						
						089c90004b
	
				 | 
					
					
						|||
| 
						
						
							
						
						19913a5e48
	
				 | 
					
					
						|||
| 
						
						
							
						
						1ef0a84bc7
	
				 | 
					
					
						|||
| 
						
						
							
						
						4b5b8ec9fa
	
				 | 
					
					
						|||
| 
						
						
							
						
						3449ac5a12
	
				 | 
					
					
						|||
| 
						
						
							
						
						bbfa2344d6
	
				 | 
					
					
						|||
| 
						
						
							
						
						2ff853b7e0
	
				 | 
					
					
						|||
| 
						
						
							
						
						bb4d3acd12
	
				 | 
					
					
						|||
| 
						
						
							
						
						074bd4d37f
	
				 | 
					
					
						|||
| 
						
						
							
						
						add96b37a6
	
				 | 
					
					
						|||
| 
						
						
							
						
						c2e8b65d0f
	
				 | 
					
					
						|||
| 
						
						
							
						
						4f57070e27
	
				 | 
					
					
						|||
| 
						
						
							
						
						2dc4e9c13b
	
				 | 
					
					
						|||
| 
						
						
							
						
						a1f6ffd226
	
				 | 
					
					
						|||
| 
						
						
							
						
						1eee1831a5
	
				 | 
					
					
						|||
| 
						
						
							
						
						86622e0c31
	
				 | 
					
					
						|||
| 
						
						
							
						
						a4772ce319
	
				 | 
					
					
						|||
| 
						
						
							
						
						0318424540
	
				 | 
					
					
						|||
| 
						
						
							
						
						8d5885bfdf
	
				 | 
					
					
						|||
| 
						
						
							
						
						41b5ddc744
	
				 | 
					
					
						|||
| 
						
						
							
						
						b308b5da18
	
				 | 
					
					
						|||
| 
						
						
							
						
						1577961aa2
	
				 | 
					
					
						|||
| 
						
						
							
						
						1fb42e689f
	
				 | 
					
					
						|||
| 
						
						
							
						
						8953e6beea
	
				 | 
					
					
						|||
| 
						
						
							
						
						2e8bbf0e43
	
				 | 
					
					
						|||
| 
						
						
							
						
						07d4ea2dde
	
				 | 
					
					
						|||
| 
						
						
							
						
						a5b5e32c3b
	
				 | 
					
					
						|||
| 
						
						
							
						
						1e8fe1411b
	
				 | 
					
					
						|||
| 
						
						
							
						
						274c5f6f66
	
				 | 
					
					
						|||
| 
						
						
							
						
						de377d3eae
	
				 | 
					
					
						|||
| 
						
						
							
						
						34388b93ea
	
				 | 
					
					
						|||
| 
						
						
							
						
						7dda3bd1ed
	
				 | 
					
					
						|||
| 
						
						
							
						
						a3a5eb33cf
	
				 | 
					
					
						|||
| 
						
						
							
						
						ee202ca28b
	
				 | 
					
					
						|||
| 
						
						
							
						
						04db6ed6a1
	
				 | 
					
					
						|||
| 
						
						
							
						
						d6e79cf976
	
				 | 
					
					
						|||
| 
						
						
							
						
						6cc4ca1f44
	
				 | 
					
					
						|||
| 
						
						
							
						
						1c25cb411f
	
				 | 
					
					
						|||
| 
						
						
							
						
						399c4bdf69
	
				 | 
					
					
						|||
| 
						
						
							
						
						a9b34ca3f2
	
				 | 
					
					
						|||
| 
						
						
							
						
						bd92ad73ec
	
				 | 
					
					
						|||
| 
						
						
							
						
						b3bc481172
	
				 | 
					
					
						|||
| 
						
						
							
						
						a3745df84b
	
				 | 
					
					
						|||
| 
						
						
							
						
						cc59e537da
	
				 | 
					
					
						|||
| 
						
						
							
						
						338ae69121
	
				 | 
					
					
						|||
| 
						
						
							
						
						1c61fcc5bc
	
				 | 
					
					
						|||
| 
						
						
							
						
						dd1d6647dc
	
				 | 
					
					
						|||
| 
						
						
							
						
						abf4f1a792
	
				 | 
					
					
						|||
| 
						
						
							
						
						6a7c86a41b
	
				 | 
					
					
						|||
| 
						
						
							
						
						6277a32fe6
	
				 | 
					
					
						|||
| 
						
						
							
						
						8f0576d6bc
	
				 | 
					
					
						|||
| 
						
						
							
						
						f56c40cf00
	
				 | 
					
					
						|||
| 
						
						
							
						
						28a1237d62
	
				 | 
					
					
						|||
| 
						
						
							
						
						4c8df56193
	
				 | 
					
					
						|||
| 
						
						
							
						
						200485246b
	
				 | 
					
					
						|||
| 
						
						
							
						
						2caebb7d19
	
				 | 
					
					
						|||
| 
						
						
							
						
						9d9d538fe6
	
				 | 
					
					
						|||
| 
						
						
							
						
						3bdffd03db
	
				 | 
					
					
						|||
| 
						
						
							
						
						a710692725
	
				 | 
					
					
						|||
| 
						
						
							
						
						859736e5be
	
				 | 
					
					
						|||
| 
						
						
							
						
						d5b2e43364
	
				 | 
					
					
						|||
| 
						
						
							
						
						33251eaca7
	
				 | 
					
					
						|||
| 
						
						
							
						
						63311644da
	
				 | 
					
					
						|||
| 
						
						
							
						
						8668e85623
	
				 | 
					
					
						|||
| 
						
						
							
						
						902eaf5a01
	
				 | 
					
					
						|||
| 
						
						
							
						
						df7c7b9f6b
	
				 | 
					
					
						|||
| 
						
						
							
						
						bb4b895cb5
	
				 | 
					
					
						|||
| 
						
						
							
						
						0f1c61ae33
	
				 | 
					
					
						|||
| 
						
						
							
						
						0359ddf99f
	
				 | 
					
					
						|||
| 
						
						
							
						
						a93fc3f88d
	
				 | 
					
					
						|||
| 
						
						
							
						
						d70aedffa8
	
				 | 
					
					
						|||
| 
						
						
							
						
						dba728e0c4
	
				 | 
					
					
						|||
| 
						
						
							
						
						544722f8e0
	
				 | 
					
					
						|||
| 
						
						
							
						
						00110a639a
	
				 | 
					
					
						|||
| 
						
						
							
						
						5af36f4954
	
				 | 
					
					
						|||
| 
						
						
							
						
						ac72a72afc
	
				 | 
					
					
						|||
| 
						
						
							
						
						a6560509d9
	
				 | 
					
					
						|||
| 
						
						
							
						
						44c28f00d6
	
				 | 
					
					
						|||
| 
						
						
							
						
						cce2b6ba51
	
				 | 
					
					
						|||
| 
						
						
							
						
						43363ea4bf
	
				 | 
					
					
						|||
| 
						
						
							
						
						00fa76cb69
	
				 | 
					
					
						|||
| 
						
						
							
						
						5e10f0ba54
	
				 | 
					
					
						|||
| 
						
						
							
						
						eb44b6fb91
	
				 | 
					
					
						|||
| 
						
						
							
						
						a012011631
	
				 | 
					
					
						|||
| 
						
						
							
						
						05ac3a0651
	
				 | 
					
					
						|||
| 
						
						
							
						
						4317c7e581
	
				 | 
					
					
						|||
| 
						
						
							
						
						8da5f807cf
	
				 | 
					
					
						|||
| 
						
						
							
						
						1b50a834a5
	
				 | 
					
					
						|||
| 
						
						
							
						
						38c19edc8b
	
				 | 
					
					
						|||
| 
						
						
							
						
						c646864805
	
				 | 
					
					
						|||
| 
						
						
							
						
						c2196df141
	
				 | 
					
					
						|||
| 
						
						
							
						
						44f2f5d4f5
	
				 | 
					
					
						|||
| 
						
						
							
						
						25c91f5a77
	
				 | 
					
					
						|||
| 
						
						
							
						
						796b05c9a5
	
				 | 
					
					
						|||
| 
						
						
							
						
						9286858573
	
				 | 
					
					
						|||
| 
						
						
							
						
						95bc91e020
	
				 | 
					
					
						|||
| 
						
						
							
						
						1754bbcf45
	
				 | 
					
					
						|||
| 
						
						
							
						
						62d82f38c8
	
				 | 
					
					
						|||
| 
						
						
							
						
						c063d93dc7
	
				 | 
					
					
						|||
| 
						
						
							
						
						0317b88c87
	
				 | 
					
					
						|||
| 
						
						
							
						
						1431188e27
	
				 | 
					
					
						|||
| 
						
						
							
						
						9cdc641b0a
	
				 | 
					
					
						|||
| 
						
						
							
						
						7b88e89489
	
				 | 
					
					
						|||
| 
						
						
							
						
						9c2d659d89
	
				 | 
					
					
						|||
| 
						
						
							
						
						9fb90607ad
	
				 | 
					
					
						|||
| 
						
						
							
						
						dd09af34b7
	
				 | 
					
					
						|||
| 
						
						
							
						
						dcbc8a90b4
	
				 | 
					
					
						|||
| 
						
						
							
						
						7cbd24dd2f
	
				 | 
					
					
						|||
| 
						
						
							
						
						1a163bdb8b
	
				 | 
					
					
						|||
| 
						
						
							
						
						cf19f82875
	
				 | 
					
					
						|||
| 
						
						
							
						
						9c9453172a
	
				 | 
					
					
						|||
| 
						
						
							
						
						973c024abe
	
				 | 
					
					
						|||
| 
						
						
							
						
						dba2b9c982
	
				 | 
					
					
						|||
| 
						
						
							
						
						b6e1dc4893
	
				 | 
					
					
						|||
| 
						
						
							
						
						f94550ec61
	
				 | 
					
					
						|||
| 
						
						
							
						
						69d45fea44
	
				 | 
					
					
						|||
| 
						
						
							
						
						4697a56760
	
				 | 
					
					
						|||
| 
						
						
							
						
						4e11970a7e
	
				 | 
					
					
						|||
| 
						
						
							
						
						ae0b5506ab
	
				 | 
					
					
						|||
| 
						
						
							
						
						701846ab39
	
				 | 
					
					
						|||
| 
						
						
							
						
						4927c8c692
	
				 | 
					
					
						|||
| 
						
						
							
						
						e6341e59bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						33fb047a73
	
				 | 
					
					
						|||
| 
						
						
							
						
						215141856a
	
				 | 
					
					
						|||
| 
						
						
							
						
						086dd66aa9
	
				 | 
					
					
						|||
| 
						
						
							
						
						8c6fe0ad32
	
				 | 
					
					
						|||
| 
						
						
							
						
						ca51c9413b
	
				 | 
					
					
						|||
| 
						
						
							
						
						857917aa92
	
				 | 
					
					
						|||
| 
						
						
							
						
						51dc56c9df
	
				 | 
					
					
						|||
| 
						
						
							
						
						bb669743b6
	
				 | 
					
					
						|||
| 
						
						
							
						
						d590d1da46
	
				 | 
					
					
						|||
| 
						
						
							
						
						d1d9caaa5e
	
				 | 
					
					
						|||
| 
						
						
							
						
						5bae7c1bd2
	
				 | 
					
					
						|||
| 
						
						
							
						
						6f83d1dceb
	
				 | 
					
					
						|||
| 
						
						
							
						
						ab70b30053
	
				 | 
					
					
						|||
| 
						
						
							
						
						ea75579b33
	
				 | 
					
					
						|||
| 
						
						
							
						
						8437164dec
	
				 | 
					
					
						|||
| 
						
						
							
						
						fec64d5595
	
				 | 
					
					
						|||
| 
						
						
							
						
						fefb4c16ac
	
				 | 
					
					
						|||
| 
						
						
							
						
						6baf1a7bbd
	
				 | 
					
					
						|||
| 
						
						
							
						
						78636fdc18
	
				 | 
					
					
						|||
| 
						
						
							
						
						e18f729488
	
				 | 
					
					
						|||
| 
						
						
							
						
						9f1041988d
	
				 | 
					
					
						|||
| 
						
						
							
						
						fa034a1a6a
	
				 | 
					
					
						|||
| 
						
						
							
						
						dcc5b6c719
	
				 | 
					
					
						|||
| 
						
						
							
						
						eca319e5e4
	
				 | 
					
					
						|||
| 
						
						
							
						
						817f1b6000
	
				 | 
					
					
						|||
| 
						
						
							
						
						31f428f4ec
	
				 | 
					
					
						|||
| 
						
						
							
						
						4b4e24e71a
	
				 | 
					
					
						|||
| 
						
						
							
						
						4e84d6a802
	
				 | 
					
					
						|||
| 
						
						
							
						
						f94171fcf2
	
				 | 
					
					
						|||
| 
						
						
							
						
						4131a926f2
	
				 | 
					
					
						|||
| 
						
						
							
						
						4fcc506d84
	
				 | 
					
					
						|||
| 
						
						
							
						
						5529727137
	
				 | 
					
					
						|||
| 
						
						
							
						
						e137afa736
	
				 | 
					
					
						|||
| 
						
						
							
						
						891420edfd
	
				 | 
					
					
						|||
| 
						
						
							
						
						0bfd3ad4ce
	
				 | 
					
					
						|||
| 
						
						
							
						
						e03bc36f63
	
				 | 
					
					
						|||
| 
						
						
							
						
						d3b34cd482
	
				 | 
					
					
						|||
| 
						
						
							
						
						b067c1948b
	
				 | 
					
					
						|||
| 
						
						
							
						
						390e21a72d
	
				 | 
					
					
						|||
| 
						
						
							
						
						876fda4f55
	
				 | 
					
					
						|||
| 
						
						
							
						
						60b2395940
	
				 | 
					
					
						|||
| 
						
						
							
						
						e7c75f8f9b
	
				 | 
					
					
						|||
| 
						
						
							
						
						a9a9d69a92
	
				 | 
					
					
						|||
| 
						
						
							
						
						26dead7ea4
	
				 | 
					
					
						|||
| 
						
						
							
						
						94f6938b9a
	
				 | 
					
					
						|||
| 
						
						
							
						
						62b1e83541
	
				 | 
					
					
						|||
| 
						
						
							
						
						bd2e929b77
	
				 | 
					
					
						|||
| 
						
						
							
						
						9a0647fdfd
	
				 | 
					
					
						|||
| 
						
						
							
						
						47b7600f5e
	
				 | 
					
					
						|||
| 
						
						
							
						
						8f98c623ee
	
				 | 
					
					
						|||
| 
						
						
							
						
						4efda5347c
	
				 | 
					
					
						|||
| 
						
						
							
						
						23429d9631
	
				 | 
					
					
						|||
| 
						
						
							
						
						ca57c2632a
	
				 | 
					
					
						|||
| 
						
						
							
						
						c55500f51a
	
				 | 
					
					
						|||
| 
						
						
							
						
						1fee920902
	
				 | 
					
					
						|||
| 
						
						
							
						
						c6096d05b5
	
				 | 
					
					
						|||
| 
						
						
							
						
						07aa11d78d
	
				 | 
					
					
						|||
| 
						
						
							
						
						de27dce09c
	
				 | 
					
					
						|||
| 
						
						
							
						
						a1b2225750
	
				 | 
					
					
						|||
| 
						
						
							
						
						b87a109d61
	
				 | 
					
					
						|||
| 
						
						
							
						
						81145064de
	
				 | 
					
					
						|||
| 
						
						
							
						
						60a8ee7a80
	
				 | 
					
					
						|||
| 
						
						
							
						
						84f8c9436f
	
				 | 
					
					
						|||
| 
						
						
							
						
						a8f7532abd
	
				 | 
					
					
						|||
| 
						
						
							
						
						8dfbd0dee2
	
				 | 
					
					
						|||
| 
						
						
							
						
						930744e165
	
				 | 
					
					
						|||
| 
						
						
							
						
						4ca8825e02
	
				 | 
					
					
						|||
| 
						
						
							
						
						024b5117b4
	
				 | 
					
					
						|||
| 
						
						
							
						
						ac6b606ccc
	
				 | 
					
					
						|||
| 
						
						
							
						
						8bba456b14
	
				 | 
					
					
						|||
| 
						
						
							
						
						bb97445a96
	
				 | 
					
					
						|||
| 
						
						
							
						
						e2adac72cc
	
				 | 
					
					
						|||
| 
						
						
							
						
						3ddb0cf205
	
				 | 
					
					
						|||
| 
						
						
							
						
						efc13db66e
	
				 | 
					
					
						|||
| 
						
						
							
						
						b6315482b7
	
				 | 
					
					
						|||
| 
						
						
							
						
						51ea785d83
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c34a6846a
	
				 | 
					
					
						|||
| 
						
						
							
						
						da507edd05
	
				 | 
					
					
						|||
| 
						
						
							
						
						580b68789b
	
				 | 
					
					
						|||
| 
						
						
							
						
						31f9feab7b
	
				 | 
					
					
						|||
| 
						
						
							
						
						41bd25e711
	
				 | 
					
					
						|||
| 
						
						
							
						
						d9435c988c
	
				 | 
					
					
						|||
| 
						
						
							
						
						919a55c90b
	
				 | 
					
					
						|||
| 
						
						
							
						
						68bb695054
	
				 | 
					
					
						|||
| 
						
						
							
						
						04ec292caf
	
				 | 
					
					
						|||
| 
						
						
							
						
						a28fbf25bc
	
				 | 
					
					
						|||
| 
						
						
							
						
						6c748439ed
	
				 | 
					
					
						|||
| 
						
						
							
						
						fbf26a7d66
	
				 | 
					
					
						|||
| 
						
						
							
						
						1a8c461af2
	
				 | 
					
					
						|||
| 
						
						
							
						
						2a73e46315
	
				 | 
					
					
						|||
| 
						
						
							
						
						a68a73cf5c
	
				 | 
					
					
						|||
| 
						
						
							
						
						433ede4bf1
	
				 | 
					
					
						|||
| 
						
						
							
						
						3ae8be8348
	
				 | 
					
					
						|||
| 
						
						
							
						
						5d019e20b5
	
				 | 
					
					
						|||
| 
						
						
							
						
						03d1798e23
	
				 | 
					
					
						|||
| 
						
						
							
						
						ef2544868d
	
				 | 
					
					
						|||
| 
						
						
							
						
						0857a04a3a
	
				 | 
					
					
						|||
| 
						
						
							
						
						4302ec71f2
	
				 | 
					
					
						|||
| 
						
						
							
						
						ee5f63e50b
	
				 | 
					
					
						|||
| 
						
						
							
						
						6e26dc13b4
	
				 | 
					
					
						|||
| 
						
						
							
						
						5c725d1968
	
				 | 
					
					
						|||
| 
						
						
							
						
						ee71fb0dd0
	
				 | 
					
					
						|||
| 
						
						
							
						
						381e5f57c7
	
				 | 
					
					
						|||
| 
						
						
							
						
						788485d81e
	
				 | 
					
					
						|||
| 
						
						
							
						
						991e74b99b
	
				 | 
					
					
						|||
| 
						
						
							
						
						38670428da
	
				 | 
					
					
						|||
| 
						
						
							
						
						a7e638207f
	
				 | 
					
					
						|||
| 
						
						
							
						
						098895bfd9
	
				 | 
					
					
						|||
| 
						
						
							
						
						235b14dc11
	
				 | 
					
					
						|||
| 
						
						
							
						
						6ce5c7c2b6
	
				 | 
					
					
						|||
| 
						
						
							
						
						07e7d39ea2
	
				 | 
					
					
						|||
| 
						
						
							
						
						562e140a1e
	
				 | 
					
					
						|||
| 
						
						
							
						
						b71d5dff57
	
				 | 
					
					
						|||
| 
						
						
							
						
						c85de6b20f
	
				 | 
					
					
						|||
| 
						
						
							
						
						3796f56e81
	
				 | 
					
					
						|||
| 
						
						
							
						
						0a11abd3fe
	
				 | 
					
					
						|||
| 
						
						
							
						
						78faf438a5
	
				 | 
					
					
						|||
| 
						
						
							
						
						f2eb7621b4
	
				 | 
					
					
						|||
| 
						
						
							
						
						8877e17108
	
				 | 
					
					
						|||
| 
						
						
							
						
						686d45553b
	
				 | 
					
					
						|||
| 
						
						
							
						
						4ba1d85363
	
				 | 
					
					
						|||
| 
						
						
							
						
						45238d78cd
	
				 | 
					
					
						|||
| 
						
						
							
						
						8a656121a3
	
				 | 
					
					
						|||
| 
						
						
							
						
						6a1278786c
	
				 | 
					
					
						|||
| 
						
						
							
						
						13ae4810ca
	
				 | 
					
					
						|||
| 
						
						
							
						
						222ba6a060
	
				 | 
					
					
						|||
| 
						
						
							
						
						8e8cf49343
	
				 | 
					
					
						|||
| 
						
						
							
						
						6de5ab6298
	
				 | 
					
					
						|||
| 
						
						
							
						
						757bc9beaa
	
				 | 
					
					
						|||
| 
						
						
							
						
						f632510d2a
	
				 | 
					
					
						|||
| 
						
						
							
						
						05453718bb
	
				 | 
					
					
						|||
| 
						
						
							
						
						cfa90fb7de
	
				 | 
					
					
						|||
| 
						
						
							
						
						f1e9e47e13
	
				 | 
					
					
						|||
| 
						
						
							
						
						764312652d
	
				 | 
					
					
						|||
| 
						
						
							
						
						e663f02754
	
				 | 
					
					
						|||
| 
						
						
							
						
						1a190001fc
	
				 | 
					
					
						|||
| 
						
						
							
						
						3bf41993a3
	
				 | 
					
					
						|||
| 
						
						
							
						
						4f19a67da3
	
				 | 
					
					
						|||
| 
						
						
							
						
						12e3c42888
	
				 | 
					
					
						|||
| 
						
						
							
						
						146c5c6977
	
				 | 
					
					
						|||
| 
						
						
							
						
						25dcc3b136
	
				 | 
					
					
						|||
| 
						
						
							
						
						09e5a02ed6
	
				 | 
					
					
						|||
| 
						
						
							
						
						af80303719
	
				 | 
					
					
						|||
| 
						
						
							
						
						d65e83a21d
	
				 | 
					
					
						|||
| 
						
						
							
						
						77de7efc55
	
				 | 
					
					
						|||
| 
						
						
							
						
						35c1f2c8ba
	
				 | 
					
					
						|||
| 
						
						
							
						
						b973d323ba
	
				 | 
					
					
						|||
| 
						
						
							
						
						231b77e6c0
	
				 | 
					
					
						|||
| 
						
						
							
						
						7160a915e2
	
				 | 
					
					
						|||
| 
						
						
							
						
						6a878fd3c4
	
				 | 
					
					
						|||
| 
						
						
							
						
						3274b64f5a
	
				 | 
					
					
						|||
| 
						
						
							
						
						feda4fd70f
	
				 | 
					
					
						|||
| 
						
						
							
						
						59af3b7e7b
	
				 | 
					
					
						|||
| 
						
						
							
						
						fc559c3d01
	
				 | 
					
					
						|||
| 
						
						
							
						
						6869816cc4
	
				 | 
					
					
						|||
| 
						
						
							
						
						8fdf9e2bc3
	
				 | 
					
					
						|||
| 
						
						
							
						
						235af37382
	
				 | 
					
					
						|||
| 
						
						
							
						
						06ab13797d
	
				 | 
					
					
						|||
| 
						
						
							
						
						e70bb20934
	
				 | 
					
					
						|||
| 
						
						
							
						
						b0de0e09bd
	
				 | 
					
					
						|||
| 
						
						
							
						
						077747f428
	
				 | 
					
					
						|||
| 
						
						
							
						
						3ae22e49ee
	
				 | 
					
					
						|||
| 
						
						
							
						
						bb67df716c
	
				 | 
					
					
						|||
| 
						
						
							
						
						4cd2978e21
	
				 | 
					
					
						|||
| 
						
						
							
						
						68e786b4e8
	
				 | 
					
					
						|||
| 
						
						
							
						
						9a1396b91f
	
				 | 
					
					
						|||
| 
						
						
							
						
						5abf6a719f
	
				 | 
					
					
						|||
| 
						
						
							
						
						0a6b06d1d0
	
				 | 
					
					
						|||
| 
						
						
							
						
						d889acc315
	
				 | 
					
					
						|||
| 
						
						
							
						
						6142bf9c53
	
				 | 
					
					
						|||
| 
						
						
							
						
						244779bd8c
	
				 | 
					
					
						|||
| 
						
						
							
						
						685defa684
	
				 | 
					
					
						|||
| 
						
						
							
						
						b935b0baf8
	
				 | 
					
					
						|||
| 
						
						
							
						
						2ac918b7ab
	
				 | 
					
					
						|||
| 
						
						
							
						
						5f8dc88fa7
	
				 | 
					
					
						|||
| 
						
						
							
						
						380ddd540b
	
				 | 
					
					
						|||
| 
						
						
							
						
						8b232dc444
	
				 | 
					
					
						|||
| 
						
						
							
						
						fa69935e67
	
				 | 
					
					
						|||
| 
						
						
							
						
						4832474c5f
	
				 | 
					
					
						|||
| 
						
						
							
						
						0110e0a5d2
	
				 | 
					
					
						|||
| 
						
						
							
						
						c49e58a0ba
	
				 | 
					
					
						|||
| 
						
						
							
						
						98bdab443a
	
				 | 
					
					
						|||
| 
						
						
							
						
						bf47782f0a
	
				 | 
					
					
						|||
| 
						
						
							
						
						c1af556751
	
				 | 
					
					
						|||
| 
						
						
							
						
						2d86ffed34
	
				 | 
					
					
						|||
| 
						
						
							
						
						aaa7cb93c3
	
				 | 
					
					
						|||
| 
						
						
							
						
						d4b51f07b5
	
				 | 
					
					
						|||
| 
						
						
							
						
						720464327c
	
				 | 
					
					
						|||
| 
						
						
							
						
						ad1ff06aff
	
				 | 
					
					
						|||
| 
						
						
							
						
						af2eb411d9
	
				 | 
					
					
						|||
| 
						
						
							
						
						004919cbc5
	
				 | 
					
					
						|||
| 
						
						
							
						
						336053f24d
	
				 | 
					
					
						|||
| 
						
						
							
						
						2f993502fc
	
				 | 
					
					
						|||
| 
						
						
							
						
						e5b1a1861c
	
				 | 
					
					
						|||
| 
						
						
							
						
						e37acf365a
	
				 | 
					
					
						|||
| 
						
						
							
						
						ccf15bc8ae
	
				 | 
					
					
						|||
| 
						
						
							
						
						5e4476ff71
	
				 | 
					
					
						|||
| 
						
						
							
						
						035997750e
	
				 | 
					
					
						|||
| 
						
						
							
						
						7a4b5cd065
	
				 | 
					
					
						|||
| 
						
						
							
						
						40c1f8327e
	
				 | 
					
					
						|||
| 
						
						
							
						
						6419209c98
	
				 | 
					
					
						|||
| 
						
						
							
						
						2d4cab52b3
	
				 | 
					
					
						|||
| 
						
						
							
						
						46edd4406c
	
				 | 
					
					
						|||
| 
						
						
							
						
						b35590a51c
	
				 | 
					
					
						|||
| 
						
						
							
						
						9899a26635
	
				 | 
					
					
						|||
| 
						
						
							
						
						24f9d21ca7
	
				 | 
					
					
						|||
| 
						
						
							
						
						ad29013e44
	
				 | 
					
					
						|||
| 
						
						
							
						
						33851295d8
	
				 | 
					
					
						|||
| 
						
						
							
						
						46f90f2f35
	
				 | 
					
					
						|||
| 
						
						
							
						
						9ba3679e89
	
				 | 
					
					
						|||
| 
						
						
							
						
						6a61d01f4d
	
				 | 
					
					
						|||
| 
						
						
							
						
						f435252492
	
				 | 
					
					
						|||
| 
						
						
							
						
						dfa1fed18b
	
				 | 
					
					
						|||
| 
						
						
							
						
						58d11ebbff
	
				 | 
					
					
						|||
| 
						
						
							
						
						1221325b3e
	
				 | 
					
					
						|||
| 
						
						
							
						
						c3eb5ca170
	
				 | 
					
					
						|||
| 
						
						
							
						
						9c0e9d8d49
	
				 | 
					
					
						|||
| 
						
						
							
						
						ada67f044a
	
				 | 
					
					
						|||
| 
						
						
							
						
						63955e881d
	
				 | 
					
					
						|||
| 
						
						
							
						
						6130f527d4
	
				 | 
					
					
						|||
| 
						
						
							
						
						3da1d32df7
	
				 | 
					
					
						|||
| 
						
						
							
						
						56d623fe52
	
				 | 
					
					
						|||
| 
						
						
							
						
						2c46ca262b
	
				 | 
					
					
						|||
| 
						
						
							
						
						39cd52905b
	
				 | 
					
					
						|||
| 
						
						
							
						
						9feb53a792
	
				 | 
					
					
						|||
| 
						
						
							
						
						4427da5343
	
				 | 
					
					
						|||
| 
						
						
							
						
						92c6ca6c35
	
				 | 
					
					
						|||
| 
						
						
							
						
						ae57c45c2a
	
				 | 
					
					
						|||
| 
						
						
							
						
						bac92f2612
	
				 | 
					
					
						|||
| 
						
						
							
						
						64ba54e8e6
	
				 | 
					
					
						|||
| 
						
						
							
						
						6e903f6f5c
	
				 | 
					
					
						|||
| 
						
						
							
						
						e23ed245db
	
				 | 
					
					
						|||
| 
						
						
							
						
						b78010ccb1
	
				 | 
					
					
						|||
| 
						
						
							
						
						c136c089fa
	
				 | 
					
					
						|||
| 
						
						
							
						
						1c2a441cb5
	
				 | 
					
					
						|||
| 
						
						
							
						
						0b6b3d8290
	
				 | 
					
					
						|||
| 
						
						
							
						
						c3a098c503
	
				 | 
					
					
						|||
| 
						
						
							
						
						577f8c0d92
	
				 | 
					
					
						|||
| 
						
						
							
						
						4d8165d790
	
				 | 
					
					
						|||
| 
						
						
							
						
						c18404efee
	
				 | 
					
					
						|||
| 
						
						
							
						
						bff2b92c9e
	
				 | 
					
					
						|||
| 
						
						
							
						
						7297c40f93
	
				 | 
					
					
						|||
| 
						
						
							
						
						ea2d159773
	
				 | 
					
					
						|||
| 
						
						
							
						
						4f035bc6b1
	
				 | 
					
					
						|||
| 
						
						
							
						
						18f7607e1b
	
				 | 
					
					
						|||
| 
						
						
							
						
						1478a9f83f
	
				 | 
					
					
						|||
| 
						
						
							
						
						9eb9cc44aa
	
				 | 
					
					
						|||
| 
						
						
							
						
						e161f77359
	
				 | 
					
					
						|||
| 
						
						
							
						
						3ed23e423b
	
				 | 
					
					
						|||
| 
						
						
							
						
						6c7d431e35
	
				 | 
					
					
						|||
| 
						
						
							
						
						006e547deb
	
				 | 
					
					
						|||
| 
						
						
							
						
						caca14036c
	
				 | 
					
					
						
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					/meson.build.user
 | 
				
			||||||
 | 
					/subprojects/*
 | 
				
			||||||
 | 
					!/subprojects/*.wrap
 | 
				
			||||||
 | 
					!/subprojects/packagefiles
 | 
				
			||||||
							
								
								
									
										5
									
								
								.gitmodules
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,3 +1,6 @@
 | 
				
			|||||||
[submodule "wuffs-mirror-release-c"]
 | 
					[submodule "wuffs-mirror-release-c"]
 | 
				
			||||||
	path = wuffs-mirror-release-c
 | 
						path = submodules/wuffs-mirror-release-c
 | 
				
			||||||
	url = https://github.com/google/wuffs-mirror-release-c
 | 
						url = https://github.com/google/wuffs-mirror-release-c
 | 
				
			||||||
 | 
					[submodule "liberty"]
 | 
				
			||||||
 | 
						path = submodules/liberty
 | 
				
			||||||
 | 
						url = https://git.janouch.name/p/liberty.git
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								LICENSE
									
									
									
									
									
								
							
							
						
						@@ -1,4 +1,4 @@
 | 
				
			|||||||
Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Permission to use, copy, modify, and/or distribute this software for any
 | 
					Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
purpose with or without fee is hereby granted.
 | 
					purpose with or without fee is hereby granted.
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										113
									
								
								README.adoc
									
									
									
									
									
								
							
							
						
						@@ -1,55 +1,104 @@
 | 
				
			|||||||
fastiv
 | 
					fiv
 | 
				
			||||||
======
 | 
					===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
'fastiv' is a fast image viewer, supporting BMP, PNG, GIF, JPEG, and optionally
 | 
					'fiv' is a slightly unconventional, general-purpose image browser and viewer
 | 
				
			||||||
raw photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever gdk-pixbuf loads.
 | 
					for Linux and Windows (macOS still has major issues).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
It still has some road to go, but it's already become quite usable,
 | 
					image::docs/fiv.webp["Screenshot of both the browser and the viewer"]
 | 
				
			||||||
and it has received basic polishing.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Non-goals
 | 
					Features
 | 
				
			||||||
---------
 | 
					--------
 | 
				
			||||||
 - fancy UI--the focus is on speed of use first, colour accuracy second
 | 
					 - Uses a compact thumbnail view, helping you browse collections comfortably.
 | 
				
			||||||
 - editing--that's what _editors_ are for, be it GIMP or Rawtherapee;
 | 
					 - Supports BMP, (A)PNG, GIF, TGA, JPEG, WebP directly, plus optionally raw
 | 
				
			||||||
   nothing beyond the most basic of adjustments is desired
 | 
					   photos, HEIC, AVIF, SVG, X11 cursors and TIFF, or whatever your gdk-pixbuf
 | 
				
			||||||
 - memory efficiency, though preloading can cause some pressure
 | 
					   modules manage to load.
 | 
				
			||||||
 - portability to non-UNIXy systems
 | 
					 - Employs high-performance file format libraries: Wuffs and libjpeg-turbo.
 | 
				
			||||||
 | 
					 - Can make use of 30-bit X.org visuals, under certain conditions.
 | 
				
			||||||
 | 
					 - Has a notion of pages, and tries to load all included content within files.
 | 
				
			||||||
 | 
					 - Can keep the zoom and position when browsing, to help with comparing
 | 
				
			||||||
 | 
					   zoomed-in images.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Explicit non-goals
 | 
				
			||||||
 | 
					------------------
 | 
				
			||||||
 | 
					 - Editing--that's what _editors_ are for, be it GIMP or Rawtherapee;
 | 
				
			||||||
 | 
					   nothing beyond the most basic of adjustments is desired.
 | 
				
			||||||
 | 
					 - Following the latest GNOME HIG to the letter--header bars are deliberately
 | 
				
			||||||
 | 
					   avoided, for their general user hostility.
 | 
				
			||||||
 | 
					 - Memory efficiency is secondary to both performance and development effort.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Aspirations
 | 
				
			||||||
 | 
					-----------
 | 
				
			||||||
 | 
					Show colours as accurately as hardware allows.  Open everything.  Be fast.
 | 
				
			||||||
 | 
					Not necessarily in this order.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Packages
 | 
					Packages
 | 
				
			||||||
--------
 | 
					--------
 | 
				
			||||||
Regular releases are sporadic.  git master should be stable enough.  You can get
 | 
					Regular releases are sporadic.  git master should be stable enough.
 | 
				
			||||||
a package with the latest development version from Archlinux's AUR.
 | 
					You can get a package with the latest development version using Arch Linux's
 | 
				
			||||||
 | 
					https://aur.archlinux.org/packages/fiv-git[AUR],
 | 
				
			||||||
 | 
					or as a https://git.janouch.name/p/nixexprs[Nix derivation].
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					https://janouch.name/cd[Windows installers can be found here],
 | 
				
			||||||
 | 
					you want the _x86_64_ version.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Building and Running
 | 
					Building and Running
 | 
				
			||||||
--------------------
 | 
					--------------------
 | 
				
			||||||
Build dependencies: Meson, pkg-config +
 | 
					Build-only dependencies:
 | 
				
			||||||
 | 
					 Meson, pkg-config, asciidoctor or asciidoc (recommended but optional) +
 | 
				
			||||||
Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
 | 
					Runtime dependencies: gtk+-3.0, glib>=2.64, pixman-1, shared-mime-info,
 | 
				
			||||||
spng>=0.7.0, libturbojpeg +
 | 
					 libturbojpeg, libwebp, libepoxy, librsvg-2.0 (for icons) +
 | 
				
			||||||
Optional dependencies: LibRaw, librsvg-2.0, xcursor, libheif, libtiff,
 | 
					Optional dependencies: lcms2, Little CMS fast float plugin,
 | 
				
			||||||
gdk-pixbuf-2.0
 | 
					 LibRaw, librsvg-2.0, xcursor, libheif, libtiff, ExifTool,
 | 
				
			||||||
 | 
					 resvg (unstable API, needs to be requested explicitly) +
 | 
				
			||||||
 | 
					Runtime dependencies for reverse image search:
 | 
				
			||||||
 | 
					 xdg-utils, cURL, jq
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 $ git clone --recursive https://git.janouch.name/p/fastiv.git
 | 
					 $ git clone --recursive https://git.janouch.name/p/fiv.git
 | 
				
			||||||
 $ meson builddir
 | 
					 $ cd fiv
 | 
				
			||||||
 | 
					 $ meson setup builddir
 | 
				
			||||||
 $ cd builddir
 | 
					 $ cd builddir
 | 
				
			||||||
 $ meson compile
 | 
					 $ meson compile
 | 
				
			||||||
 | 
					 $ meson devenv fiv
 | 
				
			||||||
 | 
					
 | 
				
			||||||
To install the application, you can do:
 | 
					The lossless JPEG cropper and reverse image search are intended to be invoked
 | 
				
			||||||
 | 
					from a file manager context menu.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 # meson install
 | 
					For proper integration, you will need to install the application.  On Debian,
 | 
				
			||||||
 | 
					you can get a quick and dirty installation package for testing purposes using:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Configuration
 | 
					 $ meson compile deb
 | 
				
			||||||
 | 
					 # dpkg -i fiv-*.deb
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Windows
 | 
				
			||||||
 | 
					~~~~~~~
 | 
				
			||||||
 | 
					'fiv' can be cross-compiled for Windows, provided that you install a bunch of
 | 
				
			||||||
 | 
					dependencies listed at the beginning of 'msys2-configure.sh',
 | 
				
			||||||
 | 
					plus rsvg-convert from librsvg2, icotool from icoutils, and msitools ≥ 0.102.
 | 
				
			||||||
 | 
					Beware that the build will take up about a gigabyte of disk space.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 $ sh -e msys2-configure.sh builddir
 | 
				
			||||||
 | 
					 $ meson compile package -C builddir
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If everything succeeds, you will find a portable build of the application
 | 
				
			||||||
 | 
					in the 'builddir/package' subdirectory, and a very basic MSI installation
 | 
				
			||||||
 | 
					package in 'builddir'.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Faster colour management
 | 
				
			||||||
 | 
					^^^^^^^^^^^^^^^^^^^^^^^^
 | 
				
			||||||
 | 
					To get the Little CMS fast float plugin, you'll have to enter MSYS2 and
 | 
				
			||||||
 | 
					https://www.msys2.org/wiki/Creating-Packages/#re-building-a-package[rebuild]
 | 
				
			||||||
 | 
					_mingw-w64-lcms2_ with the following change:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 sed -i 's/meson setup /&-Dfastfloat=true /' PKGCONFIG
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Documentation
 | 
				
			||||||
-------------
 | 
					-------------
 | 
				
			||||||
The standard means to adjust the looks of the program is through GTK+ 3 CSS.
 | 
					For information concerning usage, refer to link:docs/fiv.html[the user guide],
 | 
				
			||||||
As an example, to tightly pack browser items, put the following in your
 | 
					which can be invoked from within the program by pressing F1.
 | 
				
			||||||
_~/.config/gtk-3.0/gtk.css_:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 fastiv-browser { -FastivBrowser-spacing: 0; padding: 0; border: 0; margin: 0; }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
The GTK+ inspector will be very helpful, should you want to experiment.
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
Contributing and Support
 | 
					Contributing and Support
 | 
				
			||||||
------------------------
 | 
					------------------------
 | 
				
			||||||
Use https://git.janouch.name/p/fastiv to report any bugs, request features,
 | 
					Use https://git.janouch.name/p/fiv to report any bugs, request features,
 | 
				
			||||||
or submit pull requests.  `git send-email` is tolerated.  If you want to discuss
 | 
					or submit pull requests.  `git send-email` is tolerated.  If you want to discuss
 | 
				
			||||||
the project, feel free to join me at ircs://irc.janouch.name, channel #dev.
 | 
					the project, feel free to join me at ircs://irc.janouch.name, channel #dev.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										85
									
								
								docs/fiv.adoc
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,85 @@
 | 
				
			|||||||
 | 
					fiv(1)
 | 
				
			||||||
 | 
					======
 | 
				
			||||||
 | 
					:doctype: manpage
 | 
				
			||||||
 | 
					:manmanual: fiv Manual
 | 
				
			||||||
 | 
					:mansource: fiv {release-version}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Name
 | 
				
			||||||
 | 
					----
 | 
				
			||||||
 | 
					fiv - Image browser and viewer
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Synopsis
 | 
				
			||||||
 | 
					--------
 | 
				
			||||||
 | 
					*fiv* [_OPTION_]... [_PATH_ | _URI_]...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Description
 | 
				
			||||||
 | 
					-----------
 | 
				
			||||||
 | 
					*fiv* is a general-purpose image browser and viewer: pass it a directory path
 | 
				
			||||||
 | 
					or URI to open it for browsing, or pass an image to open it for viewing.
 | 
				
			||||||
 | 
					In case that multiple arguments are passed, they'll be opened as a virtual
 | 
				
			||||||
 | 
					directory containing all of them.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For more information concerning usage, press *F1* in the application to open
 | 
				
			||||||
 | 
					the _User Guide_.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(p): Try to merge the two, though this one focuses on command line usage.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Options
 | 
				
			||||||
 | 
					-------
 | 
				
			||||||
 | 
					*--browse*::
 | 
				
			||||||
 | 
						When an image is passed, start in browsing mode, and preselect that
 | 
				
			||||||
 | 
						image in its containing directory.  This is used by *fiv*'s inode/directory
 | 
				
			||||||
 | 
						handler to implement the "Open Containing Folder" feature of certain
 | 
				
			||||||
 | 
						applications.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*--collection*::
 | 
				
			||||||
 | 
						Always put arguments in a virtual directory, even when only one is passed.
 | 
				
			||||||
 | 
						Implies *--browse*.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*--help-all*::
 | 
				
			||||||
 | 
						Show the full list of options, including those provided by GTK+.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*--invalidate-cache*::
 | 
				
			||||||
 | 
						Invalidate the wide thumbnail cache, removing thumbnails for files that can
 | 
				
			||||||
 | 
						no longer be found.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*--list-supported-media-types*::
 | 
				
			||||||
 | 
						Output supported media types and exit.  This is used by a script to update
 | 
				
			||||||
 | 
						the list of MIME types within *fiv*'s desktop file when the list
 | 
				
			||||||
 | 
						of GdkPixbuf loaders changes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*-V*, *--version*::
 | 
				
			||||||
 | 
						Output version information and exit.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Internal options
 | 
				
			||||||
 | 
					~~~~~~~~~~~~~~~~
 | 
				
			||||||
 | 
					*--extract-thumbnail*::
 | 
				
			||||||
 | 
						Present any embedded thumbnail of the first argument on the standard output
 | 
				
			||||||
 | 
						in an application-specific bitmap format.  When both *--thumbnail*
 | 
				
			||||||
 | 
						and *--extract-thumbnail* are passed, this option takes precedence,
 | 
				
			||||||
 | 
						exiting early if successful.  This is used to enhance responsivity
 | 
				
			||||||
 | 
						of thumbnail procurement.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*--thumbnail*=_SIZE_::
 | 
				
			||||||
 | 
						Generate wide thumbnails for the first argument, in all sizes not exceeding
 | 
				
			||||||
 | 
						_SIZE_, and present the largest of them on the standard output
 | 
				
			||||||
 | 
						in an application-specific bitmap format.  Available sizes follow directory
 | 
				
			||||||
 | 
						names in the _Thumbnail Managing Standard_.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*--thumbnail-for-search*=_SIZE_::
 | 
				
			||||||
 | 
						Transform the first argument to a widely supported image file format,
 | 
				
			||||||
 | 
						and present it on the standard output.  The image will be downscaled as
 | 
				
			||||||
 | 
						necessary so as to not exceed _SIZE_ (see *--thumbnail*).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Reporting bugs
 | 
				
			||||||
 | 
					--------------
 | 
				
			||||||
 | 
					Use https://git.janouch.name/p/fiv to report bugs, request features,
 | 
				
			||||||
 | 
					or submit pull requests.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					See also
 | 
				
			||||||
 | 
					--------
 | 
				
			||||||
 | 
					_Desktop Entry Specification_,
 | 
				
			||||||
 | 
					https://specifications.freedesktop.org/desktop-entry-spec/latest/[].
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					_Thumbnail Managing Standard_,
 | 
				
			||||||
 | 
					https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html[].
 | 
				
			||||||
							
								
								
									
										133
									
								
								docs/fiv.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,133 @@
 | 
				
			|||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					<head>
 | 
				
			||||||
 | 
					<meta charset="UTF-8">
 | 
				
			||||||
 | 
					<title>fiv: User Guide</title>
 | 
				
			||||||
 | 
					<link rel="stylesheet" href="stylesheet.css">
 | 
				
			||||||
 | 
					<style>
 | 
				
			||||||
 | 
					q:lang(en):before { content: "‘"; }
 | 
				
			||||||
 | 
					q:lang(en):after { content: "’"; }
 | 
				
			||||||
 | 
					</style>
 | 
				
			||||||
 | 
					</head>
 | 
				
			||||||
 | 
					<body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h1>fiv: User Guide</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p class="details">
 | 
				
			||||||
 | 
					<span id="author">Přemysl Eric Janouch</span><br>
 | 
				
			||||||
 | 
					<span id="email"><a href="mailto:p@janouch.name">p@janouch.name</a></span><br>
 | 
				
			||||||
 | 
					<span id="revnumber">version 1.0.0,</span>
 | 
				
			||||||
 | 
					<span id="revdate">2023-04-17</span>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p class="figure"><img src="fiv.webp" alt="fiv in browser and viewer modes">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>Introduction</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p><i>fiv</i> is a general-purpose image browser and viewer. This document will
 | 
				
			||||||
 | 
					guide you through the application and help to familiarize you with it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>Controls</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p><i>fiv</i> is designed with computer mice having dedicated forwards/backwards
 | 
				
			||||||
 | 
					and page up/down buttons in mind, such as SteelSeries Sensei series. Ozone Neon
 | 
				
			||||||
 | 
					series may also be mapped this way. Your experience may be degraded with other
 | 
				
			||||||
 | 
					kinds of devices.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Controls should generally be accessible through the keyboard. Pressing
 | 
				
			||||||
 | 
					<kbd>Ctrl</kbd> + <kbd>?</kbd> will give you a convenient overview
 | 
				
			||||||
 | 
					of all shortcuts. In addition to these, remember that you may often use
 | 
				
			||||||
 | 
					<kbd>Ctrl</kbd> + <kbd>Tab</kbd> and <kbd>F6</kbd> to navigate to
 | 
				
			||||||
 | 
					different groups of widgets.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>Browser</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p><i>fiv</i> normally starts in a file browser view. On the left side of the
 | 
				
			||||||
 | 
					window, you'll find your GTK+ bookmarks, mounted locations as recognized by
 | 
				
			||||||
 | 
					GVfs, an item for entering arbitrary filesystem paths or URIs, and finally
 | 
				
			||||||
 | 
					breadcrumbs leading to the currently opened directory, as well as descendants
 | 
				
			||||||
 | 
					of it.  At the top, there is a toolbar with view controls.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>You can open items in a new window either by middle clicking on them, or with
 | 
				
			||||||
 | 
					the left mouse button while holding the <kbd>Ctrl</kbd> key.
 | 
				
			||||||
 | 
					Right clicking the directory view offers a context menu for opening files,
 | 
				
			||||||
 | 
					or even the directory itself, in a different application.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>Viewer</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>The image viewer may be both entered (so long as you have a file selected)
 | 
				
			||||||
 | 
					and exited using the <kbd>Enter</kbd> key. This way you may easily switch
 | 
				
			||||||
 | 
					between the two modes. When using the mouse, the forwards and backwards buttons
 | 
				
			||||||
 | 
					will fulfill the same function.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Double clicking the image switches full-screen view, and the mouse wheel
 | 
				
			||||||
 | 
					adjusts the zoom level.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Files are iterated in the same order, and using the same filtering as in
 | 
				
			||||||
 | 
					the browser.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>File formats</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>The list of all supported file formats may be obtained by running:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<pre>
 | 
				
			||||||
 | 
					fiv --list-supported-media-types
 | 
				
			||||||
 | 
					</pre>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Unless it has been turned off in your installation, you may extend it through
 | 
				
			||||||
 | 
					gdk-pixbuf modules.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>Thumbnails</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p><i>fiv</i> uses a custom means of storing thumbnails, and doesn't currently
 | 
				
			||||||
 | 
					invalidate this cache automatically. Should you find out that your
 | 
				
			||||||
 | 
					<i>~/.cache/thumbnails</i> directory is taking up too much space, run:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<pre>
 | 
				
			||||||
 | 
					fiv --invalidate-cache
 | 
				
			||||||
 | 
					</pre>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>to trim it down. Alternatively, if you want to get rid of <i>all</i>
 | 
				
			||||||
 | 
					thumbnails, even for existing images:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<pre>
 | 
				
			||||||
 | 
					rm -rf ~/.cache/thumbnails/wide-*
 | 
				
			||||||
 | 
					</pre>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h2>Configuration</h2>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>To adjust the few configuration options of <i>fiv</i>,
 | 
				
			||||||
 | 
					press <kbd>Ctrl</kbd> + <kbd>,</kbd> to open <i>Preferences</i>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>To make your changes take effect, restart <i>fiv</i>.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<h3>Theming</h3>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>The standard means to adjust the looks of the program is through GTK+ 3 CSS.
 | 
				
			||||||
 | 
					As an example, to tightly pack browser items, put the following in your
 | 
				
			||||||
 | 
					<i>~/.config/gtk-3.0/gtk.css</i>:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<pre>
 | 
				
			||||||
 | 
					fiv-browser { -FivBrowser-spacing: 0; padding: 0; border: 0; margin: 0; }
 | 
				
			||||||
 | 
					</pre>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Similarly, you can adjust some of the key bindings, as per the command table
 | 
				
			||||||
 | 
					in the <i>fiv-view.h</i> source file:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<pre>
 | 
				
			||||||
 | 
					@binding-set ViewBindings { bind 'p' { 'command' (print) }; }
 | 
				
			||||||
 | 
					fiv-view { -gtk-key-bindings: ViewBindings; }
 | 
				
			||||||
 | 
					</pre>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<p>Should you want to experiment, you will find the GTK+ inspector very helpful.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script>
 | 
				
			||||||
 | 
					var toc = '', all = document.querySelectorAll('h2')
 | 
				
			||||||
 | 
					for (var i = 0; i < all.length; i++) {
 | 
				
			||||||
 | 
						var name = all[i].innerHTML.replace(/[^0-9a-z]/ig, '-')
 | 
				
			||||||
 | 
						toc += '<li><p><a href="#' + name + '">' + all[i].innerHTML + '</a></li>'
 | 
				
			||||||
 | 
						all[i].id = name
 | 
				
			||||||
 | 
						all[i].innerHTML = (i + 1) + '. ' + all[i].innerHTML
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					all[0].insertAdjacentHTML('beforebegin',
 | 
				
			||||||
 | 
						'<h2>Table of Contents</h2><ol>' + toc + '</ol>')
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
							
								
								
									
										
											BIN
										
									
								
								docs/fiv.webp
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 194 KiB  | 
							
								
								
									
										9
									
								
								docs/stylesheet.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					body { max-width: 50em; margin: 0 auto 4em auto; padding: 0 2em;
 | 
				
			||||||
 | 
						font-family: sans-serif; } h1, h2, h3 { font-weight: normal; }
 | 
				
			||||||
 | 
					h1 { font-size: 2.5em; } h2 { font-size: 2em; } h3 { font-size: 1.33em; }
 | 
				
			||||||
 | 
					h2 { padding-top: .67em; border-top: 1px solid silver; }
 | 
				
			||||||
 | 
					p { line-height: 1.5; } .figure { text-align: center; } img { max-width: 100%; }
 | 
				
			||||||
 | 
					q { font-style: normal; } .details { border-bottom: 1px solid silver; }
 | 
				
			||||||
 | 
					.details br { display: none; } .details br + span:before { content: " — "; }
 | 
				
			||||||
 | 
					pre { padding: 0 1em; } kbd { border: solid #ccc; border-radius: .25em;
 | 
				
			||||||
 | 
						border-width: 1px 2px 2px 1px; padding: 0 .25em; font-family: inherit; }
 | 
				
			||||||
							
								
								
									
										126
									
								
								docs/wide-thumbnail-spec.adoc
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,126 @@
 | 
				
			|||||||
 | 
					Wide Thumbnail Managing Standard
 | 
				
			||||||
 | 
					================================
 | 
				
			||||||
 | 
					Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					v0.1, 2021-12-29: Preliminary draft
 | 
				
			||||||
 | 
					:description: Wide-format thumbnail managment specification
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Introduction
 | 
				
			||||||
 | 
					------------
 | 
				
			||||||
 | 
					This document is a follow-up to the
 | 
				
			||||||
 | 
					https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html[Thumbnail Managing Standard],
 | 
				
			||||||
 | 
					in particular version 0.9.0.  It extends the specification to cover wide-format
 | 
				
			||||||
 | 
					file thumbnails, providing a well-defined mechanism for sharing them amongst
 | 
				
			||||||
 | 
					different programs.  It also addresses new needs that have arisen with
 | 
				
			||||||
 | 
					high-density, wide-gamut monitors.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Please contact the author of this document if you intend to use it.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Rationale
 | 
				
			||||||
 | 
					~~~~~~~~~
 | 
				
			||||||
 | 
					Photos tend to be in a 4:3 format.  Yet, nearly all file browsers at the time
 | 
				
			||||||
 | 
					of this document's conception (end of year 2021) show previews in a rectangular
 | 
				
			||||||
 | 
					grid.  This wastes a lot of display real estate with padding, and looks
 | 
				
			||||||
 | 
					subjectively awkward.  The Web at large has long been moving off of this
 | 
				
			||||||
 | 
					concept, instead preferring freely flowing items of fixed height--notably on
 | 
				
			||||||
 | 
					such sites as DeviantArt and Google/Duck image search.  The general Unix desktop
 | 
				
			||||||
 | 
					keeps lagging behind.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Scaling sub-nominal thumbnail sizes up, or larger sizes down is not a practical
 | 
				
			||||||
 | 
					solution: the former gives blurry images, while the latter may waste
 | 
				
			||||||
 | 
					a significant amount of disk space.  Both require reprocessing.  Seeing as these
 | 
				
			||||||
 | 
					issues have only become worse with the higher resolutions added to the 0.9.0
 | 
				
			||||||
 | 
					revision of the preceding standard, a new one is necessary.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Storage
 | 
				
			||||||
 | 
					-------
 | 
				
			||||||
 | 
					The base directory for thumbnails is the same as in the original specification.
 | 
				
			||||||
 | 
					The list of subdirectories is similar, but a `wide-` prefix is added,
 | 
				
			||||||
 | 
					turning `large` into `wide-large`, and so on.  The `fail` directory does not
 | 
				
			||||||
 | 
					constitute an exception to this rule, and is also duplicated, if necessary.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The dimensions differ like so: the original _height_ for the respective sizes is
 | 
				
			||||||
 | 
					kept, but a factor of 2 is applied to the _width_:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 - _$XDG_CACHE_HOME/thumbnails/wide-normal_ contains previews proportionally
 | 
				
			||||||
 | 
					   scaled down to 256x128 pixels,
 | 
				
			||||||
 | 
					 - _$XDG_CACHE_HOME/thumbnails/wide-large_ contains 512x256 pixel previews,
 | 
				
			||||||
 | 
					 - _$XDG_CACHE_HOME/thumbnails/wide-x-large_ contains 1024x512 pixel previews,
 | 
				
			||||||
 | 
					 - _$XDG_CACHE_HOME/thumbnails/wide-xx-large_ contains 2048x1024 pixel previews.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					It is unspecified whether non-square pixels are scaled down accordingly, but it
 | 
				
			||||||
 | 
					is recommended to do so.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					File format
 | 
				
			||||||
 | 
					~~~~~~~~~~~
 | 
				
			||||||
 | 
					To account for very large thumbnail sizes, this specification has chosen the
 | 
				
			||||||
 | 
					WebP codec, in its _extended file format_.  Thumbnail files still derive their
 | 
				
			||||||
 | 
					name from the MD5 hash of input URIs, as in the original standard, because
 | 
				
			||||||
 | 
					this widespread algorithm shows surprisingly good results for this use case.
 | 
				
			||||||
 | 
					The filename, however, deviates in that it receives the appropriate _.webp_ file
 | 
				
			||||||
 | 
					extension.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Both lossy and lossless encodings may be used.  Animations are assumed to be
 | 
				
			||||||
 | 
					representative samples, and their timing needn't be respected.  No metadata
 | 
				
			||||||
 | 
					chunks are allowed other than _THUM_, which is described below.  Any Exif
 | 
				
			||||||
 | 
					orientation changes need to be "baked-in" to the image.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Metadata
 | 
				
			||||||
 | 
					~~~~~~~~
 | 
				
			||||||
 | 
					Because WebP doesn't directly provide any means of storing simple key-value
 | 
				
			||||||
 | 
					pairs, thumbnail attributes are stored in a custom chunk named "THUM".
 | 
				
			||||||
 | 
					It consists of a stream of NUL-terminated pairs, using the UTF-8 encoding.
 | 
				
			||||||
 | 
					The last NUL byte may not be omitted.  The behavior of repeated keys is
 | 
				
			||||||
 | 
					undefined.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					All keys from the original specification are adopted, including the extension
 | 
				
			||||||
 | 
					mechanism, plus these additions:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Additional fields
 | 
				
			||||||
 | 
					[cols="1,2"]
 | 
				
			||||||
 | 
					|===
 | 
				
			||||||
 | 
					| Key | Description
 | 
				
			||||||
 | 
					| `Thumb::ColorSpace` | The thumbnail's color space, if it is known.
 | 
				
			||||||
 | 
					|===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					The color space field may only be included if the producing program has applied
 | 
				
			||||||
 | 
					color management to the input image, e.g., using any embedded ICC profiles,
 | 
				
			||||||
 | 
					so that the color space is now known and normalized.  No rendering intent is
 | 
				
			||||||
 | 
					hereby suggested.  It is permitted to assume sRGB for input images with
 | 
				
			||||||
 | 
					unspecified color spaces.  The full list of allowed values is:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.Color spaces
 | 
				
			||||||
 | 
					[cols="1,2"]
 | 
				
			||||||
 | 
					|===
 | 
				
			||||||
 | 
					| Value | Description
 | 
				
			||||||
 | 
					| `sRGB` | IEC 61966-2-1
 | 
				
			||||||
 | 
					| `Display P3` | sRGB with DCI-P3 primaries, as used by Apple.
 | 
				
			||||||
 | 
					|===
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Interactions
 | 
				
			||||||
 | 
					------------
 | 
				
			||||||
 | 
					Programs may fall back to picking up and rescaling a square-sized thumbnail if
 | 
				
			||||||
 | 
					they fail to find a wide one, preferrably one size above what they are looking
 | 
				
			||||||
 | 
					for.  The wide-format thumbnail should then be automatically regenerated.
 | 
				
			||||||
 | 
					It should also be regenerated if the program supports color management, but it
 | 
				
			||||||
 | 
					has found a thumbnail without a color space field.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					A _normal_-sized old-specification thumbnail may be produced alongside any
 | 
				
			||||||
 | 
					wide ones, but it is strongly suggested to avoid duplicating the larger sizes.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					References
 | 
				
			||||||
 | 
					----------
 | 
				
			||||||
 | 
					 - https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html[Thumbnail Managing Standard];
 | 
				
			||||||
 | 
					 - https://developers.google.com/speed/webp/docs/riff_container[WebP Container Specification];
 | 
				
			||||||
 | 
					 - https://github.com/rurban/smhasher[smhasher -- Hash function quality and speed tests]
 | 
				
			||||||
 | 
					   evaluates MD5 as an excellent non-cryptographic hash function;
 | 
				
			||||||
 | 
					 - https://datatracker.ietf.org/doc/html/rfc3629[RFC 3629 -- UTF-8, a transformation format of ISO 10646];
 | 
				
			||||||
 | 
					 - IEC 61966-2-1:1999 -- sRGB is "only" available commercially, but reduces to
 | 
				
			||||||
 | 
					   https://www.color.org/chardata/rgb/srgb.xalter[these characteristics];
 | 
				
			||||||
 | 
					 - https://developer.apple.com/documentation/coregraphics/cgcolorspace/1408916-displayp3[Apple's brief Display P3 definition]
 | 
				
			||||||
 | 
					   and the corresponding
 | 
				
			||||||
 | 
					   https://www.color.org/chardata/rgb/DCIP3.xalter[DCI-P3 characteristics].
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Change history
 | 
				
			||||||
 | 
					--------------
 | 
				
			||||||
 | 
					v0.1, 2021-12-29, Přemysl Eric Janouch::
 | 
				
			||||||
 | 
						Preliminary draft.
 | 
				
			||||||
							
								
								
									
										1069
									
								
								fastiv-browser.c
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										2002
									
								
								fastiv-io.c
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										109
									
								
								fastiv-io.h
									
									
									
									
									
								
							
							
						
						@@ -1,109 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// fastiv-io.h: image loaders
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
					 | 
				
			||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
					 | 
				
			||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
					 | 
				
			||||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
					 | 
				
			||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
					 | 
				
			||||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
					 | 
				
			||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#pragma once
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <cairo.h>
 | 
					 | 
				
			||||||
#include <glib.h>
 | 
					 | 
				
			||||||
#include <gio/gio.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
extern const char *fastiv_io_supported_media_types[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
char **fastiv_io_all_supported_media_types(void);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Userdata are typically attached to all Cairo surfaces in an animation.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// GBytes with plain Exif/TIFF data.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_exif;
 | 
					 | 
				
			||||||
/// FastivIoOrientation, as a uintptr_t.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_orientation;
 | 
					 | 
				
			||||||
/// GBytes with plain ICC profile data.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_icc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// The next frame in a sequence, as a surface, in a chain, pre-composited.
 | 
					 | 
				
			||||||
/// There is no wrap-around.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_frame_next;
 | 
					 | 
				
			||||||
/// The previous frame in a sequence, as a surface, in a chain, pre-composited.
 | 
					 | 
				
			||||||
/// This is a weak pointer that wraps around, and needn't be present
 | 
					 | 
				
			||||||
/// for static images.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_frame_previous;
 | 
					 | 
				
			||||||
/// Frame duration in milliseconds as an intptr_t.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_frame_duration;
 | 
					 | 
				
			||||||
/// How many times to repeat the animation, or zero for +inf, as a uintptr_t.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_loops;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/// The first frame of the next page, as a surface, in a chain.
 | 
					 | 
				
			||||||
/// There is no wrap-around.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_page_next;
 | 
					 | 
				
			||||||
/// The first frame of the previous page, as a surface, in a chain.
 | 
					 | 
				
			||||||
/// There is no wrap-around. This is a weak pointer.
 | 
					 | 
				
			||||||
extern cairo_user_data_key_t fastiv_io_key_page_previous;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
cairo_surface_t *fastiv_io_open(const gchar *path, GError **error);
 | 
					 | 
				
			||||||
cairo_surface_t *fastiv_io_open_from_data(
 | 
					 | 
				
			||||||
	const char *data, size_t len, const gchar *path, GError **error);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int fastiv_io_filecmp(GFile *f1, GFile *f2);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Metadata ----------------------------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
 | 
					 | 
				
			||||||
typedef enum _FastivIoOrientation {
 | 
					 | 
				
			||||||
	FastivIoOrientationUnknown   = 0,
 | 
					 | 
				
			||||||
	FastivIoOrientation0         = 1,
 | 
					 | 
				
			||||||
	FastivIoOrientationMirror0   = 2,
 | 
					 | 
				
			||||||
	FastivIoOrientation180       = 3,
 | 
					 | 
				
			||||||
	FastivIoOrientationMirror180 = 4,
 | 
					 | 
				
			||||||
	FastivIoOrientationMirror270 = 5,
 | 
					 | 
				
			||||||
	FastivIoOrientation90        = 6,
 | 
					 | 
				
			||||||
	FastivIoOrientationMirror90  = 7,
 | 
					 | 
				
			||||||
	FastivIoOrientation270       = 8
 | 
					 | 
				
			||||||
} FastivIoOrientation;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
FastivIoOrientation fastiv_io_exif_orientation(const guint8 *exif, gsize len);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Thumbnails --------------------------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// And this is how you avoid glib-mkenums.
 | 
					 | 
				
			||||||
typedef enum _FastivIoThumbnailSize {
 | 
					 | 
				
			||||||
#define FASTIV_IO_THUMBNAIL_SIZES(XX) \
 | 
					 | 
				
			||||||
	XX(SMALL,  128, "normal") \
 | 
					 | 
				
			||||||
	XX(NORMAL, 256, "large") \
 | 
					 | 
				
			||||||
	XX(LARGE,  512, "x-large") \
 | 
					 | 
				
			||||||
	XX(HUGE,  1024, "xx-large")
 | 
					 | 
				
			||||||
#define XX(name, value, dir) FASTIV_IO_THUMBNAIL_SIZE_ ## name,
 | 
					 | 
				
			||||||
	FASTIV_IO_THUMBNAIL_SIZES(XX)
 | 
					 | 
				
			||||||
#undef XX
 | 
					 | 
				
			||||||
	FASTIV_IO_THUMBNAIL_SIZE_COUNT,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	FASTIV_IO_THUMBNAIL_SIZE_MIN = 0,
 | 
					 | 
				
			||||||
	FASTIV_IO_THUMBNAIL_SIZE_MAX = FASTIV_IO_THUMBNAIL_SIZE_COUNT - 1
 | 
					 | 
				
			||||||
} FastivIoThumbnailSize;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
GType fastiv_io_thumbnail_size_get_type(void) G_GNUC_CONST;
 | 
					 | 
				
			||||||
#define FASTIV_TYPE_IO_THUMBNAIL_SIZE (fastiv_io_thumbnail_size_get_type())
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
typedef struct _FastivIoThumbnailSizeInfo {
 | 
					 | 
				
			||||||
	int size;                           ///< Nominal size in pixels
 | 
					 | 
				
			||||||
	const char *thumbnail_spec_name;    ///< thumbnail-spec directory name
 | 
					 | 
				
			||||||
} FastivIoThumbnailSizeInfo;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
extern FastivIoThumbnailSizeInfo
 | 
					 | 
				
			||||||
	fastiv_io_thumbnail_sizes[FASTIV_IO_THUMBNAIL_SIZE_COUNT];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
cairo_surface_t *fastiv_io_lookup_thumbnail(
 | 
					 | 
				
			||||||
	GFile *target, FastivIoThumbnailSize size);
 | 
					 | 
				
			||||||
							
								
								
									
										433
									
								
								fastiv-sidebar.c
									
									
									
									
									
								
							
							
						
						@@ -1,433 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// fastiv-sidebar.c: molesting GtkPlacesSidebar
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
					 | 
				
			||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
					 | 
				
			||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
					 | 
				
			||||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
					 | 
				
			||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
					 | 
				
			||||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
					 | 
				
			||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <gtk/gtk.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include "fastiv-io.h"  // fastiv_io_filecmp
 | 
					 | 
				
			||||||
#include "fastiv-sidebar.h"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct _FastivSidebar {
 | 
					 | 
				
			||||||
	GtkScrolledWindow parent_instance;
 | 
					 | 
				
			||||||
	GtkPlacesSidebar *places;
 | 
					 | 
				
			||||||
	GtkWidget *toolbar;
 | 
					 | 
				
			||||||
	GtkWidget *listbox;
 | 
					 | 
				
			||||||
	GFile *location;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
G_DEFINE_TYPE(FastivSidebar, fastiv_sidebar, GTK_TYPE_SCROLLED_WINDOW)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
G_DEFINE_QUARK(fastiv-sidebar-location-quark, fastiv_sidebar_location)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum {
 | 
					 | 
				
			||||||
	OPEN_LOCATION,
 | 
					 | 
				
			||||||
	LAST_SIGNAL,
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Globals are, sadly, the canonical way of storing signal numbers.
 | 
					 | 
				
			||||||
static guint sidebar_signals[LAST_SIGNAL];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_sidebar_dispose(GObject *gobject)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivSidebar *self = FASTIV_SIDEBAR(gobject);
 | 
					 | 
				
			||||||
	g_clear_object(&self->location);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	G_OBJECT_CLASS(fastiv_sidebar_parent_class)->dispose(gobject);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_sidebar_class_init(FastivSidebarClass *klass)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
					 | 
				
			||||||
	object_class->dispose = fastiv_sidebar_dispose;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// You're giving me no choice, Adwaita.
 | 
					 | 
				
			||||||
	// Your style is hardcoded to match against the class' CSS name.
 | 
					 | 
				
			||||||
	// And I need replicate the internal widget structure.
 | 
					 | 
				
			||||||
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
 | 
					 | 
				
			||||||
	gtk_widget_class_set_css_name(widget_class, "placessidebar");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO(p): Consider a return value, and using it.
 | 
					 | 
				
			||||||
	sidebar_signals[OPEN_LOCATION] =
 | 
					 | 
				
			||||||
		g_signal_new("open_location", G_TYPE_FROM_CLASS(klass), 0, 0,
 | 
					 | 
				
			||||||
			NULL, NULL, NULL, G_TYPE_NONE,
 | 
					 | 
				
			||||||
			2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
on_rowlabel_query_tooltip(GtkWidget *widget,
 | 
					 | 
				
			||||||
	G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y,
 | 
					 | 
				
			||||||
	G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GtkLabel *label = GTK_LABEL(widget);
 | 
					 | 
				
			||||||
	if (!pango_layout_is_ellipsized(gtk_label_get_layout(label)))
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_tooltip_set_text(tooltip, gtk_label_get_text(label));
 | 
					 | 
				
			||||||
	return TRUE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static GtkWidget *
 | 
					 | 
				
			||||||
create_row(GFile *file, const char *icon_name)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// TODO(p): Handle errors better.
 | 
					 | 
				
			||||||
	GFileInfo *info =
 | 
					 | 
				
			||||||
		g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
 | 
					 | 
				
			||||||
			G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, NULL);
 | 
					 | 
				
			||||||
	if (!info)
 | 
					 | 
				
			||||||
		return NULL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	const char *name = g_file_info_get_display_name(info);
 | 
					 | 
				
			||||||
	GtkWidget *rowbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *rowimage =
 | 
					 | 
				
			||||||
		gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU);
 | 
					 | 
				
			||||||
	gtk_style_context_add_class(
 | 
					 | 
				
			||||||
		gtk_widget_get_style_context(rowimage), "sidebar-icon");
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(rowbox), rowimage);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *rowlabel = gtk_label_new(name);
 | 
					 | 
				
			||||||
	gtk_label_set_ellipsize(GTK_LABEL(rowlabel), PANGO_ELLIPSIZE_END);
 | 
					 | 
				
			||||||
	gtk_widget_set_has_tooltip(rowlabel, TRUE);
 | 
					 | 
				
			||||||
	g_signal_connect(rowlabel, "query-tooltip",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_rowlabel_query_tooltip), NULL);
 | 
					 | 
				
			||||||
	gtk_style_context_add_class(
 | 
					 | 
				
			||||||
		gtk_widget_get_style_context(rowlabel), "sidebar-label");
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(rowbox), rowlabel);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *revealer = gtk_revealer_new();
 | 
					 | 
				
			||||||
	gtk_revealer_set_reveal_child(
 | 
					 | 
				
			||||||
		GTK_REVEALER(revealer), TRUE);
 | 
					 | 
				
			||||||
	gtk_revealer_set_transition_type(
 | 
					 | 
				
			||||||
		GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_NONE);
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(revealer), rowbox);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *row = gtk_list_box_row_new();
 | 
					 | 
				
			||||||
	g_object_set_qdata_full(G_OBJECT(row), fastiv_sidebar_location_quark(),
 | 
					 | 
				
			||||||
		g_object_ref(file), (GDestroyNotify) g_object_unref);
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(row), revealer);
 | 
					 | 
				
			||||||
	gtk_widget_show_all(row);
 | 
					 | 
				
			||||||
	return row;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gint
 | 
					 | 
				
			||||||
listbox_compare(
 | 
					 | 
				
			||||||
	GtkListBoxRow *row1, GtkListBoxRow *row2, G_GNUC_UNUSED gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	return fastiv_io_filecmp(
 | 
					 | 
				
			||||||
		g_object_get_qdata(G_OBJECT(row1), fastiv_sidebar_location_quark()),
 | 
					 | 
				
			||||||
		g_object_get_qdata(G_OBJECT(row2), fastiv_sidebar_location_quark()));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
update_location(FastivSidebar *self, GFile *location)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	if (location) {
 | 
					 | 
				
			||||||
		g_clear_object(&self->location);
 | 
					 | 
				
			||||||
		self->location = g_object_ref(location);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_places_sidebar_set_location(self->places, self->location);
 | 
					 | 
				
			||||||
	gtk_container_foreach(GTK_CONTAINER(self->listbox),
 | 
					 | 
				
			||||||
		(GtkCallback) gtk_widget_destroy, NULL);
 | 
					 | 
				
			||||||
	g_return_if_fail(self->location != NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GFile *iter = g_object_ref(self->location);
 | 
					 | 
				
			||||||
	while (TRUE) {
 | 
					 | 
				
			||||||
		GFile *parent = g_file_get_parent(iter);
 | 
					 | 
				
			||||||
		g_object_unref(iter);
 | 
					 | 
				
			||||||
		if (!(iter = parent))
 | 
					 | 
				
			||||||
			break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		gtk_list_box_prepend(GTK_LIST_BOX(self->listbox),
 | 
					 | 
				
			||||||
			create_row(parent, "go-up-symbolic"));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Other options are "folder-{visiting,open}-symbolic", though the former
 | 
					 | 
				
			||||||
	// is mildly inappropriate (means: open in another window).
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(self->listbox),
 | 
					 | 
				
			||||||
		create_row(self->location, "circle-filled-symbolic"));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GFileEnumerator *enumerator = g_file_enumerate_children(self->location,
 | 
					 | 
				
			||||||
		G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME
 | 
					 | 
				
			||||||
		"," G_FILE_ATTRIBUTE_STANDARD_NAME
 | 
					 | 
				
			||||||
		"," G_FILE_ATTRIBUTE_STANDARD_TYPE
 | 
					 | 
				
			||||||
		"," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
 | 
					 | 
				
			||||||
		G_FILE_QUERY_INFO_NONE, NULL, NULL);
 | 
					 | 
				
			||||||
	if (!enumerator)
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO(p): gtk_list_box_set_filter_func(), or even use a model,
 | 
					 | 
				
			||||||
	// which could be shared with FastivBrowser.
 | 
					 | 
				
			||||||
	while (TRUE) {
 | 
					 | 
				
			||||||
		GFileInfo *info = NULL;
 | 
					 | 
				
			||||||
		GFile *child = NULL;
 | 
					 | 
				
			||||||
		if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) ||
 | 
					 | 
				
			||||||
			!info)
 | 
					 | 
				
			||||||
			break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY &&
 | 
					 | 
				
			||||||
			!g_file_info_get_is_hidden(info))
 | 
					 | 
				
			||||||
			gtk_container_add(GTK_CONTAINER(self->listbox),
 | 
					 | 
				
			||||||
				create_row(child, "go-down-symbolic"));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	g_object_unref(enumerator);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_open_breadcrumb(
 | 
					 | 
				
			||||||
	G_GNUC_UNUSED GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivSidebar *self = FASTIV_SIDEBAR(user_data);
 | 
					 | 
				
			||||||
	GFile *location =
 | 
					 | 
				
			||||||
		g_object_get_qdata(G_OBJECT(row), fastiv_sidebar_location_quark());
 | 
					 | 
				
			||||||
	g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0,
 | 
					 | 
				
			||||||
		location, GTK_PLACES_OPEN_NORMAL);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,
 | 
					 | 
				
			||||||
	GtkPlacesOpenFlags flags, gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivSidebar *self = FASTIV_SIDEBAR(user_data);
 | 
					 | 
				
			||||||
	g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Deselect the item in GtkPlacesSidebar, if unsuccessful.
 | 
					 | 
				
			||||||
	update_location(self, NULL);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
complete_path(GFile *location, GtkListStore *model)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// TODO(p): Do not enter directories unless followed by '/'.
 | 
					 | 
				
			||||||
	// This information has already been stripped from `location`.
 | 
					 | 
				
			||||||
	GFile *parent = G_FILE_TYPE_DIRECTORY ==
 | 
					 | 
				
			||||||
		g_file_query_file_type(location, G_FILE_QUERY_INFO_NONE, NULL)
 | 
					 | 
				
			||||||
		? g_object_ref(location)
 | 
					 | 
				
			||||||
		: g_file_get_parent(location);
 | 
					 | 
				
			||||||
	if (!parent)
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GFileEnumerator *enumerator = g_file_enumerate_children(parent,
 | 
					 | 
				
			||||||
		G_FILE_ATTRIBUTE_STANDARD_NAME
 | 
					 | 
				
			||||||
		"," G_FILE_ATTRIBUTE_STANDARD_TYPE
 | 
					 | 
				
			||||||
		"," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
 | 
					 | 
				
			||||||
		G_FILE_QUERY_INFO_NONE, NULL, NULL);
 | 
					 | 
				
			||||||
	if (!enumerator)
 | 
					 | 
				
			||||||
		goto fail_enumerator;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	while (TRUE) {
 | 
					 | 
				
			||||||
		GFileInfo *info = NULL;
 | 
					 | 
				
			||||||
		GFile *child = NULL;
 | 
					 | 
				
			||||||
		if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) ||
 | 
					 | 
				
			||||||
			!info)
 | 
					 | 
				
			||||||
			break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY ||
 | 
					 | 
				
			||||||
			g_file_info_get_is_hidden(info))
 | 
					 | 
				
			||||||
			continue;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		char *parse_name = g_file_get_parse_name(child);
 | 
					 | 
				
			||||||
		if (!g_str_has_suffix(parse_name, G_DIR_SEPARATOR_S)) {
 | 
					 | 
				
			||||||
			char *save = parse_name;
 | 
					 | 
				
			||||||
			parse_name = g_strdup_printf("%s%c", parse_name, G_DIR_SEPARATOR);
 | 
					 | 
				
			||||||
			g_free(save);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		gtk_list_store_insert_with_values(model, NULL, -1, 0, parse_name, -1);
 | 
					 | 
				
			||||||
		g_free(parse_name);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g_object_unref(enumerator);
 | 
					 | 
				
			||||||
fail_enumerator:
 | 
					 | 
				
			||||||
	g_object_unref(parent);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static GFile *
 | 
					 | 
				
			||||||
resolve_location(FastivSidebar *self, const char *text)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// Relative paths produce invalid GFile objects with this function.
 | 
					 | 
				
			||||||
	// And even if they didn't, we have our own root for them.
 | 
					 | 
				
			||||||
	GFile *file = g_file_parse_name(text);
 | 
					 | 
				
			||||||
	if (g_uri_is_valid(text, G_URI_FLAGS_PARSE_RELAXED, NULL) ||
 | 
					 | 
				
			||||||
		g_file_peek_path(file))
 | 
					 | 
				
			||||||
		return file;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GFile *absolute =
 | 
					 | 
				
			||||||
		g_file_get_child_for_display_name(self->location, text, NULL);
 | 
					 | 
				
			||||||
	if (!absolute)
 | 
					 | 
				
			||||||
		return file;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g_object_unref(file);
 | 
					 | 
				
			||||||
	return absolute;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_enter_location_changed(GtkEntry *entry, gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivSidebar *self = FASTIV_SIDEBAR(user_data);
 | 
					 | 
				
			||||||
	const char *text = gtk_entry_get_text(entry);
 | 
					 | 
				
			||||||
	GFile *location = resolve_location(self, text);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Don't touch the network anywhere around here, URIs are a no-no.
 | 
					 | 
				
			||||||
	GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(entry));
 | 
					 | 
				
			||||||
	if (!g_file_peek_path(location) || g_file_query_exists(location, NULL))
 | 
					 | 
				
			||||||
		gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING);
 | 
					 | 
				
			||||||
	else
 | 
					 | 
				
			||||||
		gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// XXX: For some reason, this jumps around with longer lists.
 | 
					 | 
				
			||||||
	GtkEntryCompletion *completion = gtk_entry_get_completion(entry);
 | 
					 | 
				
			||||||
	GtkTreeModel *model = gtk_entry_completion_get_model(completion);
 | 
					 | 
				
			||||||
	gtk_list_store_clear(GTK_LIST_STORE(model));
 | 
					 | 
				
			||||||
	if (g_file_peek_path(location))
 | 
					 | 
				
			||||||
		complete_path(location, GTK_LIST_STORE(model));
 | 
					 | 
				
			||||||
	g_object_unref(location);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_show_enter_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar,
 | 
					 | 
				
			||||||
	G_GNUC_UNUSED gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivSidebar *self = FASTIV_SIDEBAR(user_data);
 | 
					 | 
				
			||||||
	GtkWidget *dialog = gtk_dialog_new_with_buttons("Enter location",
 | 
					 | 
				
			||||||
		GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(self))),
 | 
					 | 
				
			||||||
		GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL |
 | 
					 | 
				
			||||||
			GTK_DIALOG_USE_HEADER_BAR,
 | 
					 | 
				
			||||||
		"_Open", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkListStore *model = gtk_list_store_new(1, G_TYPE_STRING);
 | 
					 | 
				
			||||||
	gtk_tree_sortable_set_sort_column_id(
 | 
					 | 
				
			||||||
		GTK_TREE_SORTABLE(model), 0, GTK_SORT_ASCENDING);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkEntryCompletion *completion = gtk_entry_completion_new();
 | 
					 | 
				
			||||||
	gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
 | 
					 | 
				
			||||||
	gtk_entry_completion_set_text_column(completion, 0);
 | 
					 | 
				
			||||||
	// TODO(p): Complete ~ paths so that they start with ~, then we can filter.
 | 
					 | 
				
			||||||
	gtk_entry_completion_set_match_func(
 | 
					 | 
				
			||||||
		completion, (GtkEntryCompletionMatchFunc) gtk_true, NULL, NULL);
 | 
					 | 
				
			||||||
	g_object_unref(model);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *entry = gtk_entry_new();
 | 
					 | 
				
			||||||
	gtk_entry_set_completion(GTK_ENTRY(entry), completion);
 | 
					 | 
				
			||||||
	gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
 | 
					 | 
				
			||||||
	g_signal_connect(entry, "changed",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_enter_location_changed), self);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(content), entry);
 | 
					 | 
				
			||||||
	gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
 | 
					 | 
				
			||||||
	gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
 | 
					 | 
				
			||||||
	gtk_window_set_default_size(GTK_WINDOW(dialog), 800, -1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GdkGeometry geometry = {.max_width = G_MAXSHORT, .max_height = -1};
 | 
					 | 
				
			||||||
	gtk_window_set_geometry_hints(
 | 
					 | 
				
			||||||
		GTK_WINDOW(dialog), NULL, &geometry, GDK_HINT_MAX_SIZE);
 | 
					 | 
				
			||||||
	gtk_widget_show_all(dialog);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
 | 
					 | 
				
			||||||
		const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
 | 
					 | 
				
			||||||
		GFile *location = resolve_location(self, text);
 | 
					 | 
				
			||||||
		g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0,
 | 
					 | 
				
			||||||
			location, GTK_PLACES_OPEN_NORMAL);
 | 
					 | 
				
			||||||
		g_object_unref(location);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	gtk_widget_destroy(dialog);
 | 
					 | 
				
			||||||
	g_object_unref(completion);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Deselect the item in GtkPlacesSidebar, if unsuccessful.
 | 
					 | 
				
			||||||
	update_location(self, NULL);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_sidebar_init(FastivSidebar *self)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// TODO(p): Transplant functionality from the shitty GtkPlacesSidebar.
 | 
					 | 
				
			||||||
	// We cannot reasonably place any new items within its own GtkListBox,
 | 
					 | 
				
			||||||
	// so we need to replicate the style hierarchy to some extent.
 | 
					 | 
				
			||||||
	self->places = GTK_PLACES_SIDEBAR(gtk_places_sidebar_new());
 | 
					 | 
				
			||||||
	gtk_places_sidebar_set_show_recent(self->places, FALSE);
 | 
					 | 
				
			||||||
	gtk_places_sidebar_set_show_trash(self->places, FALSE);
 | 
					 | 
				
			||||||
	gtk_places_sidebar_set_open_flags(self->places,
 | 
					 | 
				
			||||||
		GTK_PLACES_OPEN_NORMAL | GTK_PLACES_OPEN_NEW_WINDOW);
 | 
					 | 
				
			||||||
	g_signal_connect(self->places, "open-location",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_open_location), self);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_places_sidebar_set_show_enter_location(self->places, TRUE);
 | 
					 | 
				
			||||||
	g_signal_connect(self->places, "show-enter-location",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_show_enter_location), self);
 | 
					 | 
				
			||||||
	gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->places),
 | 
					 | 
				
			||||||
		GTK_POLICY_NEVER, GTK_POLICY_NEVER);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// None of GtkActionBar, GtkToolbar, .inline-toolbar is appropriate.
 | 
					 | 
				
			||||||
	// It is either side-favouring borders or excess button padding.
 | 
					 | 
				
			||||||
	self->toolbar = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 12);
 | 
					 | 
				
			||||||
	gtk_style_context_add_class(
 | 
					 | 
				
			||||||
		gtk_widget_get_style_context(self->toolbar), GTK_STYLE_CLASS_TOOLBAR);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	self->listbox = gtk_list_box_new();
 | 
					 | 
				
			||||||
	gtk_list_box_set_selection_mode(
 | 
					 | 
				
			||||||
		GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE);
 | 
					 | 
				
			||||||
	g_signal_connect(self->listbox, "row-activated",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_open_breadcrumb), self);
 | 
					 | 
				
			||||||
	gtk_list_box_set_sort_func(
 | 
					 | 
				
			||||||
		GTK_LIST_BOX(self->listbox), listbox_compare, self, NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Fill up what would otherwise be wasted space,
 | 
					 | 
				
			||||||
	// as it is in the examples of Nautilus and Thunar.
 | 
					 | 
				
			||||||
	GtkWidget *superbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
 | 
					 | 
				
			||||||
	gtk_container_add(
 | 
					 | 
				
			||||||
		GTK_CONTAINER(superbox), GTK_WIDGET(self->places));
 | 
					 | 
				
			||||||
	gtk_container_add(
 | 
					 | 
				
			||||||
		GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL));
 | 
					 | 
				
			||||||
	gtk_container_add(
 | 
					 | 
				
			||||||
		GTK_CONTAINER(superbox), self->toolbar);
 | 
					 | 
				
			||||||
	gtk_container_add(
 | 
					 | 
				
			||||||
		GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL));
 | 
					 | 
				
			||||||
	gtk_container_add(
 | 
					 | 
				
			||||||
		GTK_CONTAINER(superbox), self->listbox);
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(self), superbox);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_scrolled_window_set_policy(
 | 
					 | 
				
			||||||
		GTK_SCROLLED_WINDOW(self), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
 | 
					 | 
				
			||||||
	gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)),
 | 
					 | 
				
			||||||
		GTK_STYLE_CLASS_SIDEBAR);
 | 
					 | 
				
			||||||
	gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)),
 | 
					 | 
				
			||||||
		"fastiv");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Public interface --------------------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void
 | 
					 | 
				
			||||||
fastiv_sidebar_set_location(FastivSidebar *self, GFile *location)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	g_return_if_fail(FASTIV_IS_SIDEBAR(self));
 | 
					 | 
				
			||||||
	update_location(self, location);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void
 | 
					 | 
				
			||||||
fastiv_sidebar_show_enter_location(FastivSidebar *self)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	g_return_if_fail(FASTIV_IS_SIDEBAR(self));
 | 
					 | 
				
			||||||
	g_signal_emit_by_name(self->places, "show-enter-location");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
GtkBox *
 | 
					 | 
				
			||||||
fastiv_sidebar_get_toolbar(FastivSidebar *self)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	g_return_val_if_fail(FASTIV_IS_SIDEBAR(self), NULL);
 | 
					 | 
				
			||||||
	return GTK_BOX(self->toolbar);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										704
									
								
								fastiv-view.c
									
									
									
									
									
								
							
							
						
						@@ -1,704 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// fastiv-view.c: fast image viewer - view widget
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
					 | 
				
			||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
					 | 
				
			||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
					 | 
				
			||||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
					 | 
				
			||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
					 | 
				
			||||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
					 | 
				
			||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <math.h>
 | 
					 | 
				
			||||||
#include <stdbool.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <gtk/gtk.h>
 | 
					 | 
				
			||||||
#ifdef GDK_WINDOWING_X11
 | 
					 | 
				
			||||||
#include <gdk/gdkx.h>
 | 
					 | 
				
			||||||
#endif  // GDK_WINDOWING_X11
 | 
					 | 
				
			||||||
#ifdef GDK_WINDOWING_QUARTZ
 | 
					 | 
				
			||||||
#include <gdk/gdkquartz.h>
 | 
					 | 
				
			||||||
#endif  // GDK_WINDOWING_QUARTZ
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include "fastiv-io.h"
 | 
					 | 
				
			||||||
#include "fastiv-view.h"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct _FastivView {
 | 
					 | 
				
			||||||
	GtkWidget parent_instance;
 | 
					 | 
				
			||||||
	cairo_surface_t *image;             ///< The loaded image (sequence)
 | 
					 | 
				
			||||||
	cairo_surface_t *page;              ///< Current page within image, weak
 | 
					 | 
				
			||||||
	cairo_surface_t *frame;             ///< Current frame within page, weak
 | 
					 | 
				
			||||||
	FastivIoOrientation orientation;    ///< Current page orientation
 | 
					 | 
				
			||||||
	bool filter;                        ///< Smooth scaling toggle
 | 
					 | 
				
			||||||
	bool scale_to_fit;                  ///< Image no larger than the allocation
 | 
					 | 
				
			||||||
	double scale;                       ///< Scaling factor
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	int remaining_loops;                ///< Greater than zero if limited
 | 
					 | 
				
			||||||
	gint64 frame_time;                  ///< Current frame's start, µs precision
 | 
					 | 
				
			||||||
	gulong frame_update_connection;     ///< GdkFrameClock::update
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
G_DEFINE_TYPE(FastivView, fastiv_view, GTK_TYPE_WIDGET)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static FastivIoOrientation view_left[9] = {
 | 
					 | 
				
			||||||
	[FastivIoOrientationUnknown] = FastivIoOrientationUnknown,
 | 
					 | 
				
			||||||
	[FastivIoOrientation0] = FastivIoOrientation270,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror0] = FastivIoOrientationMirror270,
 | 
					 | 
				
			||||||
	[FastivIoOrientation180] = FastivIoOrientation90,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror180] = FastivIoOrientationMirror90,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror270] = FastivIoOrientationMirror180,
 | 
					 | 
				
			||||||
	[FastivIoOrientation90] = FastivIoOrientation0,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror90] = FastivIoOrientationMirror0,
 | 
					 | 
				
			||||||
	[FastivIoOrientation270] = FastivIoOrientation180
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static FastivIoOrientation view_mirror[9] = {
 | 
					 | 
				
			||||||
	[FastivIoOrientationUnknown] = FastivIoOrientationUnknown,
 | 
					 | 
				
			||||||
	[FastivIoOrientation0] = FastivIoOrientationMirror0,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror0] = FastivIoOrientation0,
 | 
					 | 
				
			||||||
	[FastivIoOrientation180] = FastivIoOrientationMirror180,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror180] = FastivIoOrientation180,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror270] = FastivIoOrientation270,
 | 
					 | 
				
			||||||
	[FastivIoOrientation90] = FastivIoOrientationMirror270,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror90] = FastivIoOrientation90,
 | 
					 | 
				
			||||||
	[FastivIoOrientation270] = FastivIoOrientationMirror270
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static FastivIoOrientation view_right[9] = {
 | 
					 | 
				
			||||||
	[FastivIoOrientationUnknown] = FastivIoOrientationUnknown,
 | 
					 | 
				
			||||||
	[FastivIoOrientation0] = FastivIoOrientation90,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror0] = FastivIoOrientationMirror90,
 | 
					 | 
				
			||||||
	[FastivIoOrientation180] = FastivIoOrientation270,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror180] = FastivIoOrientationMirror270,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror270] = FastivIoOrientationMirror0,
 | 
					 | 
				
			||||||
	[FastivIoOrientation90] = FastivIoOrientation180,
 | 
					 | 
				
			||||||
	[FastivIoOrientationMirror90] = FastivIoOrientationMirror180,
 | 
					 | 
				
			||||||
	[FastivIoOrientation270] = FastivIoOrientation0
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum {
 | 
					 | 
				
			||||||
	PROP_SCALE = 1,
 | 
					 | 
				
			||||||
	PROP_SCALE_TO_FIT,
 | 
					 | 
				
			||||||
	N_PROPERTIES
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static GParamSpec *view_properties[N_PROPERTIES];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_finalize(GObject *gobject)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(gobject);
 | 
					 | 
				
			||||||
	cairo_surface_destroy(self->image);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	G_OBJECT_CLASS(fastiv_view_parent_class)->finalize(gobject);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_get_property(
 | 
					 | 
				
			||||||
	GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(object);
 | 
					 | 
				
			||||||
	switch (property_id) {
 | 
					 | 
				
			||||||
	case PROP_SCALE:
 | 
					 | 
				
			||||||
		g_value_set_double(value, self->scale);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case PROP_SCALE_TO_FIT:
 | 
					 | 
				
			||||||
		g_value_set_boolean(value, self->scale_to_fit);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
get_surface_dimensions(FastivView *self, double *width, double *height)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	*width = *height = 0;
 | 
					 | 
				
			||||||
	if (!self->image)
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cairo_rectangle_t extents = {};
 | 
					 | 
				
			||||||
	switch (cairo_surface_get_type(self->page)) {
 | 
					 | 
				
			||||||
	case CAIRO_SURFACE_TYPE_IMAGE:
 | 
					 | 
				
			||||||
		switch (self->orientation) {
 | 
					 | 
				
			||||||
		case FastivIoOrientation90:
 | 
					 | 
				
			||||||
		case FastivIoOrientationMirror90:
 | 
					 | 
				
			||||||
		case FastivIoOrientation270:
 | 
					 | 
				
			||||||
		case FastivIoOrientationMirror270:
 | 
					 | 
				
			||||||
			*width = cairo_image_surface_get_height(self->page);
 | 
					 | 
				
			||||||
			*height = cairo_image_surface_get_width(self->page);
 | 
					 | 
				
			||||||
			break;
 | 
					 | 
				
			||||||
		default:
 | 
					 | 
				
			||||||
			*width = cairo_image_surface_get_width(self->page);
 | 
					 | 
				
			||||||
			*height = cairo_image_surface_get_height(self->page);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	case CAIRO_SURFACE_TYPE_RECORDING:
 | 
					 | 
				
			||||||
		if (!cairo_recording_surface_get_extents(self->page, &extents)) {
 | 
					 | 
				
			||||||
			cairo_recording_surface_ink_extents(self->page,
 | 
					 | 
				
			||||||
				&extents.x, &extents.y, &extents.width, &extents.height);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		*width = extents.width;
 | 
					 | 
				
			||||||
		*height = extents.height;
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		g_assert_not_reached();
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
get_display_dimensions(FastivView *self, int *width, int *height)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	double w, h;
 | 
					 | 
				
			||||||
	get_surface_dimensions(self, &w, &h);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	*width = ceil(w * self->scale);
 | 
					 | 
				
			||||||
	*height = ceil(h * self->scale);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_get_preferred_height(
 | 
					 | 
				
			||||||
	GtkWidget *widget, gint *minimum, gint *natural)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(widget);
 | 
					 | 
				
			||||||
	if (self->scale_to_fit) {
 | 
					 | 
				
			||||||
		double sw, sh;
 | 
					 | 
				
			||||||
		get_surface_dimensions(self, &sw, &sh);
 | 
					 | 
				
			||||||
		*natural = ceil(sh);
 | 
					 | 
				
			||||||
		*minimum = 1;
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		int dw, dh;
 | 
					 | 
				
			||||||
		get_display_dimensions(self, &dw, &dh);
 | 
					 | 
				
			||||||
		*minimum = *natural = dh;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_get_preferred_width(GtkWidget *widget, gint *minimum, gint *natural)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(widget);
 | 
					 | 
				
			||||||
	if (self->scale_to_fit) {
 | 
					 | 
				
			||||||
		double sw, sh;
 | 
					 | 
				
			||||||
		get_surface_dimensions(self, &sw, &sh);
 | 
					 | 
				
			||||||
		*natural = ceil(sw);
 | 
					 | 
				
			||||||
		*minimum = 1;
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		int dw, dh;
 | 
					 | 
				
			||||||
		get_display_dimensions(self, &dw, &dh);
 | 
					 | 
				
			||||||
		*minimum = *natural = dw;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_size_allocate(GtkWidget *widget, GtkAllocation *allocation)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GTK_WIDGET_CLASS(fastiv_view_parent_class)
 | 
					 | 
				
			||||||
		->size_allocate(widget, allocation);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(widget);
 | 
					 | 
				
			||||||
	if (!self->image || !self->scale_to_fit)
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	double w, h;
 | 
					 | 
				
			||||||
	get_surface_dimensions(self, &w, &h);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	self->scale = 1;
 | 
					 | 
				
			||||||
	if (ceil(w * self->scale) > allocation->width)
 | 
					 | 
				
			||||||
		self->scale = allocation->width / w;
 | 
					 | 
				
			||||||
	if (ceil(h * self->scale) > allocation->height)
 | 
					 | 
				
			||||||
		self->scale = allocation->height / h;
 | 
					 | 
				
			||||||
	g_object_notify_by_pspec(G_OBJECT(widget), view_properties[PROP_SCALE]);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_realize(GtkWidget *widget)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GtkAllocation allocation;
 | 
					 | 
				
			||||||
	gtk_widget_get_allocation(widget, &allocation);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GdkWindowAttr attributes = {
 | 
					 | 
				
			||||||
		.window_type = GDK_WINDOW_CHILD,
 | 
					 | 
				
			||||||
		.x = allocation.x,
 | 
					 | 
				
			||||||
		.y = allocation.y,
 | 
					 | 
				
			||||||
		.width = allocation.width,
 | 
					 | 
				
			||||||
		.height = allocation.height,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Input-only would presumably also work (as in GtkPathBar, e.g.),
 | 
					 | 
				
			||||||
		// but it merely seems to involve more work.
 | 
					 | 
				
			||||||
		.wclass = GDK_INPUT_OUTPUT,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Assuming here that we can't ask for a higher-precision Visual
 | 
					 | 
				
			||||||
		// than what we get automatically.
 | 
					 | 
				
			||||||
		.visual = gtk_widget_get_visual(widget),
 | 
					 | 
				
			||||||
		.event_mask = gtk_widget_get_events(widget) | GDK_SCROLL_MASK |
 | 
					 | 
				
			||||||
			GDK_KEY_PRESS_MASK | GDK_BUTTON_PRESS_MASK,
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// We need this window to receive input events at all.
 | 
					 | 
				
			||||||
	GdkWindow *window = gdk_window_new(gtk_widget_get_parent_window(widget),
 | 
					 | 
				
			||||||
		&attributes, GDK_WA_X | GDK_WA_Y | GDK_WA_VISUAL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Without the following call, or the rendering mode set to "recording",
 | 
					 | 
				
			||||||
	// RGB30 degrades to RGB24, because gdk_window_begin_paint_internal()
 | 
					 | 
				
			||||||
	// creates backing stores using cairo_content_t constants.
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	// It completely breaks the Quartz backend, so limit it to X11.
 | 
					 | 
				
			||||||
#ifdef GDK_WINDOWING_X11
 | 
					 | 
				
			||||||
	// FIXME: This causes some flicker while scrolling, because it disables
 | 
					 | 
				
			||||||
	// double buffering, see: https://gitlab.gnome.org/GNOME/gtk/-/issues/2560
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	// If GTK+'s OpenGL integration fails to deliver, we need to use the window
 | 
					 | 
				
			||||||
	// directly, sidestepping the toolkit entirely.
 | 
					 | 
				
			||||||
	if (GDK_IS_X11_WINDOW(window))
 | 
					 | 
				
			||||||
		gdk_window_ensure_native(window);
 | 
					 | 
				
			||||||
#endif  // GDK_WINDOWING_X11
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_widget_register_window(widget, window);
 | 
					 | 
				
			||||||
	gtk_widget_set_window(widget, window);
 | 
					 | 
				
			||||||
	gtk_widget_set_realized(widget, TRUE);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
fastiv_view_draw(GtkWidget *widget, cairo_t *cr)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// Placed here due to our using a native GdkWindow on X11,
 | 
					 | 
				
			||||||
	// which makes the widget have no double buffering or default background.
 | 
					 | 
				
			||||||
	GtkAllocation allocation;
 | 
					 | 
				
			||||||
	gtk_widget_get_allocation(widget, &allocation);
 | 
					 | 
				
			||||||
	gtk_render_background(gtk_widget_get_style_context(widget), cr, 0, 0,
 | 
					 | 
				
			||||||
		allocation.width, allocation.height);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(widget);
 | 
					 | 
				
			||||||
	if (!self->image ||
 | 
					 | 
				
			||||||
		!gtk_cairo_should_draw_window(cr, gtk_widget_get_window(widget)))
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	int w, h;
 | 
					 | 
				
			||||||
	double sw, sh;
 | 
					 | 
				
			||||||
	get_display_dimensions(self, &w, &h);
 | 
					 | 
				
			||||||
	get_surface_dimensions(self, &sw, &sh);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	double x = 0;
 | 
					 | 
				
			||||||
	double y = 0;
 | 
					 | 
				
			||||||
	if (w < allocation.width)
 | 
					 | 
				
			||||||
		x = round((allocation.width - w) / 2.);
 | 
					 | 
				
			||||||
	if (h < allocation.height)
 | 
					 | 
				
			||||||
		y = round((allocation.height - h) / 2.);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// FIXME: Recording surfaces do not work well with CAIRO_SURFACE_TYPE_XLIB,
 | 
					 | 
				
			||||||
	// we always get a shitty pixmap, where transparency contains junk.
 | 
					 | 
				
			||||||
	if (cairo_surface_get_type(self->frame) == CAIRO_SURFACE_TYPE_RECORDING) {
 | 
					 | 
				
			||||||
		cairo_surface_t *image =
 | 
					 | 
				
			||||||
			cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h);
 | 
					 | 
				
			||||||
		cairo_t *tcr = cairo_create(image);
 | 
					 | 
				
			||||||
		cairo_scale(tcr, self->scale, self->scale);
 | 
					 | 
				
			||||||
		cairo_set_source_surface(tcr, self->frame, 0, 0);
 | 
					 | 
				
			||||||
		cairo_paint(tcr);
 | 
					 | 
				
			||||||
		cairo_destroy(tcr);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		cairo_set_source_surface(cr, image, x, y);
 | 
					 | 
				
			||||||
		cairo_paint(cr);
 | 
					 | 
				
			||||||
		cairo_surface_destroy(image);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// XXX: The rounding together with padding may result in up to
 | 
					 | 
				
			||||||
	// a pixel's worth of made-up picture data.
 | 
					 | 
				
			||||||
	cairo_rectangle(cr, x, y, w, h);
 | 
					 | 
				
			||||||
	cairo_clip(cr);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cairo_translate(cr, x, y);
 | 
					 | 
				
			||||||
	cairo_scale(cr, self->scale, self->scale);
 | 
					 | 
				
			||||||
	cairo_set_source_surface(cr, self->frame, 0, 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cairo_matrix_t matrix = {};
 | 
					 | 
				
			||||||
	cairo_matrix_init_identity(&matrix);
 | 
					 | 
				
			||||||
	switch (self->orientation) {
 | 
					 | 
				
			||||||
	case FastivIoOrientation90:
 | 
					 | 
				
			||||||
		cairo_matrix_rotate(&matrix, -M_PI_2);
 | 
					 | 
				
			||||||
		cairo_matrix_translate(&matrix, -sw, 0);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case FastivIoOrientation180:
 | 
					 | 
				
			||||||
		cairo_matrix_scale(&matrix, -1, -1);
 | 
					 | 
				
			||||||
		cairo_matrix_translate(&matrix, -sw, -sh);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case FastivIoOrientation270:
 | 
					 | 
				
			||||||
		cairo_matrix_rotate(&matrix, +M_PI_2);
 | 
					 | 
				
			||||||
		cairo_matrix_translate(&matrix, 0, -sh);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case FastivIoOrientationMirror0:
 | 
					 | 
				
			||||||
		cairo_matrix_scale(&matrix, -1, +1);
 | 
					 | 
				
			||||||
		cairo_matrix_translate(&matrix, -sw, 0);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case FastivIoOrientationMirror90:
 | 
					 | 
				
			||||||
		cairo_matrix_rotate(&matrix, +M_PI_2);
 | 
					 | 
				
			||||||
		cairo_matrix_scale(&matrix, -1, +1);
 | 
					 | 
				
			||||||
		cairo_matrix_translate(&matrix, -sw, -sh);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case FastivIoOrientationMirror180:
 | 
					 | 
				
			||||||
		cairo_matrix_scale(&matrix, +1, -1);
 | 
					 | 
				
			||||||
		cairo_matrix_translate(&matrix, 0, -sh);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case FastivIoOrientationMirror270:
 | 
					 | 
				
			||||||
		cairo_matrix_rotate(&matrix, -M_PI_2);
 | 
					 | 
				
			||||||
		cairo_matrix_scale(&matrix, -1, +1);
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cairo_pattern_t *pattern = cairo_get_source(cr);
 | 
					 | 
				
			||||||
	cairo_pattern_set_matrix(pattern, &matrix);
 | 
					 | 
				
			||||||
	cairo_pattern_set_extend(pattern, CAIRO_EXTEND_PAD);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO(p): Prescale it ourselves to an off-screen bitmap, gamma-correctly.
 | 
					 | 
				
			||||||
	if (self->filter)
 | 
					 | 
				
			||||||
		cairo_pattern_set_filter(pattern, CAIRO_FILTER_GOOD);
 | 
					 | 
				
			||||||
	else
 | 
					 | 
				
			||||||
		cairo_pattern_set_filter(pattern, CAIRO_FILTER_NEAREST);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#ifdef GDK_WINDOWING_QUARTZ
 | 
					 | 
				
			||||||
	// Not supported there. Acts a bit like repeating, but weirdly offset.
 | 
					 | 
				
			||||||
	if (GDK_IS_QUARTZ_WINDOW(gtk_widget_get_window(widget)))
 | 
					 | 
				
			||||||
		cairo_pattern_set_extend(pattern, CAIRO_EXTEND_NONE);
 | 
					 | 
				
			||||||
#endif  // GDK_WINDOWING_QUARTZ
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	cairo_paint(cr);
 | 
					 | 
				
			||||||
	return TRUE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
fastiv_view_button_press_event(GtkWidget *widget, GdkEventButton *event)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GTK_WIDGET_CLASS(fastiv_view_parent_class)
 | 
					 | 
				
			||||||
		->button_press_event(widget, event);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (event->button == GDK_BUTTON_PRIMARY &&
 | 
					 | 
				
			||||||
		gtk_widget_get_focus_on_click(widget))
 | 
					 | 
				
			||||||
		gtk_widget_grab_focus(widget);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO(p): Use for left button scroll drag, which may rather be a gesture.
 | 
					 | 
				
			||||||
	return FALSE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#define SCALE_STEP 1.4
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
set_scale_to_fit(FastivView *self, bool scale_to_fit)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	self->scale_to_fit = scale_to_fit;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_widget_queue_resize(GTK_WIDGET(self));
 | 
					 | 
				
			||||||
	g_object_notify_by_pspec(
 | 
					 | 
				
			||||||
		G_OBJECT(self), view_properties[PROP_SCALE_TO_FIT]);
 | 
					 | 
				
			||||||
	return TRUE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
set_scale(FastivView *self, double scale)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	self->scale = scale;
 | 
					 | 
				
			||||||
	g_object_notify_by_pspec(
 | 
					 | 
				
			||||||
		G_OBJECT(self), view_properties[PROP_SCALE]);
 | 
					 | 
				
			||||||
	return set_scale_to_fit(self, false);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
fastiv_view_scroll_event(GtkWidget *widget, GdkEventScroll *event)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(widget);
 | 
					 | 
				
			||||||
	if (!self->image)
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
	if (event->state & gtk_accelerator_get_default_mod_mask())
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch (event->direction) {
 | 
					 | 
				
			||||||
	case GDK_SCROLL_UP:
 | 
					 | 
				
			||||||
		return set_scale(self, self->scale * SCALE_STEP);
 | 
					 | 
				
			||||||
	case GDK_SCROLL_DOWN:
 | 
					 | 
				
			||||||
		return set_scale(self, self->scale / SCALE_STEP);
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		// For some reason, we can also get GDK_SCROLL_SMOOTH.
 | 
					 | 
				
			||||||
		// Left/right are good to steal from GtkScrolledWindow for consistency.
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
stop_animating(FastivView *self)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self));
 | 
					 | 
				
			||||||
	if (!clock || !self->frame_update_connection)
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g_signal_handler_disconnect(clock, self->frame_update_connection);
 | 
					 | 
				
			||||||
	gdk_frame_clock_end_updating(clock);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	self->frame_time = 0;
 | 
					 | 
				
			||||||
	self->frame_update_connection = 0;
 | 
					 | 
				
			||||||
	self->remaining_loops = 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
advance_frame(FastivView *self)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	cairo_surface_t *next =
 | 
					 | 
				
			||||||
		cairo_surface_get_user_data(self->frame, &fastiv_io_key_frame_next);
 | 
					 | 
				
			||||||
	if (next) {
 | 
					 | 
				
			||||||
		self->frame = next;
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		if (self->remaining_loops && !--self->remaining_loops)
 | 
					 | 
				
			||||||
			return FALSE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		self->frame = self->page;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return TRUE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
advance_animation(FastivView *self, GdkFrameClock *clock)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	gint64 now = gdk_frame_clock_get_frame_time(clock);
 | 
					 | 
				
			||||||
	while (true) {
 | 
					 | 
				
			||||||
		// TODO(p): See if infinite frames can actually happen, and how.
 | 
					 | 
				
			||||||
		intptr_t duration = (intptr_t) cairo_surface_get_user_data(
 | 
					 | 
				
			||||||
			self->frame, &fastiv_io_key_frame_duration);
 | 
					 | 
				
			||||||
		if (duration < 0)
 | 
					 | 
				
			||||||
			return FALSE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Do not busy loop. GIF timings are given in hundredths of a second.
 | 
					 | 
				
			||||||
		if (duration == 0)
 | 
					 | 
				
			||||||
			duration = gdk_frame_timings_get_refresh_interval(
 | 
					 | 
				
			||||||
				gdk_frame_clock_get_current_timings(clock)) / 1000;
 | 
					 | 
				
			||||||
		if (duration == 0)
 | 
					 | 
				
			||||||
			duration = 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		gint64 then = self->frame_time + duration * 1000;
 | 
					 | 
				
			||||||
		if (then > now)
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		if (!advance_frame(self))
 | 
					 | 
				
			||||||
			return FALSE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		self->frame_time = then;
 | 
					 | 
				
			||||||
		gtk_widget_queue_draw(GTK_WIDGET(self));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_frame_clock_update(GdkFrameClock *clock, gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(user_data);
 | 
					 | 
				
			||||||
	if (!advance_animation(self, clock))
 | 
					 | 
				
			||||||
		stop_animating(self);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
start_animating(FastivView *self)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	stop_animating(self);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GdkFrameClock *clock = gtk_widget_get_frame_clock(GTK_WIDGET(self));
 | 
					 | 
				
			||||||
	if (!clock || !self->image ||
 | 
					 | 
				
			||||||
		!cairo_surface_get_user_data(self->page, &fastiv_io_key_frame_next))
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	self->frame_time = gdk_frame_clock_get_frame_time(clock);
 | 
					 | 
				
			||||||
	self->frame_update_connection = g_signal_connect(
 | 
					 | 
				
			||||||
		clock, "update", G_CALLBACK(on_frame_clock_update), self);
 | 
					 | 
				
			||||||
	self->remaining_loops = (uintptr_t) cairo_surface_get_user_data(
 | 
					 | 
				
			||||||
		self->page, &fastiv_io_key_loops);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gdk_frame_clock_begin_updating(clock);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
switch_page(FastivView *self, cairo_surface_t *page)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GtkWidget *widget = GTK_WIDGET(self);
 | 
					 | 
				
			||||||
	self->frame = self->page = page;
 | 
					 | 
				
			||||||
	if ((self->orientation = (uintptr_t) cairo_surface_get_user_data(
 | 
					 | 
				
			||||||
			 self->page, &fastiv_io_key_orientation)) ==
 | 
					 | 
				
			||||||
		FastivIoOrientationUnknown)
 | 
					 | 
				
			||||||
		self->orientation = FastivIoOrientation0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	start_animating(self);
 | 
					 | 
				
			||||||
	gtk_widget_queue_resize(widget);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_map(GtkWidget *widget)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GTK_WIDGET_CLASS(fastiv_view_parent_class)->map(widget);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Loading before mapping will fail to obtain a GdkFrameClock.
 | 
					 | 
				
			||||||
	start_animating(FASTIV_VIEW(widget));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
void
 | 
					 | 
				
			||||||
fastiv_view_unmap(GtkWidget *widget)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	stop_animating(FASTIV_VIEW(widget));
 | 
					 | 
				
			||||||
	GTK_WIDGET_CLASS(fastiv_view_parent_class)->unmap(widget);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
fastiv_view_key_press_event(GtkWidget *widget, GdkEventKey *event)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivView *self = FASTIV_VIEW(widget);
 | 
					 | 
				
			||||||
	if (!self->image)
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// It should not matter that GDK_KEY_plus involves holding Shift.
 | 
					 | 
				
			||||||
	guint state = event->state & gtk_accelerator_get_default_mod_mask() &
 | 
					 | 
				
			||||||
		~GDK_SHIFT_MASK;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// The standard, intuitive bindings.
 | 
					 | 
				
			||||||
	if (state == GDK_CONTROL_MASK) {
 | 
					 | 
				
			||||||
		switch (event->keyval) {
 | 
					 | 
				
			||||||
		case GDK_KEY_0:
 | 
					 | 
				
			||||||
			return set_scale(self, 1.0);
 | 
					 | 
				
			||||||
		case GDK_KEY_plus:
 | 
					 | 
				
			||||||
			return set_scale(self, self->scale * SCALE_STEP);
 | 
					 | 
				
			||||||
		case GDK_KEY_minus:
 | 
					 | 
				
			||||||
			return set_scale(self, self->scale / SCALE_STEP);
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (state != 0)
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch (event->keyval) {
 | 
					 | 
				
			||||||
	case GDK_KEY_1:
 | 
					 | 
				
			||||||
	case GDK_KEY_2:
 | 
					 | 
				
			||||||
	case GDK_KEY_3:
 | 
					 | 
				
			||||||
	case GDK_KEY_4:
 | 
					 | 
				
			||||||
	case GDK_KEY_5:
 | 
					 | 
				
			||||||
	case GDK_KEY_6:
 | 
					 | 
				
			||||||
	case GDK_KEY_7:
 | 
					 | 
				
			||||||
	case GDK_KEY_8:
 | 
					 | 
				
			||||||
	case GDK_KEY_9:
 | 
					 | 
				
			||||||
		return set_scale(self, event->keyval - GDK_KEY_0);
 | 
					 | 
				
			||||||
	case GDK_KEY_plus:
 | 
					 | 
				
			||||||
		return set_scale(self, self->scale * SCALE_STEP);
 | 
					 | 
				
			||||||
	case GDK_KEY_minus:
 | 
					 | 
				
			||||||
		return set_scale(self, self->scale / SCALE_STEP);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	case GDK_KEY_x:  // Inspired by gThumb.
 | 
					 | 
				
			||||||
		return set_scale_to_fit(self, !self->scale_to_fit);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	case GDK_KEY_i:
 | 
					 | 
				
			||||||
		self->filter = !self->filter;
 | 
					 | 
				
			||||||
		gtk_widget_queue_draw(widget);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	case GDK_KEY_less:
 | 
					 | 
				
			||||||
		self->orientation = view_left[self->orientation];
 | 
					 | 
				
			||||||
		gtk_widget_queue_resize(widget);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	case GDK_KEY_equal:
 | 
					 | 
				
			||||||
		self->orientation = view_mirror[self->orientation];
 | 
					 | 
				
			||||||
		gtk_widget_queue_draw(widget);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	case GDK_KEY_greater:
 | 
					 | 
				
			||||||
		self->orientation = view_right[self->orientation];
 | 
					 | 
				
			||||||
		gtk_widget_queue_resize(widget);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	case GDK_KEY_bracketleft: {
 | 
					 | 
				
			||||||
		cairo_surface_t *page = cairo_surface_get_user_data(
 | 
					 | 
				
			||||||
			self->page, &fastiv_io_key_page_previous);
 | 
					 | 
				
			||||||
		if (page)
 | 
					 | 
				
			||||||
			switch_page(self, page);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	case GDK_KEY_bracketright: {
 | 
					 | 
				
			||||||
		cairo_surface_t *page = cairo_surface_get_user_data(
 | 
					 | 
				
			||||||
			self->page, &fastiv_io_key_page_next);
 | 
					 | 
				
			||||||
		if (page)
 | 
					 | 
				
			||||||
			switch_page(self, page);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	case GDK_KEY_braceleft:
 | 
					 | 
				
			||||||
		stop_animating(self);
 | 
					 | 
				
			||||||
		if (!(self->frame = cairo_surface_get_user_data(
 | 
					 | 
				
			||||||
				self->frame, &fastiv_io_key_frame_previous)))
 | 
					 | 
				
			||||||
			self->frame = self->page;
 | 
					 | 
				
			||||||
		gtk_widget_queue_draw(widget);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	case GDK_KEY_braceright:
 | 
					 | 
				
			||||||
		stop_animating(self);
 | 
					 | 
				
			||||||
		if (!(self->frame = cairo_surface_get_user_data(
 | 
					 | 
				
			||||||
				self->frame, &fastiv_io_key_frame_next)))
 | 
					 | 
				
			||||||
			self->frame = self->page;
 | 
					 | 
				
			||||||
		gtk_widget_queue_draw(widget);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return FALSE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_class_init(FastivViewClass *klass)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
					 | 
				
			||||||
	object_class->finalize = fastiv_view_finalize;
 | 
					 | 
				
			||||||
	object_class->get_property = fastiv_view_get_property;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	view_properties[PROP_SCALE] = g_param_spec_double(
 | 
					 | 
				
			||||||
		"scale", "Scale", "Zoom level",
 | 
					 | 
				
			||||||
		0, G_MAXDOUBLE, 1.0, G_PARAM_READABLE);
 | 
					 | 
				
			||||||
	view_properties[PROP_SCALE_TO_FIT] = g_param_spec_boolean(
 | 
					 | 
				
			||||||
		"scale-to-fit", "Scale to fit", "Scale images down to fit the window",
 | 
					 | 
				
			||||||
		TRUE, G_PARAM_READABLE);
 | 
					 | 
				
			||||||
	g_object_class_install_properties(
 | 
					 | 
				
			||||||
		object_class, N_PROPERTIES, view_properties);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
 | 
					 | 
				
			||||||
	widget_class->get_preferred_height = fastiv_view_get_preferred_height;
 | 
					 | 
				
			||||||
	widget_class->get_preferred_width = fastiv_view_get_preferred_width;
 | 
					 | 
				
			||||||
	widget_class->size_allocate = fastiv_view_size_allocate;
 | 
					 | 
				
			||||||
	widget_class->map = fastiv_view_map;
 | 
					 | 
				
			||||||
	widget_class->unmap = fastiv_view_unmap;
 | 
					 | 
				
			||||||
	widget_class->realize = fastiv_view_realize;
 | 
					 | 
				
			||||||
	widget_class->draw = fastiv_view_draw;
 | 
					 | 
				
			||||||
	widget_class->button_press_event = fastiv_view_button_press_event;
 | 
					 | 
				
			||||||
	widget_class->scroll_event = fastiv_view_scroll_event;
 | 
					 | 
				
			||||||
	widget_class->key_press_event = fastiv_view_key_press_event;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO(p): Later override "screen_changed", recreate Pango layouts there,
 | 
					 | 
				
			||||||
	// if we get to have any, or otherwise reflect DPI changes.
 | 
					 | 
				
			||||||
	gtk_widget_class_set_css_name(widget_class, "fastiv-view");
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
fastiv_view_init(FastivView *self)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	gtk_widget_set_can_focus(GTK_WIDGET(self), TRUE);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	self->filter = true;
 | 
					 | 
				
			||||||
	self->scale = 1.0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Picture loading ---------------------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// TODO(p): Progressive picture loading, or at least async/cancellable.
 | 
					 | 
				
			||||||
gboolean
 | 
					 | 
				
			||||||
fastiv_view_open(FastivView *self, const gchar *path, GError **error)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	cairo_surface_t *surface = fastiv_io_open(path, error);
 | 
					 | 
				
			||||||
	if (!surface)
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
	if (self->image)
 | 
					 | 
				
			||||||
		cairo_surface_destroy(self->image);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	self->frame = self->page = NULL;
 | 
					 | 
				
			||||||
	self->image = surface;
 | 
					 | 
				
			||||||
	switch_page(self, self->image);
 | 
					 | 
				
			||||||
	set_scale_to_fit(self, true);
 | 
					 | 
				
			||||||
	return TRUE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										743
									
								
								fastiv.c
									
									
									
									
									
								
							
							
						
						@@ -1,743 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// fastiv.c: fast image viewer
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
					 | 
				
			||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
					 | 
				
			||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
					 | 
				
			||||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
					 | 
				
			||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
					 | 
				
			||||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
					 | 
				
			||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <glib.h>
 | 
					 | 
				
			||||||
#include <glib/gstdio.h>
 | 
					 | 
				
			||||||
#include <gtk/gtk.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <stdarg.h>
 | 
					 | 
				
			||||||
#include <stdio.h>
 | 
					 | 
				
			||||||
#include <stdlib.h>
 | 
					 | 
				
			||||||
#include <errno.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <fnmatch.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include "config.h"
 | 
					 | 
				
			||||||
#include "fastiv-browser.h"
 | 
					 | 
				
			||||||
#include "fastiv-io.h"
 | 
					 | 
				
			||||||
#include "fastiv-sidebar.h"
 | 
					 | 
				
			||||||
#include "fastiv-view.h"
 | 
					 | 
				
			||||||
#include "xdg.h"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Utilities ---------------------------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void exit_fatal(const gchar *format, ...) G_GNUC_PRINTF(1, 2);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
exit_fatal(const gchar *format, ...)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	va_list ap;
 | 
					 | 
				
			||||||
	va_start(ap, format);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gchar *format_nl = g_strdup_printf("%s\n", format);
 | 
					 | 
				
			||||||
	vfprintf(stderr, format_nl, ap);
 | 
					 | 
				
			||||||
	free(format_nl);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	va_end(ap);
 | 
					 | 
				
			||||||
	exit(EXIT_FAILURE);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Main --------------------------------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct {
 | 
					 | 
				
			||||||
	gchar **supported_globs;
 | 
					 | 
				
			||||||
	gboolean filtering;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gchar *directory;
 | 
					 | 
				
			||||||
	GPtrArray *files;
 | 
					 | 
				
			||||||
	gint files_index;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gchar *basename;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *window;
 | 
					 | 
				
			||||||
	GtkWidget *stack;
 | 
					 | 
				
			||||||
	GtkWidget *view;
 | 
					 | 
				
			||||||
	GtkWidget *view_scroller;
 | 
					 | 
				
			||||||
	GtkWidget *browser;
 | 
					 | 
				
			||||||
	GtkWidget *browser_scroller;
 | 
					 | 
				
			||||||
	GtkWidget *browser_paned;
 | 
					 | 
				
			||||||
	GtkWidget *browser_sidebar;
 | 
					 | 
				
			||||||
	GtkWidget *plus;
 | 
					 | 
				
			||||||
	GtkWidget *minus;
 | 
					 | 
				
			||||||
} g;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
is_supported(const gchar *filename)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
 | 
					 | 
				
			||||||
	if (!utf8)
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gchar *lowercased = g_utf8_strdown(utf8, -1);
 | 
					 | 
				
			||||||
	g_free(utf8);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// XXX: fnmatch() uses the /locale/ encoding, but who cares nowadays.
 | 
					 | 
				
			||||||
	for (gchar **p = g.supported_globs; *p; p++)
 | 
					 | 
				
			||||||
		if (!fnmatch(*p, lowercased, 0)) {
 | 
					 | 
				
			||||||
			g_free(lowercased);
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g_free(lowercased);
 | 
					 | 
				
			||||||
	return FALSE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
show_error_dialog(GError *error)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GtkWidget *dialog =
 | 
					 | 
				
			||||||
		gtk_message_dialog_new(GTK_WINDOW(g.window), GTK_DIALOG_MODAL,
 | 
					 | 
				
			||||||
			GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
 | 
					 | 
				
			||||||
	gtk_dialog_run(GTK_DIALOG(dialog));
 | 
					 | 
				
			||||||
	gtk_widget_destroy(dialog);
 | 
					 | 
				
			||||||
	g_error_free(error);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
switch_to_browser(void)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	gtk_window_set_title(GTK_WINDOW(g.window), g.directory);
 | 
					 | 
				
			||||||
	gtk_stack_set_visible_child(GTK_STACK(g.stack), g.browser_paned);
 | 
					 | 
				
			||||||
	gtk_widget_grab_focus(g.browser_scroller);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
switch_to_view(const char *path)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	gtk_window_set_title(GTK_WINDOW(g.window), path);
 | 
					 | 
				
			||||||
	gtk_stack_set_visible_child(GTK_STACK(g.stack), g.view_scroller);
 | 
					 | 
				
			||||||
	gtk_widget_grab_focus(g.view);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
load_directory(const gchar *dirname)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	if (dirname) {
 | 
					 | 
				
			||||||
		free(g.directory);
 | 
					 | 
				
			||||||
		g.directory = g_strdup(dirname);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		GtkAdjustment *vadjustment = gtk_scrolled_window_get_vadjustment(
 | 
					 | 
				
			||||||
			GTK_SCROLLED_WINDOW(g.browser_scroller));
 | 
					 | 
				
			||||||
		gtk_adjustment_set_value(
 | 
					 | 
				
			||||||
			vadjustment, gtk_adjustment_get_lower(vadjustment));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g_ptr_array_set_size(g.files, 0);
 | 
					 | 
				
			||||||
	g.files_index = -1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GFile *file = g_file_new_for_path(g.directory);
 | 
					 | 
				
			||||||
	fastiv_sidebar_set_location(FASTIV_SIDEBAR(g.browser_sidebar), file);
 | 
					 | 
				
			||||||
	g_object_unref(file);
 | 
					 | 
				
			||||||
	fastiv_browser_load(FASTIV_BROWSER(g.browser),
 | 
					 | 
				
			||||||
		g.filtering ? is_supported : NULL, g.directory);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GError *error = NULL;
 | 
					 | 
				
			||||||
	GDir *dir = g_dir_open(g.directory, 0, &error);
 | 
					 | 
				
			||||||
	if (dir) {
 | 
					 | 
				
			||||||
		for (const gchar *name = NULL; (name = g_dir_read_name(dir)); ) {
 | 
					 | 
				
			||||||
			// This really wants to make you use readdir() directly.
 | 
					 | 
				
			||||||
			char *absolute = g_canonicalize_filename(name, g.directory);
 | 
					 | 
				
			||||||
			gboolean is_dir = g_file_test(absolute, G_FILE_TEST_IS_DIR);
 | 
					 | 
				
			||||||
			g_free(absolute);
 | 
					 | 
				
			||||||
			if (is_dir || !is_supported(name))
 | 
					 | 
				
			||||||
				continue;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			// XXX: We presume that this basename is from the same directory.
 | 
					 | 
				
			||||||
			if (!g_strcmp0(g.basename, name))
 | 
					 | 
				
			||||||
				g.files_index = g.files->len;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
			g_ptr_array_add(g.files, g_strdup(name));
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		g_dir_close(dir);
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		show_error_dialog(error);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	g_ptr_array_add(g.files, NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// XXX: When something outside the filtered entries is open, the index is
 | 
					 | 
				
			||||||
	// kept at -1, and browsing doesn't work. How to behave here?
 | 
					 | 
				
			||||||
	// Should we add it to the pointer array as an exception?
 | 
					 | 
				
			||||||
	if (dirname)
 | 
					 | 
				
			||||||
		switch_to_browser();
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_filtering_toggled(GtkToggleButton *button, G_GNUC_UNUSED gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	g.filtering = gtk_toggle_button_get_active(button);
 | 
					 | 
				
			||||||
	if (g.directory)
 | 
					 | 
				
			||||||
		load_directory(NULL);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
open(const gchar *path)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	g_return_if_fail(g_path_is_absolute(path));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GError *error = NULL;
 | 
					 | 
				
			||||||
	if (!fastiv_view_open(FASTIV_VIEW(g.view), path, &error)) {
 | 
					 | 
				
			||||||
		char *base = g_filename_display_basename(path);
 | 
					 | 
				
			||||||
		g_prefix_error(&error, "%s: ", base);
 | 
					 | 
				
			||||||
		show_error_dialog(error);
 | 
					 | 
				
			||||||
		g_free(base);
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gchar *uri = g_filename_to_uri(path, NULL, NULL);
 | 
					 | 
				
			||||||
	if (uri) {
 | 
					 | 
				
			||||||
		gtk_recent_manager_add_item(gtk_recent_manager_get_default(), uri);
 | 
					 | 
				
			||||||
		g_free(uri);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gchar *basename = g_path_get_basename(path);
 | 
					 | 
				
			||||||
	g_free(g.basename);
 | 
					 | 
				
			||||||
	g.basename = basename;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// So that load_directory() itself can be used for reloading.
 | 
					 | 
				
			||||||
	gchar *dirname = g_path_get_dirname(path);
 | 
					 | 
				
			||||||
	if (!g.files->len /* hack to always load the directory after launch */ ||
 | 
					 | 
				
			||||||
		!g.directory || strcmp(dirname, g.directory)) {
 | 
					 | 
				
			||||||
		load_directory(dirname);
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		g.files_index = -1;
 | 
					 | 
				
			||||||
		for (guint i = 0; i + 1 < g.files->len; i++) {
 | 
					 | 
				
			||||||
			if (!g_strcmp0(g.basename, g_ptr_array_index(g.files, i)))
 | 
					 | 
				
			||||||
				g.files_index = i;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	g_free(dirname);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch_to_view(path);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static GtkWidget *
 | 
					 | 
				
			||||||
create_open_dialog(void)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GtkWidget *dialog = gtk_file_chooser_dialog_new("Open file",
 | 
					 | 
				
			||||||
		GTK_WINDOW(g.window), GTK_FILE_CHOOSER_ACTION_OPEN,
 | 
					 | 
				
			||||||
		"_Cancel", GTK_RESPONSE_CANCEL,
 | 
					 | 
				
			||||||
		"_Open", GTK_RESPONSE_ACCEPT, NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkFileFilter *filter = gtk_file_filter_new();
 | 
					 | 
				
			||||||
	for (const char **p = fastiv_io_supported_media_types; *p; p++)
 | 
					 | 
				
			||||||
		gtk_file_filter_add_mime_type(filter, *p);
 | 
					 | 
				
			||||||
#ifdef HAVE_GDKPIXBUF
 | 
					 | 
				
			||||||
	gtk_file_filter_add_pixbuf_formats(filter);
 | 
					 | 
				
			||||||
#endif  // HAVE_GDKPIXBUF
 | 
					 | 
				
			||||||
	gtk_file_filter_set_name(filter, "Supported images");
 | 
					 | 
				
			||||||
	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), filter);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkFileFilter *all_files = gtk_file_filter_new();
 | 
					 | 
				
			||||||
	gtk_file_filter_set_name(all_files, "All files");
 | 
					 | 
				
			||||||
	gtk_file_filter_add_pattern(all_files, "*");
 | 
					 | 
				
			||||||
	gtk_file_chooser_add_filter(GTK_FILE_CHOOSER(dialog), all_files);
 | 
					 | 
				
			||||||
	return dialog;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_open(void)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	static GtkWidget *dialog;
 | 
					 | 
				
			||||||
	if (!dialog)
 | 
					 | 
				
			||||||
		dialog = create_open_dialog();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Apparently, just keeping the dialog around doesn't mean
 | 
					 | 
				
			||||||
	// that it will remember its last location.
 | 
					 | 
				
			||||||
	gtk_file_chooser_set_current_folder(GTK_FILE_CHOOSER(dialog), g.directory);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// The default is local-only, single item. Paths are returned absolute.
 | 
					 | 
				
			||||||
	switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
 | 
					 | 
				
			||||||
		gchar *path;
 | 
					 | 
				
			||||||
	case GTK_RESPONSE_ACCEPT:
 | 
					 | 
				
			||||||
		path = gtk_file_chooser_get_filename(GTK_FILE_CHOOSER(dialog));
 | 
					 | 
				
			||||||
		open(path);
 | 
					 | 
				
			||||||
		g_free(path);
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case GTK_RESPONSE_NONE:
 | 
					 | 
				
			||||||
		dialog = NULL;
 | 
					 | 
				
			||||||
		return;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_widget_hide(dialog);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_previous(void)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	if (g.files_index >= 0) {
 | 
					 | 
				
			||||||
		int previous =
 | 
					 | 
				
			||||||
			(g.files->len - 1 + g.files_index - 1) % (g.files->len - 1);
 | 
					 | 
				
			||||||
		char *absolute = g_canonicalize_filename(
 | 
					 | 
				
			||||||
			g_ptr_array_index(g.files, previous), g.directory);
 | 
					 | 
				
			||||||
		open(absolute);
 | 
					 | 
				
			||||||
		g_free(absolute);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_next(void)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	if (g.files_index >= 0) {
 | 
					 | 
				
			||||||
		int next = (g.files_index + 1) % (g.files->len - 1);
 | 
					 | 
				
			||||||
		char *absolute = g_canonicalize_filename(
 | 
					 | 
				
			||||||
			g_ptr_array_index(g.files, next), g.directory);
 | 
					 | 
				
			||||||
		open(absolute);
 | 
					 | 
				
			||||||
		g_free(absolute);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
spawn_path(const char *path)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	char *argv[] = {PROJECT_NAME, (char *) path, NULL};
 | 
					 | 
				
			||||||
	GError *error = NULL;
 | 
					 | 
				
			||||||
	g_spawn_async(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
 | 
					 | 
				
			||||||
		NULL, &error);
 | 
					 | 
				
			||||||
	g_clear_error(&error);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_item_activated(G_GNUC_UNUSED FastivBrowser *browser, GFile *location,
 | 
					 | 
				
			||||||
	GtkPlacesOpenFlags flags, G_GNUC_UNUSED gpointer data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	gchar *path = g_file_get_path(location);
 | 
					 | 
				
			||||||
	if (path) {
 | 
					 | 
				
			||||||
		if (flags == GTK_PLACES_OPEN_NEW_WINDOW)
 | 
					 | 
				
			||||||
			spawn_path(path);
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			open(path);
 | 
					 | 
				
			||||||
		g_free(path);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
open_any_path(const char *path, gboolean force_browser)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	GStatBuf st;
 | 
					 | 
				
			||||||
	gchar *canonical = g_canonicalize_filename(path, g.directory);
 | 
					 | 
				
			||||||
	gboolean success = !g_stat(canonical, &st);
 | 
					 | 
				
			||||||
	if (!success) {
 | 
					 | 
				
			||||||
		show_error_dialog(g_error_new(G_FILE_ERROR,
 | 
					 | 
				
			||||||
			g_file_error_from_errno(errno), "%s: %s", path, g_strerror(errno)));
 | 
					 | 
				
			||||||
	} else if (S_ISDIR(st.st_mode)) {
 | 
					 | 
				
			||||||
		load_directory(canonical);
 | 
					 | 
				
			||||||
	} else if (force_browser) {
 | 
					 | 
				
			||||||
		// GNOME, e.g., invokes this as a hint to focus the particular file,
 | 
					 | 
				
			||||||
		// which we can't currently do yet.
 | 
					 | 
				
			||||||
		gchar *directory = g_path_get_dirname(canonical);
 | 
					 | 
				
			||||||
		load_directory(directory);
 | 
					 | 
				
			||||||
		g_free(directory);
 | 
					 | 
				
			||||||
	} else {
 | 
					 | 
				
			||||||
		open(canonical);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	g_free(canonical);
 | 
					 | 
				
			||||||
	return success;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,
 | 
					 | 
				
			||||||
	GtkPlacesOpenFlags flags, G_GNUC_UNUSED gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	gchar *path = g_file_get_path(location);
 | 
					 | 
				
			||||||
	if (path) {
 | 
					 | 
				
			||||||
		if (flags & GTK_PLACES_OPEN_NEW_WINDOW)
 | 
					 | 
				
			||||||
			spawn_path(path);
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			open_any_path(path, FALSE);
 | 
					 | 
				
			||||||
		g_free(path);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_toolbar_zoom(G_GNUC_UNUSED GtkButton *button, gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivIoThumbnailSize size = FASTIV_IO_THUMBNAIL_SIZE_COUNT;
 | 
					 | 
				
			||||||
	g_object_get(g.browser, "thumbnail-size", &size, NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	size += (gintptr) user_data;
 | 
					 | 
				
			||||||
	g_return_if_fail(size >= FASTIV_IO_THUMBNAIL_SIZE_MIN &&
 | 
					 | 
				
			||||||
		size <= FASTIV_IO_THUMBNAIL_SIZE_MAX);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g_object_set(g.browser, "thumbnail-size", size, NULL);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
on_notify_thumbnail_size(
 | 
					 | 
				
			||||||
	GObject *object, GParamSpec *param_spec, G_GNUC_UNUSED gpointer user_data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	FastivIoThumbnailSize size = 0;
 | 
					 | 
				
			||||||
	g_object_get(object, g_param_spec_get_name(param_spec), &size, NULL);
 | 
					 | 
				
			||||||
	gtk_widget_set_sensitive(g.plus, size < FASTIV_IO_THUMBNAIL_SIZE_MAX);
 | 
					 | 
				
			||||||
	gtk_widget_set_sensitive(g.minus, size > FASTIV_IO_THUMBNAIL_SIZE_MIN);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
toggle_fullscreen(void)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	if (gdk_window_get_state(gtk_widget_get_window(g.window)) &
 | 
					 | 
				
			||||||
		GDK_WINDOW_STATE_FULLSCREEN)
 | 
					 | 
				
			||||||
		gtk_window_unfullscreen(GTK_WINDOW(g.window));
 | 
					 | 
				
			||||||
	else
 | 
					 | 
				
			||||||
		gtk_window_fullscreen(GTK_WINDOW(g.window));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// Cursor keys, e.g., simply cannot be bound through accelerators
 | 
					 | 
				
			||||||
// (and GtkWidget::keynav-failed would arguably be an awful solution).
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// GtkBindingSets can be added directly through GtkStyleContext,
 | 
					 | 
				
			||||||
// but that would still require setting up action signals on the widget class,
 | 
					 | 
				
			||||||
// which is extremely cumbersome.  GtkWidget::move-focus has no return value,
 | 
					 | 
				
			||||||
// so we can't override that and abort further handling.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Therefore, bind directly to keypresses.  Order can be fine-tuned with
 | 
					 | 
				
			||||||
// g_signal_connect{,after}(), or overriding the handler and either tactically
 | 
					 | 
				
			||||||
// chaining up or using gtk_window_propagate_key_event().
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
on_key_press(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
 | 
					 | 
				
			||||||
	G_GNUC_UNUSED gpointer data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	switch (event->state & gtk_accelerator_get_default_mod_mask()) {
 | 
					 | 
				
			||||||
	case GDK_CONTROL_MASK:
 | 
					 | 
				
			||||||
		switch (event->keyval) {
 | 
					 | 
				
			||||||
		case GDK_KEY_o:
 | 
					 | 
				
			||||||
			on_open();
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		case GDK_KEY_l:
 | 
					 | 
				
			||||||
			fastiv_sidebar_show_enter_location(
 | 
					 | 
				
			||||||
				FASTIV_SIDEBAR(g.browser_sidebar));
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		case GDK_KEY_n:
 | 
					 | 
				
			||||||
			spawn_path(g.directory);
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		case GDK_KEY_q:
 | 
					 | 
				
			||||||
		case GDK_KEY_w:
 | 
					 | 
				
			||||||
			gtk_widget_destroy(g.window);
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case 0:
 | 
					 | 
				
			||||||
		switch (event->keyval) {
 | 
					 | 
				
			||||||
		case GDK_KEY_Escape:
 | 
					 | 
				
			||||||
		case GDK_KEY_q:
 | 
					 | 
				
			||||||
			gtk_widget_destroy(g.window);
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		case GDK_KEY_o:
 | 
					 | 
				
			||||||
			on_open();
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		case GDK_KEY_F5:
 | 
					 | 
				
			||||||
		case GDK_KEY_r:
 | 
					 | 
				
			||||||
			load_directory(NULL);
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		case GDK_KEY_F9:
 | 
					 | 
				
			||||||
			gtk_widget_set_visible(g.browser_sidebar,
 | 
					 | 
				
			||||||
				!gtk_widget_is_visible(g.browser_sidebar));
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		case GDK_KEY_F11:
 | 
					 | 
				
			||||||
		case GDK_KEY_f:
 | 
					 | 
				
			||||||
			toggle_fullscreen();
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return FALSE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
on_key_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventKey *event,
 | 
					 | 
				
			||||||
	G_GNUC_UNUSED gpointer data)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	switch (event->state & gtk_accelerator_get_default_mod_mask()) {
 | 
					 | 
				
			||||||
	case 0:
 | 
					 | 
				
			||||||
		switch (event->keyval) {
 | 
					 | 
				
			||||||
		case GDK_KEY_Left:
 | 
					 | 
				
			||||||
		case GDK_KEY_Up:
 | 
					 | 
				
			||||||
		case GDK_KEY_Page_Up:
 | 
					 | 
				
			||||||
			on_previous();
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		case GDK_KEY_Right:
 | 
					 | 
				
			||||||
		case GDK_KEY_Down:
 | 
					 | 
				
			||||||
		case GDK_KEY_Page_Down:
 | 
					 | 
				
			||||||
		case GDK_KEY_space:
 | 
					 | 
				
			||||||
			on_next();
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		case GDK_KEY_Tab:
 | 
					 | 
				
			||||||
		case GDK_KEY_Return:
 | 
					 | 
				
			||||||
			switch_to_browser();
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		break;
 | 
					 | 
				
			||||||
	case GDK_MOD1_MASK:
 | 
					 | 
				
			||||||
		switch (event->keyval) {
 | 
					 | 
				
			||||||
		case GDK_KEY_Left:
 | 
					 | 
				
			||||||
			switch_to_browser();
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return FALSE;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
on_button_press_view(G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	if ((event->state & gtk_accelerator_get_default_mod_mask()))
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
	switch (event->button) {
 | 
					 | 
				
			||||||
	case 8:  // back
 | 
					 | 
				
			||||||
		switch_to_browser();
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	case GDK_BUTTON_PRIMARY:
 | 
					 | 
				
			||||||
		if (event->type == GDK_2BUTTON_PRESS) {
 | 
					 | 
				
			||||||
			toggle_fullscreen();
 | 
					 | 
				
			||||||
			return TRUE;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static gboolean
 | 
					 | 
				
			||||||
on_button_press_browser(G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	if ((event->state & gtk_accelerator_get_default_mod_mask()))
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
	switch (event->button) {
 | 
					 | 
				
			||||||
	case 9:  // forward
 | 
					 | 
				
			||||||
		// FIXME: This is inconsistent, normally there is an absolute path.
 | 
					 | 
				
			||||||
		switch_to_view(g.basename);
 | 
					 | 
				
			||||||
		return TRUE;
 | 
					 | 
				
			||||||
	default:
 | 
					 | 
				
			||||||
		return FALSE;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int
 | 
					 | 
				
			||||||
main(int argc, char *argv[])
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	gboolean show_version = FALSE, show_supported_media_types = FALSE,
 | 
					 | 
				
			||||||
		browse = FALSE;
 | 
					 | 
				
			||||||
	gchar **path_args = NULL;
 | 
					 | 
				
			||||||
	const GOptionEntry options[] = {
 | 
					 | 
				
			||||||
		{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &path_args,
 | 
					 | 
				
			||||||
			NULL, "[FILE | DIRECTORY]"},
 | 
					 | 
				
			||||||
		{"list-supported-media-types", 0, G_OPTION_FLAG_IN_MAIN,
 | 
					 | 
				
			||||||
			G_OPTION_ARG_NONE, &show_supported_media_types,
 | 
					 | 
				
			||||||
			"Output supported media types and exit", NULL},
 | 
					 | 
				
			||||||
		{"browse", 0, G_OPTION_FLAG_IN_MAIN,
 | 
					 | 
				
			||||||
			G_OPTION_ARG_NONE, &browse,
 | 
					 | 
				
			||||||
			"Start in filesystem browsing mode", NULL},
 | 
					 | 
				
			||||||
		{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
 | 
					 | 
				
			||||||
			&show_version, "Output version information and exit", NULL},
 | 
					 | 
				
			||||||
		{},
 | 
					 | 
				
			||||||
	};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GError *error = NULL;
 | 
					 | 
				
			||||||
	gboolean initialized = gtk_init_with_args(
 | 
					 | 
				
			||||||
		&argc, &argv, " - fast image viewer", options, NULL, &error);
 | 
					 | 
				
			||||||
	if (show_version) {
 | 
					 | 
				
			||||||
		printf(PROJECT_NAME " " PROJECT_VERSION "\n");
 | 
					 | 
				
			||||||
		return 0;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (show_supported_media_types) {
 | 
					 | 
				
			||||||
		for (char **types = fastiv_io_all_supported_media_types(); *types; )
 | 
					 | 
				
			||||||
			g_print("%s\n", *types++);
 | 
					 | 
				
			||||||
		return 0;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (!initialized)
 | 
					 | 
				
			||||||
		exit_fatal("%s", error->message);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// NOTE: Firefox and Eye of GNOME both interpret multiple arguments
 | 
					 | 
				
			||||||
	// in a special way. This is problematic, because one-element lists
 | 
					 | 
				
			||||||
	// are unrepresentable.
 | 
					 | 
				
			||||||
	// TODO(p): Complain to the user if there's more than one argument.
 | 
					 | 
				
			||||||
	// Best show the help message, if we can figure that out.
 | 
					 | 
				
			||||||
	const gchar *path_arg = path_args ? path_args[0] : NULL;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_window_set_default_icon_name(PROJECT_NAME);
 | 
					 | 
				
			||||||
	gtk_icon_theme_add_resource_path(
 | 
					 | 
				
			||||||
		gtk_icon_theme_get_default(), "/org/gnome/design/IconLibrary/");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// This is incredibly broken https://stackoverflow.com/a/51054396/76313
 | 
					 | 
				
			||||||
	// thus resolving the problem using overlaps.
 | 
					 | 
				
			||||||
	const char *style = "@define-color fastiv-tile #3c3c3c; \
 | 
					 | 
				
			||||||
		fastiv-view, fastiv-browser { background: #222; } \
 | 
					 | 
				
			||||||
		placessidebar.fastiv .toolbar { padding: 2px 6px; } \
 | 
					 | 
				
			||||||
		placessidebar.fastiv box > separator { margin: 4px 0; } \
 | 
					 | 
				
			||||||
		fastiv-browser { padding: 5px; } \
 | 
					 | 
				
			||||||
		fastiv-browser.item { \
 | 
					 | 
				
			||||||
			border: 1px solid rgba(255, 255, 255, 0.375); \
 | 
					 | 
				
			||||||
			margin: 10px; color: #000; \
 | 
					 | 
				
			||||||
			background: #333; \
 | 
					 | 
				
			||||||
			background-image: \
 | 
					 | 
				
			||||||
				linear-gradient(45deg, @fastiv-tile 26%, transparent 26%), \
 | 
					 | 
				
			||||||
				linear-gradient(-45deg, @fastiv-tile 26%, transparent 26%), \
 | 
					 | 
				
			||||||
				linear-gradient(45deg, transparent 74%, @fastiv-tile 74%), \
 | 
					 | 
				
			||||||
				linear-gradient(-45deg, transparent 74%, @fastiv-tile 74%); \
 | 
					 | 
				
			||||||
			background-size: 40px 40px; \
 | 
					 | 
				
			||||||
			background-position: 0 0, 0 20px, 20px -20px, -20px 0px; \
 | 
					 | 
				
			||||||
		} \
 | 
					 | 
				
			||||||
		fastiv-browser.item.symbolic { \
 | 
					 | 
				
			||||||
			border-color: transparent; color: #222; \
 | 
					 | 
				
			||||||
			background: #2c2c2c; background-image: none; \
 | 
					 | 
				
			||||||
		}";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkCssProvider *provider = gtk_css_provider_new();
 | 
					 | 
				
			||||||
	gtk_css_provider_load_from_data(provider, style, strlen(style), NULL);
 | 
					 | 
				
			||||||
	gtk_style_context_add_provider_for_screen(gdk_screen_get_default(),
 | 
					 | 
				
			||||||
		GTK_STYLE_PROVIDER(provider), GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
 | 
					 | 
				
			||||||
	g_object_unref(provider);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g.view_scroller = gtk_scrolled_window_new(NULL, NULL);
 | 
					 | 
				
			||||||
	g.view = g_object_new(FASTIV_TYPE_VIEW, NULL);
 | 
					 | 
				
			||||||
	gtk_widget_set_vexpand(g.view, TRUE);
 | 
					 | 
				
			||||||
	gtk_widget_set_hexpand(g.view, TRUE);
 | 
					 | 
				
			||||||
	g_signal_connect(g.view, "key-press-event",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_key_press_view), NULL);
 | 
					 | 
				
			||||||
	g_signal_connect(g.view, "button-press-event",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_button_press_view), NULL);
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(g.view_scroller), g.view);
 | 
					 | 
				
			||||||
	gtk_widget_show_all(g.view_scroller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Maybe our custom widgets should derive colours from the theme instead.
 | 
					 | 
				
			||||||
	gtk_scrolled_window_set_overlay_scrolling(
 | 
					 | 
				
			||||||
		GTK_SCROLLED_WINDOW(g.view_scroller), FALSE);
 | 
					 | 
				
			||||||
	g_object_set(gtk_settings_get_default(),
 | 
					 | 
				
			||||||
		"gtk-application-prefer-dark-theme", TRUE, NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g.browser_scroller = gtk_scrolled_window_new(NULL, NULL);
 | 
					 | 
				
			||||||
	g.browser = g_object_new(FASTIV_TYPE_BROWSER, NULL);
 | 
					 | 
				
			||||||
	gtk_widget_set_vexpand(g.browser, TRUE);
 | 
					 | 
				
			||||||
	gtk_widget_set_hexpand(g.browser, TRUE);
 | 
					 | 
				
			||||||
	g_signal_connect(g.browser, "item-activated",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_item_activated), NULL);
 | 
					 | 
				
			||||||
	g_signal_connect(g.browser, "button-press-event",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_button_press_browser), NULL);
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(g.browser_scroller), g.browser);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Christ, no, do not scroll all the way to the top on focus.
 | 
					 | 
				
			||||||
	GtkWidget *browser_port = gtk_bin_get_child(GTK_BIN(g.browser_scroller));
 | 
					 | 
				
			||||||
	gtk_container_set_focus_hadjustment(GTK_CONTAINER(browser_port), NULL);
 | 
					 | 
				
			||||||
	gtk_container_set_focus_vadjustment(GTK_CONTAINER(browser_port), NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO(p): As with GtkFileChooserWidget, bind:
 | 
					 | 
				
			||||||
	//  - C-h to filtering,
 | 
					 | 
				
			||||||
	//  - M-Up to going a level above,
 | 
					 | 
				
			||||||
	//  - mayhaps forward the rest to the sidebar, somehow.
 | 
					 | 
				
			||||||
	g.browser_sidebar = g_object_new(FASTIV_TYPE_SIDEBAR, NULL);
 | 
					 | 
				
			||||||
	g_signal_connect(g.browser_sidebar, "open-location",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_open_location), NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// The opposite case, and it doesn't work from the init function.
 | 
					 | 
				
			||||||
	GtkWidget *sidebar_port = gtk_bin_get_child(GTK_BIN(g.browser_sidebar));
 | 
					 | 
				
			||||||
	gtk_container_set_focus_hadjustment(GTK_CONTAINER(sidebar_port),
 | 
					 | 
				
			||||||
		gtk_scrolled_window_get_hadjustment(
 | 
					 | 
				
			||||||
			GTK_SCROLLED_WINDOW(g.browser_sidebar)));
 | 
					 | 
				
			||||||
	gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port),
 | 
					 | 
				
			||||||
		gtk_scrolled_window_get_vadjustment(
 | 
					 | 
				
			||||||
			GTK_SCROLLED_WINDOW(g.browser_sidebar)));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g.plus = gtk_button_new_from_icon_name("zoom-in-symbolic",
 | 
					 | 
				
			||||||
		GTK_ICON_SIZE_BUTTON);
 | 
					 | 
				
			||||||
	gtk_widget_set_tooltip_text(g.plus, "Larger thumbnails");
 | 
					 | 
				
			||||||
	g_signal_connect(g.plus, "clicked",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_toolbar_zoom), (gpointer) +1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g.minus = gtk_button_new_from_icon_name("zoom-out-symbolic",
 | 
					 | 
				
			||||||
		GTK_ICON_SIZE_BUTTON);
 | 
					 | 
				
			||||||
	gtk_widget_set_tooltip_text(g.minus, "Smaller thumbnails");
 | 
					 | 
				
			||||||
	g_signal_connect(g.minus, "clicked",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_toolbar_zoom), (gpointer) -1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *zoom_group = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
 | 
					 | 
				
			||||||
	gtk_style_context_add_class(
 | 
					 | 
				
			||||||
		gtk_widget_get_style_context(zoom_group), GTK_STYLE_CLASS_LINKED);
 | 
					 | 
				
			||||||
	gtk_box_pack_start(GTK_BOX(zoom_group), g.plus, FALSE, FALSE, 0);
 | 
					 | 
				
			||||||
	gtk_box_pack_start(GTK_BOX(zoom_group), g.minus, FALSE, FALSE, 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkWidget *funnel = gtk_toggle_button_new();
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(funnel),
 | 
					 | 
				
			||||||
		gtk_image_new_from_icon_name("funnel-symbolic", GTK_ICON_SIZE_BUTTON));
 | 
					 | 
				
			||||||
	gtk_widget_set_tooltip_text(funnel, "Hide unsupported files");
 | 
					 | 
				
			||||||
	g_signal_connect(funnel, "toggled",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_filtering_toggled), NULL);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	GtkBox *toolbar =
 | 
					 | 
				
			||||||
		fastiv_sidebar_get_toolbar(FASTIV_SIDEBAR(g.browser_sidebar));
 | 
					 | 
				
			||||||
	gtk_box_pack_start(toolbar, zoom_group, FALSE, FALSE, 0);
 | 
					 | 
				
			||||||
	gtk_box_pack_start(toolbar, funnel, FALSE, FALSE, 0);
 | 
					 | 
				
			||||||
	gtk_widget_set_halign(GTK_WIDGET(toolbar), GTK_ALIGN_CENTER);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g.browser_paned = gtk_paned_new(GTK_ORIENTATION_HORIZONTAL);
 | 
					 | 
				
			||||||
	gtk_paned_add1(GTK_PANED(g.browser_paned), g.browser_sidebar);
 | 
					 | 
				
			||||||
	gtk_paned_add2(GTK_PANED(g.browser_paned), g.browser_scroller);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO(p): Can we not do it here separately?
 | 
					 | 
				
			||||||
	gtk_widget_show_all(g.browser_paned);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g.stack = gtk_stack_new();
 | 
					 | 
				
			||||||
	gtk_stack_set_transition_type(
 | 
					 | 
				
			||||||
		GTK_STACK(g.stack), GTK_STACK_TRANSITION_TYPE_NONE);
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(g.stack), g.view_scroller);
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(g.stack), g.browser_paned);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 | 
					 | 
				
			||||||
	g_signal_connect(g.window, "destroy",
 | 
					 | 
				
			||||||
		G_CALLBACK(gtk_main_quit), NULL);
 | 
					 | 
				
			||||||
	g_signal_connect(g.window, "key-press-event",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_key_press), NULL);
 | 
					 | 
				
			||||||
	gtk_container_add(GTK_CONTAINER(g.window), g.stack);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	char **types = fastiv_io_all_supported_media_types();
 | 
					 | 
				
			||||||
	g.supported_globs = extract_mime_globs((const char **) types);
 | 
					 | 
				
			||||||
	g_strfreev(types);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g_signal_connect(g.browser, "notify::thumbnail-size",
 | 
					 | 
				
			||||||
		G_CALLBACK(on_notify_thumbnail_size), NULL);
 | 
					 | 
				
			||||||
	on_toolbar_zoom(NULL, (gpointer) 0);
 | 
					 | 
				
			||||||
	gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(funnel), TRUE);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	g.files = g_ptr_array_new_full(16, g_free);
 | 
					 | 
				
			||||||
	g.directory = g_get_current_dir();
 | 
					 | 
				
			||||||
	if (!path_arg || !open_any_path(path_arg, browse))
 | 
					 | 
				
			||||||
		open_any_path(g.directory, FALSE);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Try to get half of the screen vertically, in 4:3 aspect ratio.
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	// We need the GdkMonitor before the GtkWindow has a GdkWindow (i.e.,
 | 
					 | 
				
			||||||
	// before it is realized). Take the smallest dimensions, out of desperation.
 | 
					 | 
				
			||||||
	GdkDisplay *display = gtk_widget_get_display(g.window);
 | 
					 | 
				
			||||||
	int unit = G_MAXINT;
 | 
					 | 
				
			||||||
	for (int i = gdk_display_get_n_monitors(display); i--; ) {
 | 
					 | 
				
			||||||
		GdkRectangle geometry = {};
 | 
					 | 
				
			||||||
		gdk_monitor_get_geometry(
 | 
					 | 
				
			||||||
			gdk_display_get_monitor(display, i), &geometry);
 | 
					 | 
				
			||||||
		unit = MIN(unit, MIN(geometry.width, geometry.height) / 6);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Ask for at least 800x600, to cover ridiculously heterogenous setups.
 | 
					 | 
				
			||||||
	unit = MAX(200, unit);
 | 
					 | 
				
			||||||
	gtk_window_set_default_size(GTK_WINDOW(g.window), 4 * unit, 3 * unit);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	gtk_widget_show_all(g.window);
 | 
					 | 
				
			||||||
	gtk_main();
 | 
					 | 
				
			||||||
	return 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,10 +0,0 @@
 | 
				
			|||||||
[Desktop Entry]
 | 
					 | 
				
			||||||
Type=Application
 | 
					 | 
				
			||||||
Name=fastiv
 | 
					 | 
				
			||||||
GenericName=Image Viewer
 | 
					 | 
				
			||||||
Icon=fastiv
 | 
					 | 
				
			||||||
Exec=fastiv -- %f
 | 
					 | 
				
			||||||
Terminal=false
 | 
					 | 
				
			||||||
StartupNotify=true
 | 
					 | 
				
			||||||
Categories=Graphics;2DGraphics;Viewer;
 | 
					 | 
				
			||||||
MimeType=image/png;image/bmp;image/gif;image/jpeg;
 | 
					 | 
				
			||||||
							
								
								
									
										43
									
								
								fastiv.svg
									
									
									
									
									
								
							
							
						
						@@ -1,43 +0,0 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
					 | 
				
			||||||
<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
 | 
					 | 
				
			||||||
   xmlns:xlink="http://www.w3.org/1999/xlink"
 | 
					 | 
				
			||||||
   xmlns="http://www.w3.org/2000/svg"
 | 
					 | 
				
			||||||
   xmlns:svg="http://www.w3.org/2000/svg">
 | 
					 | 
				
			||||||
  <defs>
 | 
					 | 
				
			||||||
    <linearGradient id="base-background" x1="0" y1="0" x2="1" y2="1">
 | 
					 | 
				
			||||||
      <stop stop-color="#ffffff" offset="0.1" />
 | 
					 | 
				
			||||||
      <stop stop-color="#7f7f7f" offset="1" />
 | 
					 | 
				
			||||||
    </linearGradient>
 | 
					 | 
				
			||||||
    <linearGradient id="i-background">
 | 
					 | 
				
			||||||
      <stop stop-color="#e0e0e0" offset="0" />
 | 
					 | 
				
			||||||
      <stop stop-color="#ffffff" offset="1" />
 | 
					 | 
				
			||||||
    </linearGradient>
 | 
					 | 
				
			||||||
    <linearGradient id="red-background" x1="0" y1="0" x2="0" y2="1">
 | 
					 | 
				
			||||||
      <stop stop-color="#ff9b9b" offset="0" />
 | 
					 | 
				
			||||||
      <stop stop-color="#ffffff" offset="0.8" />
 | 
					 | 
				
			||||||
    </linearGradient>
 | 
					 | 
				
			||||||
    <linearGradient id="green-background" x1="0" y1="0" x2="0" y2="1">
 | 
					 | 
				
			||||||
      <stop stop-color="#98ff98" offset="0" />
 | 
					 | 
				
			||||||
      <stop stop-color="#ffffff" offset="0.8" />
 | 
					 | 
				
			||||||
    </linearGradient>
 | 
					 | 
				
			||||||
    <linearGradient id="blue-background" x1="0" y1="0" x2="0" y2="1">
 | 
					 | 
				
			||||||
      <stop stop-color="#9999ff" offset="0" />
 | 
					 | 
				
			||||||
      <stop stop-color="#ffffff" offset="0.8" />
 | 
					 | 
				
			||||||
    </linearGradient>
 | 
					 | 
				
			||||||
  </defs>
 | 
					 | 
				
			||||||
  <rect x="1" y="1" width="46" height="46" ry="5"
 | 
					 | 
				
			||||||
     style="fill: url(#base-background)" stroke="#bfbfbf" stroke-width="2" />
 | 
					 | 
				
			||||||
  <circle cx="13.5" cy="12" r="4"
 | 
					 | 
				
			||||||
     style="fill: url(#i-background)" stroke="#000000" stroke-width="2" />
 | 
					 | 
				
			||||||
  <path d="m 9.75,24 0,13 a 3.5 3.5 180 0 0 7.5,0 l 0,-13 a 3.5 3.5 180 0 0 -7.5,0 z"
 | 
					 | 
				
			||||||
     style="fill: url(#i-background)" stroke="#000000" stroke-width="2" />
 | 
					 | 
				
			||||||
  <path d="m 22,8.25 6,0 3,3 3,-3 6,0 -9,10 z"
 | 
					 | 
				
			||||||
     style="fill: url(#red-background)" stroke="#000000" stroke-width="2"
 | 
					 | 
				
			||||||
     stroke-linejoin="round" />
 | 
					 | 
				
			||||||
  <path d="m 22,19.5 6,0 3,3 3,-3 6,0 -9,10 z"
 | 
					 | 
				
			||||||
     style="fill: url(#green-background)" stroke="#000000" stroke-width="2"
 | 
					 | 
				
			||||||
     stroke-linejoin="round" />
 | 
					 | 
				
			||||||
  <path d="m 22,30.75 6,0 3,3 3,-3 6,0 -9,10 z"
 | 
					 | 
				
			||||||
     style="fill: url(#blue-background)" stroke="#000000" stroke-width="2"
 | 
					 | 
				
			||||||
     stroke-linejoin="round" />
 | 
					 | 
				
			||||||
</svg>
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 2.0 KiB  | 
@@ -1,9 +1,10 @@
 | 
				
			|||||||
[Desktop Entry]
 | 
					[Desktop Entry]
 | 
				
			||||||
Type=Application
 | 
					Type=Application
 | 
				
			||||||
Name=fastiv
 | 
					Name=fiv
 | 
				
			||||||
GenericName=File Browser
 | 
					GenericName=Image Browser
 | 
				
			||||||
Icon=fastiv
 | 
					X-GNOME-FullName=fiv Image Browser
 | 
				
			||||||
Exec=fastiv --browse -- %f
 | 
					Icon=fiv
 | 
				
			||||||
 | 
					Exec=fiv --browse -- %u
 | 
				
			||||||
NoDisplay=true
 | 
					NoDisplay=true
 | 
				
			||||||
Terminal=false
 | 
					Terminal=false
 | 
				
			||||||
StartupNotify=true
 | 
					StartupNotify=true
 | 
				
			||||||
							
								
								
									
										2041
									
								
								fiv-browser.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -1,7 +1,7 @@
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
// fastiv-view.h: fast image viewer - view widget
 | 
					// fiv-browser.h: filesystem browsing widget
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
@@ -17,10 +17,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "fiv-io-model.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <gtk/gtk.h>
 | 
					#include <gtk/gtk.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define FASTIV_TYPE_VIEW (fastiv_view_get_type())
 | 
					#define FIV_TYPE_BROWSER (fiv_browser_get_type())
 | 
				
			||||||
G_DECLARE_FINAL_TYPE(FastivView, fastiv_view, FASTIV, VIEW, GtkWidget)
 | 
					G_DECLARE_FINAL_TYPE(FivBrowser, fiv_browser, FIV, BROWSER, GtkWidget)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Try to open the given file, synchronously, to be displayed by the widget.
 | 
					GtkWidget *fiv_browser_new(FivIoModel *model);
 | 
				
			||||||
gboolean fastiv_view_open(FastivView *self, const gchar *path, GError **error);
 | 
					void fiv_browser_select(FivBrowser *self, const char *uri);
 | 
				
			||||||
							
								
								
									
										729
									
								
								fiv-collection.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,729 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-collection.c: GVfs extension for grouping arbitrary files together
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <gio/gio.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "fiv-collection.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static struct {
 | 
				
			||||||
 | 
						GFile **files;
 | 
				
			||||||
 | 
						gsize files_len;
 | 
				
			||||||
 | 
					} g;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gboolean
 | 
				
			||||||
 | 
					fiv_collection_uri_matches(const char *uri)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						static const char prefix[] = FIV_COLLECTION_SCHEME ":";
 | 
				
			||||||
 | 
						return !g_ascii_strncasecmp(uri, prefix, sizeof prefix - 1);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GFile **
 | 
				
			||||||
 | 
					fiv_collection_get_contents(gsize *len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						*len = g.files_len;
 | 
				
			||||||
 | 
						return g.files;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_collection_reload(gchar **uris)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (g.files) {
 | 
				
			||||||
 | 
							for (gsize i = 0; i < g.files_len; i++)
 | 
				
			||||||
 | 
								g_object_unref(g.files[i]);
 | 
				
			||||||
 | 
							g_free(g.files);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.files_len = g_strv_length(uris);
 | 
				
			||||||
 | 
						g.files = g_malloc0_n(g.files_len + 1, sizeof *g.files);
 | 
				
			||||||
 | 
						for (gsize i = 0; i < g.files_len; i++)
 | 
				
			||||||
 | 
							g.files[i] = g_file_new_for_uri(uris[i]);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Declarations ------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define FIV_TYPE_COLLECTION_FILE (fiv_collection_file_get_type())
 | 
				
			||||||
 | 
					G_DECLARE_FINAL_TYPE(
 | 
				
			||||||
 | 
						FivCollectionFile, fiv_collection_file, FIV, COLLECTION_FILE, GObject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct _FivCollectionFile {
 | 
				
			||||||
 | 
						GObject parent_instance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gint index;                         ///< Original index into g.files, or -1
 | 
				
			||||||
 | 
						GFile *target;                      ///< The wrapped file, or NULL for root
 | 
				
			||||||
 | 
						gchar *subpath;                     ///< Any subpath, rooted at the target
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define FIV_TYPE_COLLECTION_ENUMERATOR (fiv_collection_enumerator_get_type())
 | 
				
			||||||
 | 
					G_DECLARE_FINAL_TYPE(FivCollectionEnumerator, fiv_collection_enumerator, FIV,
 | 
				
			||||||
 | 
						COLLECTION_ENUMERATOR, GFileEnumerator)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct _FivCollectionEnumerator {
 | 
				
			||||||
 | 
						GFileEnumerator parent_instance;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *attributes;                  ///< Attributes to look up
 | 
				
			||||||
 | 
						gsize index;                        ///< Root: index into g.files
 | 
				
			||||||
 | 
						GFileEnumerator *subenumerator;     ///< Non-root: a wrapped enumerator
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Enumerator --------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					G_DEFINE_TYPE(
 | 
				
			||||||
 | 
						FivCollectionEnumerator, fiv_collection_enumerator, G_TYPE_FILE_ENUMERATOR)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_collection_enumerator_finalize(GObject *object)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(object);
 | 
				
			||||||
 | 
						g_free(self->attributes);
 | 
				
			||||||
 | 
						g_clear_object(&self->subenumerator);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFileInfo *
 | 
				
			||||||
 | 
					fiv_collection_enumerator_next_file(GFileEnumerator *enumerator,
 | 
				
			||||||
 | 
						GCancellable *cancellable, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(enumerator);
 | 
				
			||||||
 | 
						if (self->subenumerator) {
 | 
				
			||||||
 | 
							GFileInfo *info = g_file_enumerator_next_file(
 | 
				
			||||||
 | 
								self->subenumerator, cancellable, error);
 | 
				
			||||||
 | 
							if (!info)
 | 
				
			||||||
 | 
								return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO(p): Consider discarding certain classes of attributes
 | 
				
			||||||
 | 
							// from the results (adjusting "attributes" is generally unreliable).
 | 
				
			||||||
 | 
							GFile *target = g_file_enumerator_get_child(self->subenumerator, info);
 | 
				
			||||||
 | 
							gchar *target_uri = g_file_get_uri(target);
 | 
				
			||||||
 | 
							g_object_unref(target);
 | 
				
			||||||
 | 
							g_file_info_set_attribute_string(
 | 
				
			||||||
 | 
								info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, target_uri);
 | 
				
			||||||
 | 
							g_free(target_uri);
 | 
				
			||||||
 | 
							return info;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (self->index >= g.files_len)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivCollectionFile *file = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
 | 
				
			||||||
 | 
						file->index = self->index;
 | 
				
			||||||
 | 
						file->target = g_object_ref(g.files[self->index++]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFileInfo *info = g_file_query_info(G_FILE(file), self->attributes,
 | 
				
			||||||
 | 
							G_FILE_QUERY_INFO_NONE, cancellable, error);
 | 
				
			||||||
 | 
						g_object_unref(file);
 | 
				
			||||||
 | 
						return info;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					fiv_collection_enumerator_close(
 | 
				
			||||||
 | 
						GFileEnumerator *enumerator, GCancellable *cancellable, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionEnumerator *self = FIV_COLLECTION_ENUMERATOR(enumerator);
 | 
				
			||||||
 | 
						if (self->subenumerator)
 | 
				
			||||||
 | 
							return g_file_enumerator_close(self->subenumerator, cancellable, error);
 | 
				
			||||||
 | 
						return TRUE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_collection_enumerator_class_init(FivCollectionEnumeratorClass *klass)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
				
			||||||
 | 
						object_class->finalize = fiv_collection_enumerator_finalize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFileEnumeratorClass *enumerator_class = G_FILE_ENUMERATOR_CLASS(klass);
 | 
				
			||||||
 | 
						enumerator_class->next_file = fiv_collection_enumerator_next_file;
 | 
				
			||||||
 | 
						enumerator_class->close_fn = fiv_collection_enumerator_close;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_collection_enumerator_init(G_GNUC_UNUSED FivCollectionEnumerator *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Proxying GFile implementation -------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void fiv_collection_file_file_iface_init(GFileIface *iface);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					G_DEFINE_TYPE_WITH_CODE(FivCollectionFile, fiv_collection_file, G_TYPE_OBJECT,
 | 
				
			||||||
 | 
						G_IMPLEMENT_INTERFACE(G_TYPE_FILE, fiv_collection_file_file_iface_init))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_collection_file_finalize(GObject *object)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(object);
 | 
				
			||||||
 | 
						if (self->target)
 | 
				
			||||||
 | 
							g_object_unref(self->target);
 | 
				
			||||||
 | 
						g_free(self->subpath);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					fiv_collection_file_dup(GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
 | 
				
			||||||
 | 
						if (self->target)
 | 
				
			||||||
 | 
							new->target = g_object_ref(self->target);
 | 
				
			||||||
 | 
						new->subpath = g_strdup(self->subpath);
 | 
				
			||||||
 | 
						return G_FILE(new);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static guint
 | 
				
			||||||
 | 
					fiv_collection_file_hash(GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						guint hash = g_int_hash(&self->index);
 | 
				
			||||||
 | 
						if (self->target)
 | 
				
			||||||
 | 
							hash ^= g_file_hash(self->target);
 | 
				
			||||||
 | 
						if (self->subpath)
 | 
				
			||||||
 | 
							hash ^= g_str_hash(self->subpath);
 | 
				
			||||||
 | 
						return hash;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					fiv_collection_file_equal(GFile *file1, GFile *file2)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *cf1 = FIV_COLLECTION_FILE(file1);
 | 
				
			||||||
 | 
						FivCollectionFile *cf2 = FIV_COLLECTION_FILE(file2);
 | 
				
			||||||
 | 
						return cf1->index == cf2->index && cf1->target == cf2->target &&
 | 
				
			||||||
 | 
							!g_strcmp0(cf1->subpath, cf2->subpath);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					fiv_collection_file_is_native(G_GNUC_UNUSED GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return FALSE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					fiv_collection_file_has_uri_scheme(
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GFile *file, const char *uri_scheme)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return !g_ascii_strcasecmp(uri_scheme, FIV_COLLECTION_SCHEME);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					fiv_collection_file_get_uri_scheme(G_GNUC_UNUSED GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return g_strdup(FIV_COLLECTION_SCHEME);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					get_prefixed_name(FivCollectionFile *self, const char *name)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return g_strdup_printf("%d. %s", self->index + 1, name);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					get_target_basename(FivCollectionFile *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(self->target != NULL, g_strdup(""));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The "http" scheme doesn't behave nicely, make something up if needed.
 | 
				
			||||||
 | 
						// Foreign roots likewise need to be fixed up for our needs.
 | 
				
			||||||
 | 
						gchar *basename = g_file_get_basename(self->target);
 | 
				
			||||||
 | 
						if (!basename || *basename == '/') {
 | 
				
			||||||
 | 
							g_free(basename);
 | 
				
			||||||
 | 
							basename = g_file_get_uri_scheme(self->target);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *name = get_prefixed_name(self, basename);
 | 
				
			||||||
 | 
						g_free(basename);
 | 
				
			||||||
 | 
						return name;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					fiv_collection_file_get_basename(GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						if (!self->target)
 | 
				
			||||||
 | 
							return g_strdup("/");
 | 
				
			||||||
 | 
						if (self->subpath)
 | 
				
			||||||
 | 
							return g_path_get_basename(self->subpath);
 | 
				
			||||||
 | 
						return get_target_basename(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					fiv_collection_file_get_path(G_GNUC_UNUSED GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// This doesn't seem to be worth implementing (for compatible targets).
 | 
				
			||||||
 | 
						return NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					get_unescaped_uri(FivCollectionFile *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GString *unescaped = g_string_new(FIV_COLLECTION_SCHEME ":/");
 | 
				
			||||||
 | 
						if (!self->target)
 | 
				
			||||||
 | 
							return g_string_free(unescaped, FALSE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *basename = get_target_basename(self);
 | 
				
			||||||
 | 
						g_string_append(unescaped, basename);
 | 
				
			||||||
 | 
						g_free(basename);
 | 
				
			||||||
 | 
						if (self->subpath)
 | 
				
			||||||
 | 
							g_string_append(g_string_append(unescaped, "/"), self->subpath);
 | 
				
			||||||
 | 
						return g_string_free(unescaped, FALSE);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					fiv_collection_file_get_uri(GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gchar *unescaped = get_unescaped_uri(FIV_COLLECTION_FILE(file));
 | 
				
			||||||
 | 
						gchar *uri = g_uri_escape_string(
 | 
				
			||||||
 | 
							unescaped, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH, FALSE);
 | 
				
			||||||
 | 
						g_free(unescaped);
 | 
				
			||||||
 | 
						return uri;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					fiv_collection_file_get_parse_name(GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gchar *unescaped = get_unescaped_uri(FIV_COLLECTION_FILE(file));
 | 
				
			||||||
 | 
						gchar *parse_name = g_uri_escape_string(
 | 
				
			||||||
 | 
							unescaped, G_URI_RESERVED_CHARS_ALLOWED_IN_PATH " ", TRUE);
 | 
				
			||||||
 | 
						g_free(unescaped);
 | 
				
			||||||
 | 
						return parse_name;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					fiv_collection_file_get_parent(GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						if (!self->target)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
 | 
				
			||||||
 | 
						if (self->subpath) {
 | 
				
			||||||
 | 
							new->index = self->index;
 | 
				
			||||||
 | 
							new->target = g_object_ref(self->target);
 | 
				
			||||||
 | 
							if (strchr(self->subpath, '/'))
 | 
				
			||||||
 | 
								new->subpath = g_path_get_dirname(self->subpath);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return G_FILE(new);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					fiv_collection_file_prefix_matches(GFile *prefix, GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						FivCollectionFile *parent = FIV_COLLECTION_FILE(prefix);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The root has no parents.
 | 
				
			||||||
 | 
						if (!self->target)
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The root prefixes everything that is not the root.
 | 
				
			||||||
 | 
						if (!parent->target)
 | 
				
			||||||
 | 
							return TRUE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (self->index != parent->index || !self->subpath)
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
						if (!parent->subpath)
 | 
				
			||||||
 | 
							return TRUE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return g_str_has_prefix(self->subpath, parent->subpath) &&
 | 
				
			||||||
 | 
							self->subpath[strlen(parent->subpath)] == '/';
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This virtual method seems to be intended for local files only,
 | 
				
			||||||
 | 
					// and documentation claims that the result is in filesystem encoding.
 | 
				
			||||||
 | 
					// For us, paths are mostly opaque strings of arbitrary encoding, however.
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					fiv_collection_file_get_relative_path(GFile *parent, GFile *descendant)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(descendant);
 | 
				
			||||||
 | 
						FivCollectionFile *prefix = FIV_COLLECTION_FILE(parent);
 | 
				
			||||||
 | 
						if (!fiv_collection_file_prefix_matches(parent, descendant))
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_assert((!prefix->target && self->target) ||
 | 
				
			||||||
 | 
							(prefix->target && self->target && self->subpath));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (!prefix->target) {
 | 
				
			||||||
 | 
							gchar *basename = get_target_basename(self);
 | 
				
			||||||
 | 
							gchar *path = g_build_path("/", basename, self->subpath, NULL);
 | 
				
			||||||
 | 
							g_free(basename);
 | 
				
			||||||
 | 
							return path;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return prefix->subpath
 | 
				
			||||||
 | 
							? g_strdup(self->subpath + strlen(prefix->subpath) + 1)
 | 
				
			||||||
 | 
							: g_strdup(self->subpath);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					get_file_for_path(const char *path)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// Skip all initial slashes, making the result relative to the root.
 | 
				
			||||||
 | 
						if (!*(path += strspn(path, "/")))
 | 
				
			||||||
 | 
							return g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						char *end = NULL;
 | 
				
			||||||
 | 
						guint64 i = g_ascii_strtoull(path, &end, 10);
 | 
				
			||||||
 | 
						if (i <= 0 || i > g.files_len || *end != '.')
 | 
				
			||||||
 | 
							return g_file_new_for_uri("");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
 | 
				
			||||||
 | 
						new->index = --i;
 | 
				
			||||||
 | 
						new->target = g_object_ref(g.files[i]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const char *subpath = strchr(path, '/');
 | 
				
			||||||
 | 
						if (subpath && subpath[1])
 | 
				
			||||||
 | 
							new->subpath = g_strdup(++subpath);
 | 
				
			||||||
 | 
						return G_FILE(new);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					fiv_collection_file_resolve_relative_path(
 | 
				
			||||||
 | 
						GFile *file, const char *relative_path)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						if (!self->target)
 | 
				
			||||||
 | 
							return get_file_for_path(relative_path);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *basename = get_target_basename(self);
 | 
				
			||||||
 | 
						gchar *root = g_build_path("/", "/", basename, self->subpath, NULL);
 | 
				
			||||||
 | 
						g_free(basename);
 | 
				
			||||||
 | 
						gchar *canonicalized = g_canonicalize_filename(relative_path, root);
 | 
				
			||||||
 | 
						GFile *result = get_file_for_path(canonicalized);
 | 
				
			||||||
 | 
						g_free(canonicalized);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					get_target_subpathed(FivCollectionFile *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return self->subpath
 | 
				
			||||||
 | 
							? g_file_resolve_relative_path(self->target, self->subpath)
 | 
				
			||||||
 | 
							: g_object_ref(self->target);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					fiv_collection_file_get_child_for_display_name(
 | 
				
			||||||
 | 
						GFile *file, const char *display_name, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						if (!self->target)
 | 
				
			||||||
 | 
							return get_file_for_path(display_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Implementations often redirect to g_file_resolve_relative_path().
 | 
				
			||||||
 | 
						// We don't want to go up (and possibly receive a "/" basename),
 | 
				
			||||||
 | 
						// nor do we want to skip path elements.
 | 
				
			||||||
 | 
						// TODO(p): This should still be implementable, via URI inspection.
 | 
				
			||||||
 | 
						if (strchr(display_name, '/')) {
 | 
				
			||||||
 | 
							g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT,
 | 
				
			||||||
 | 
								"Display name must not contain path separators");
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *intermediate = get_target_subpathed(self);
 | 
				
			||||||
 | 
						GFile *resolved =
 | 
				
			||||||
 | 
							g_file_get_child_for_display_name(intermediate, display_name, error);
 | 
				
			||||||
 | 
						g_object_unref(intermediate);
 | 
				
			||||||
 | 
						if (!resolved)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Try to retrieve the display name converted to whatever insanity
 | 
				
			||||||
 | 
						// the target might have chosen to encode its paths with.
 | 
				
			||||||
 | 
						gchar *converted = g_file_get_basename(resolved);
 | 
				
			||||||
 | 
						g_object_unref(resolved);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivCollectionFile *new = g_object_new(FIV_TYPE_COLLECTION_FILE, NULL);
 | 
				
			||||||
 | 
						new->index = self->index;
 | 
				
			||||||
 | 
						new->target = g_object_ref(self->target);
 | 
				
			||||||
 | 
						new->subpath = self->subpath
 | 
				
			||||||
 | 
							? g_build_path("/", self->subpath, converted, NULL)
 | 
				
			||||||
 | 
							: g_strdup(converted);
 | 
				
			||||||
 | 
						g_free(converted);
 | 
				
			||||||
 | 
						return G_FILE(new);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFileEnumerator *
 | 
				
			||||||
 | 
					fiv_collection_file_enumerate_children(GFile *file, const char *attributes,
 | 
				
			||||||
 | 
						GFileQueryInfoFlags flags, GCancellable *cancellable, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						FivCollectionEnumerator *enumerator = g_object_new(
 | 
				
			||||||
 | 
							FIV_TYPE_COLLECTION_ENUMERATOR, "container", file, NULL);
 | 
				
			||||||
 | 
						enumerator->attributes = g_strdup(attributes);
 | 
				
			||||||
 | 
						if (self->target) {
 | 
				
			||||||
 | 
							GFile *intermediate = get_target_subpathed(self);
 | 
				
			||||||
 | 
							enumerator->subenumerator = g_file_enumerate_children(
 | 
				
			||||||
 | 
								intermediate, enumerator->attributes, flags, cancellable, error);
 | 
				
			||||||
 | 
							g_object_unref(intermediate);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return G_FILE_ENUMERATOR(enumerator);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(p): Implement async variants of this proxying method.
 | 
				
			||||||
 | 
					static GFileInfo *
 | 
				
			||||||
 | 
					fiv_collection_file_query_info(GFile *file, const char *attributes,
 | 
				
			||||||
 | 
						GFileQueryInfoFlags flags, GCancellable *cancellable,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						GError *e = NULL;
 | 
				
			||||||
 | 
						if (!self->target) {
 | 
				
			||||||
 | 
							GFileInfo *info = g_file_info_new();
 | 
				
			||||||
 | 
							g_file_info_set_file_type(info, G_FILE_TYPE_DIRECTORY);
 | 
				
			||||||
 | 
							g_file_info_set_name(info, "/");
 | 
				
			||||||
 | 
							g_file_info_set_display_name(info, "Collection");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							GIcon *icon = g_icon_new_for_string("shapes-symbolic", NULL);
 | 
				
			||||||
 | 
							if (icon) {
 | 
				
			||||||
 | 
								g_file_info_set_symbolic_icon(info, icon);
 | 
				
			||||||
 | 
								g_object_unref(icon);
 | 
				
			||||||
 | 
							} else {
 | 
				
			||||||
 | 
								g_warning("%s", e->message);
 | 
				
			||||||
 | 
								g_error_free(e);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return info;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The "http" scheme doesn't behave nicely, make something up if needed.
 | 
				
			||||||
 | 
						GFile *intermediate = get_target_subpathed(self);
 | 
				
			||||||
 | 
						GFileInfo *info =
 | 
				
			||||||
 | 
							g_file_query_info(intermediate, attributes, flags, cancellable, &e);
 | 
				
			||||||
 | 
						if (!info) {
 | 
				
			||||||
 | 
							g_warning("%s", e->message);
 | 
				
			||||||
 | 
							g_error_free(e);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							info = g_file_info_new();
 | 
				
			||||||
 | 
							g_file_info_set_file_type(info, G_FILE_TYPE_REGULAR);
 | 
				
			||||||
 | 
							gchar *basename = g_file_get_basename(intermediate);
 | 
				
			||||||
 | 
							g_file_info_set_name(info, basename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// The display name is "guaranteed to always be set" when queried,
 | 
				
			||||||
 | 
							// which is up to implementations.
 | 
				
			||||||
 | 
							gchar *safe = g_utf8_make_valid(basename, -1);
 | 
				
			||||||
 | 
							g_free(basename);
 | 
				
			||||||
 | 
							g_file_info_set_display_name(info, safe);
 | 
				
			||||||
 | 
							g_free(safe);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *target_uri = g_file_get_uri(intermediate);
 | 
				
			||||||
 | 
						g_file_info_set_attribute_string(
 | 
				
			||||||
 | 
							info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI, target_uri);
 | 
				
			||||||
 | 
						g_free(target_uri);
 | 
				
			||||||
 | 
						g_object_unref(intermediate);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Ensure all basenames that might have been set have the numeric prefix.
 | 
				
			||||||
 | 
						const char *name = NULL;
 | 
				
			||||||
 | 
						if (!self->subpath) {
 | 
				
			||||||
 | 
							// Always set this, because various schemes may not do so themselves,
 | 
				
			||||||
 | 
							// which then troubles GFileEnumerator.
 | 
				
			||||||
 | 
							gchar *basename = get_target_basename(self);
 | 
				
			||||||
 | 
							g_file_info_set_name(info, basename);
 | 
				
			||||||
 | 
							g_free(basename);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (g_file_info_has_attribute(
 | 
				
			||||||
 | 
									info, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME) &&
 | 
				
			||||||
 | 
								(name = g_file_info_get_display_name(info))) {
 | 
				
			||||||
 | 
								gchar *prefixed = get_prefixed_name(self, name);
 | 
				
			||||||
 | 
								g_file_info_set_display_name(info, prefixed);
 | 
				
			||||||
 | 
								g_free(prefixed);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (g_file_info_has_attribute(
 | 
				
			||||||
 | 
									info, G_FILE_ATTRIBUTE_STANDARD_EDIT_NAME) &&
 | 
				
			||||||
 | 
								(name = g_file_info_get_edit_name(info))) {
 | 
				
			||||||
 | 
								gchar *prefixed = get_prefixed_name(self, name);
 | 
				
			||||||
 | 
								g_file_info_set_edit_name(info, prefixed);
 | 
				
			||||||
 | 
								g_free(prefixed);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return info;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFileInfo *
 | 
				
			||||||
 | 
					fiv_collection_file_query_filesystem_info(G_GNUC_UNUSED GFile *file,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED const char *attributes,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GCancellable *cancellable, G_GNUC_UNUSED GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFileInfo *info = g_file_info_new();
 | 
				
			||||||
 | 
						GFileAttributeMatcher *matcher = g_file_attribute_matcher_new(attributes);
 | 
				
			||||||
 | 
						if (g_file_attribute_matcher_matches(
 | 
				
			||||||
 | 
								matcher, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE)) {
 | 
				
			||||||
 | 
							g_file_info_set_attribute_string(
 | 
				
			||||||
 | 
								info, G_FILE_ATTRIBUTE_FILESYSTEM_TYPE, FIV_COLLECTION_SCHEME);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (g_file_attribute_matcher_matches(
 | 
				
			||||||
 | 
								matcher, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY)) {
 | 
				
			||||||
 | 
							g_file_info_set_attribute_boolean(
 | 
				
			||||||
 | 
								info, G_FILE_ATTRIBUTE_FILESYSTEM_READONLY, TRUE);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_file_attribute_matcher_unref(matcher);
 | 
				
			||||||
 | 
						return info;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					fiv_collection_file_set_display_name(G_GNUC_UNUSED GFile *file,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED const char *display_name,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GCancellable *cancellable, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_set_error_literal(
 | 
				
			||||||
 | 
							error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "Operation not supported");
 | 
				
			||||||
 | 
						return NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFileInputStream *
 | 
				
			||||||
 | 
					fiv_collection_file_read(GFile *file, GCancellable *cancellable, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						if (!self->target) {
 | 
				
			||||||
 | 
							g_set_error_literal(
 | 
				
			||||||
 | 
								error, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, "Is a directory");
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *intermediate = get_target_subpathed(self);
 | 
				
			||||||
 | 
						GFileInputStream *stream = g_file_read(intermediate, cancellable, error);
 | 
				
			||||||
 | 
						g_object_unref(intermediate);
 | 
				
			||||||
 | 
						return stream;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_read(GObject *source_object, GAsyncResult *res, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFile *intermediate = G_FILE(source_object);
 | 
				
			||||||
 | 
						GTask *task = G_TASK(user_data);
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						GFileInputStream *result = g_file_read_finish(intermediate, res, &error);
 | 
				
			||||||
 | 
						if (result)
 | 
				
			||||||
 | 
							g_task_return_pointer(task, result, g_object_unref);
 | 
				
			||||||
 | 
						else
 | 
				
			||||||
 | 
							g_task_return_error(task, error);
 | 
				
			||||||
 | 
						g_object_unref(task);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_collection_file_read_async(GFile *file, int io_priority,
 | 
				
			||||||
 | 
						GCancellable *cancellable, GAsyncReadyCallback callback, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivCollectionFile *self = FIV_COLLECTION_FILE(file);
 | 
				
			||||||
 | 
						GTask *task = g_task_new(file, cancellable, callback, user_data);
 | 
				
			||||||
 | 
						g_task_set_name(task, __func__);
 | 
				
			||||||
 | 
						g_task_set_priority(task, io_priority);
 | 
				
			||||||
 | 
						if (!self->target) {
 | 
				
			||||||
 | 
							g_task_return_new_error(
 | 
				
			||||||
 | 
								task, G_IO_ERROR, G_IO_ERROR_IS_DIRECTORY, "Is a directory");
 | 
				
			||||||
 | 
							g_object_unref(task);
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *intermediate = get_target_subpathed(self);
 | 
				
			||||||
 | 
						g_file_read_async(intermediate, io_priority, cancellable, on_read, task);
 | 
				
			||||||
 | 
						g_object_unref(intermediate);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFileInputStream *
 | 
				
			||||||
 | 
					fiv_collection_file_read_finish(
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GFile *file, GAsyncResult *res, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return g_task_propagate_pointer(G_TASK(res), error);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_collection_file_file_iface_init(GFileIface *iface)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// Required methods that would segfault if unimplemented.
 | 
				
			||||||
 | 
						iface->dup = fiv_collection_file_dup;
 | 
				
			||||||
 | 
						iface->hash = fiv_collection_file_hash;
 | 
				
			||||||
 | 
						iface->equal = fiv_collection_file_equal;
 | 
				
			||||||
 | 
						iface->is_native = fiv_collection_file_is_native;
 | 
				
			||||||
 | 
						iface->has_uri_scheme = fiv_collection_file_has_uri_scheme;
 | 
				
			||||||
 | 
						iface->get_uri_scheme = fiv_collection_file_get_uri_scheme;
 | 
				
			||||||
 | 
						iface->get_basename = fiv_collection_file_get_basename;
 | 
				
			||||||
 | 
						iface->get_path = fiv_collection_file_get_path;
 | 
				
			||||||
 | 
						iface->get_uri = fiv_collection_file_get_uri;
 | 
				
			||||||
 | 
						iface->get_parse_name = fiv_collection_file_get_parse_name;
 | 
				
			||||||
 | 
						iface->get_parent = fiv_collection_file_get_parent;
 | 
				
			||||||
 | 
						iface->prefix_matches = fiv_collection_file_prefix_matches;
 | 
				
			||||||
 | 
						iface->get_relative_path = fiv_collection_file_get_relative_path;
 | 
				
			||||||
 | 
						iface->resolve_relative_path = fiv_collection_file_resolve_relative_path;
 | 
				
			||||||
 | 
						iface->get_child_for_display_name =
 | 
				
			||||||
 | 
							fiv_collection_file_get_child_for_display_name;
 | 
				
			||||||
 | 
						iface->set_display_name = fiv_collection_file_set_display_name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Optional methods.
 | 
				
			||||||
 | 
						iface->enumerate_children = fiv_collection_file_enumerate_children;
 | 
				
			||||||
 | 
						iface->query_info = fiv_collection_file_query_info;
 | 
				
			||||||
 | 
						iface->query_filesystem_info = fiv_collection_file_query_filesystem_info;
 | 
				
			||||||
 | 
						iface->read_fn = fiv_collection_file_read;
 | 
				
			||||||
 | 
						iface->read_async = fiv_collection_file_read_async;
 | 
				
			||||||
 | 
						iface->read_finish = fiv_collection_file_read_finish;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						iface->supports_thread_contexts = TRUE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_collection_file_class_init(FivCollectionFileClass *klass)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
				
			||||||
 | 
						object_class->finalize = fiv_collection_file_finalize;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_collection_file_init(FivCollectionFile *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						self->index = -1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					get_file_for_uri(G_GNUC_UNUSED GVfs *vfs, const char *identifier,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						static const char prefix[] = FIV_COLLECTION_SCHEME ":";
 | 
				
			||||||
 | 
						const char *path = identifier + sizeof prefix - 1;
 | 
				
			||||||
 | 
						if (!g_str_has_prefix(identifier, prefix))
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Specifying the authority is not supported.
 | 
				
			||||||
 | 
						if (g_str_has_prefix(path, "//"))
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Otherwise, it needs to look like an absolute path.
 | 
				
			||||||
 | 
						if (!g_str_has_prefix(path, "/"))
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): Figure out what to do about queries and fragments.
 | 
				
			||||||
 | 
						// GDummyFile carries them across level, which seems rather arbitrary.
 | 
				
			||||||
 | 
						const char *trailing = strpbrk(path, "?#");
 | 
				
			||||||
 | 
						gchar *unescaped = g_uri_unescape_segment(path, trailing, "/");
 | 
				
			||||||
 | 
						if (!unescaped)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *result = get_file_for_path(unescaped);
 | 
				
			||||||
 | 
						g_free(unescaped);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					parse_name(GVfs *vfs, const char *identifier, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// get_file_for_uri() already parses a superset of URIs.
 | 
				
			||||||
 | 
						return get_file_for_uri(vfs, identifier, user_data);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_collection_register(void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GVfs *vfs = g_vfs_get_default();
 | 
				
			||||||
 | 
						if (!g_vfs_register_uri_scheme(vfs, FIV_COLLECTION_SCHEME,
 | 
				
			||||||
 | 
								get_file_for_uri, NULL, NULL, parse_name, NULL, NULL))
 | 
				
			||||||
 | 
							g_warning(FIV_COLLECTION_SCHEME " scheme registration failed");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										25
									
								
								fiv-collection.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,25 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-collection.h: GVfs extension for grouping arbitrary files together
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <gio/gio.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define FIV_COLLECTION_SCHEME "collection"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gboolean fiv_collection_uri_matches(const char *uri);
 | 
				
			||||||
 | 
					GFile **fiv_collection_get_contents(gsize *len);
 | 
				
			||||||
 | 
					void fiv_collection_reload(gchar **uris);
 | 
				
			||||||
 | 
					void fiv_collection_register(void);
 | 
				
			||||||
							
								
								
									
										573
									
								
								fiv-context-menu.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,573 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-context-menu.c: popup menu
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "config.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "fiv-collection.h"
 | 
				
			||||||
 | 
					#include "fiv-context-menu.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					G_DEFINE_QUARK(fiv-context-menu-cancellable-quark, fiv_context_menu_cancellable)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GtkWidget *
 | 
				
			||||||
 | 
					info_start_group(GtkWidget *vbox, const char *group)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkWidget *label = gtk_label_new(group);
 | 
				
			||||||
 | 
						gtk_widget_set_hexpand(label, TRUE);
 | 
				
			||||||
 | 
						gtk_widget_set_halign(label, GTK_ALIGN_FILL);
 | 
				
			||||||
 | 
						PangoAttrList *attrs = pango_attr_list_new();
 | 
				
			||||||
 | 
						pango_attr_list_insert(attrs, pango_attr_weight_new(PANGO_WEIGHT_BOLD));
 | 
				
			||||||
 | 
						gtk_label_set_attributes(GTK_LABEL(label), attrs);
 | 
				
			||||||
 | 
						pango_attr_list_unref(attrs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *grid = gtk_grid_new();
 | 
				
			||||||
 | 
						GtkWidget *expander = gtk_expander_new(NULL);
 | 
				
			||||||
 | 
						gtk_expander_set_label_widget(GTK_EXPANDER(expander), label);
 | 
				
			||||||
 | 
						gtk_expander_set_expanded(GTK_EXPANDER(expander), TRUE);
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(expander), grid);
 | 
				
			||||||
 | 
						gtk_grid_set_column_spacing(GTK_GRID(grid), 10);
 | 
				
			||||||
 | 
						gtk_box_pack_start(GTK_BOX(vbox), expander, FALSE, FALSE, 0);
 | 
				
			||||||
 | 
						return grid;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GtkWidget *
 | 
				
			||||||
 | 
					info_parse(char *tsv)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkSizeGroup *sg = gtk_size_group_new(GTK_SIZE_GROUP_HORIZONTAL);
 | 
				
			||||||
 | 
						GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 10);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const char *last_group = NULL;
 | 
				
			||||||
 | 
						GtkWidget *grid = NULL;
 | 
				
			||||||
 | 
						int line = 1, row = 0;
 | 
				
			||||||
 | 
						for (char *nl; (nl = strchr(tsv, '\n')); line++, tsv = ++nl) {
 | 
				
			||||||
 | 
							*nl = 0;
 | 
				
			||||||
 | 
							if (nl > tsv && nl[-1] == '\r')
 | 
				
			||||||
 | 
								nl[-1] = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							char *group = tsv, *tag = strchr(group, '\t');
 | 
				
			||||||
 | 
							if (!tag) {
 | 
				
			||||||
 | 
								g_warning("ExifTool parse error on line %d", line);
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							*tag++ = 0;
 | 
				
			||||||
 | 
							for (char *p = group; *p; p++)
 | 
				
			||||||
 | 
								if (*p == '_')
 | 
				
			||||||
 | 
									*p = ' ';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							char *value = strchr(tag, '\t');
 | 
				
			||||||
 | 
							if (!value) {
 | 
				
			||||||
 | 
								g_warning("ExifTool parse error on line %d", line);
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							*value++ = 0;
 | 
				
			||||||
 | 
							if (!last_group || strcmp(last_group, group)) {
 | 
				
			||||||
 | 
								grid = info_start_group(vbox, (last_group = group));
 | 
				
			||||||
 | 
								row = 0;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							GtkWidget *a = gtk_label_new(tag);
 | 
				
			||||||
 | 
							gtk_size_group_add_widget(sg, a);
 | 
				
			||||||
 | 
							gtk_label_set_selectable(GTK_LABEL(a), TRUE);
 | 
				
			||||||
 | 
							gtk_label_set_xalign(GTK_LABEL(a), 0.);
 | 
				
			||||||
 | 
							gtk_grid_attach(GTK_GRID(grid), a, 0, row, 1, 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							GtkWidget *b = gtk_label_new(value);
 | 
				
			||||||
 | 
							gtk_label_set_selectable(GTK_LABEL(b), TRUE);
 | 
				
			||||||
 | 
							gtk_label_set_xalign(GTK_LABEL(b), 0.);
 | 
				
			||||||
 | 
							gtk_label_set_line_wrap(GTK_LABEL(b), TRUE);
 | 
				
			||||||
 | 
							gtk_widget_set_hexpand(b, TRUE);
 | 
				
			||||||
 | 
							gtk_grid_attach(GTK_GRID(grid), b, 1, row, 1, 1);
 | 
				
			||||||
 | 
							row++;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						g_object_unref(sg);
 | 
				
			||||||
 | 
						return vbox;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GtkWidget *
 | 
				
			||||||
 | 
					info_make_bar(const char *message)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkWidget *info = gtk_info_bar_new();
 | 
				
			||||||
 | 
						gtk_info_bar_set_message_type(GTK_INFO_BAR(info), GTK_MESSAGE_WARNING);
 | 
				
			||||||
 | 
						GtkWidget *info_area = gtk_info_bar_get_content_area(GTK_INFO_BAR(info));
 | 
				
			||||||
 | 
						// When the label is made selectable, Escape doesn't work when it has focus.
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(info_area), gtk_label_new(message));
 | 
				
			||||||
 | 
						return info;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					info_redirect_error(gpointer dialog, GError *error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// The dialog has been closed and destroyed.
 | 
				
			||||||
 | 
						if (g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED)) {
 | 
				
			||||||
 | 
							g_error_free(error);
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkContainer *content_area =
 | 
				
			||||||
 | 
							GTK_CONTAINER(gtk_dialog_get_content_area(GTK_DIALOG(dialog)));
 | 
				
			||||||
 | 
						gtk_container_foreach(content_area, (GtkCallback) gtk_widget_destroy, NULL);
 | 
				
			||||||
 | 
						gtk_container_add(content_area, info_make_bar(error->message));
 | 
				
			||||||
 | 
						if (g_error_matches(error, G_SPAWN_ERROR, G_SPAWN_ERROR_NOENT)) {
 | 
				
			||||||
 | 
							gtk_box_pack_start(GTK_BOX(content_area),
 | 
				
			||||||
 | 
								gtk_label_new("Please install ExifTool."), TRUE, FALSE, 12);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_error_free(error);
 | 
				
			||||||
 | 
						gtk_widget_show_all(GTK_WIDGET(dialog));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gchar *
 | 
				
			||||||
 | 
					bytes_to_utf8(GBytes *bytes)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gsize length = 0;
 | 
				
			||||||
 | 
						gconstpointer data = g_bytes_get_data(bytes, &length);
 | 
				
			||||||
 | 
						gchar *utf8 = data ? g_utf8_make_valid(data, length) : g_strdup("");
 | 
				
			||||||
 | 
						g_bytes_unref(bytes);
 | 
				
			||||||
 | 
						return utf8;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_info_finished(GObject *source_object, GAsyncResult *res, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						GBytes *bytes_out = NULL, *bytes_err = NULL;
 | 
				
			||||||
 | 
						if (!g_subprocess_communicate_finish(
 | 
				
			||||||
 | 
								G_SUBPROCESS(source_object), res, &bytes_out, &bytes_err, &error)) {
 | 
				
			||||||
 | 
							info_redirect_error(user_data, error);
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *out = bytes_to_utf8(bytes_out);
 | 
				
			||||||
 | 
						gchar *err = bytes_to_utf8(bytes_err);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *dialog = GTK_WIDGET(user_data);
 | 
				
			||||||
 | 
						GtkWidget *content_area = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
 | 
				
			||||||
 | 
						gtk_container_foreach(
 | 
				
			||||||
 | 
							GTK_CONTAINER(content_area), (GtkCallback) gtk_widget_destroy, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *scroller = gtk_scrolled_window_new(NULL, NULL);
 | 
				
			||||||
 | 
						gtk_box_pack_start(GTK_BOX(content_area), scroller, TRUE, TRUE, 0);
 | 
				
			||||||
 | 
						GtkWidget *vbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(scroller), vbox);
 | 
				
			||||||
 | 
						if (*err)
 | 
				
			||||||
 | 
							gtk_container_add(GTK_CONTAINER(vbox), info_make_bar(g_strstrip(err)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *info = info_parse(out);
 | 
				
			||||||
 | 
						gtk_style_context_add_class(
 | 
				
			||||||
 | 
							gtk_widget_get_style_context(info), "fiv-information");
 | 
				
			||||||
 | 
						gtk_box_pack_start(GTK_BOX(vbox), info, TRUE, TRUE, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_free(out);
 | 
				
			||||||
 | 
						g_free(err);
 | 
				
			||||||
 | 
						gtk_widget_show_all(dialog);
 | 
				
			||||||
 | 
						gtk_widget_grab_focus(scroller);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					info_spawn(GtkWidget *dialog, const char *path, GBytes *bytes_in)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						int flags = G_SUBPROCESS_FLAGS_STDOUT_PIPE | G_SUBPROCESS_FLAGS_STDERR_PIPE;
 | 
				
			||||||
 | 
						if (bytes_in)
 | 
				
			||||||
 | 
							flags |= G_SUBPROCESS_FLAGS_STDIN_PIPE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GSubprocessLauncher *launcher = g_subprocess_launcher_new(flags);
 | 
				
			||||||
 | 
					#ifdef G_OS_WIN32
 | 
				
			||||||
 | 
						// Both to find wperl, and then to let wperl find the nearby exiftool.
 | 
				
			||||||
 | 
						gchar *prefix = g_win32_get_package_installation_directory_of_module(NULL);
 | 
				
			||||||
 | 
						g_subprocess_launcher_set_cwd(launcher, prefix);
 | 
				
			||||||
 | 
						g_free(prefix);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): Add a fallback to internal capabilities.
 | 
				
			||||||
 | 
						// The simplest is to specify the filename and the resolution.
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						GSubprocess *subprocess = g_subprocess_launcher_spawn(launcher, &error,
 | 
				
			||||||
 | 
					#ifdef G_OS_WIN32
 | 
				
			||||||
 | 
							"wperl",
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
							"exiftool", "-tab", "-groupNames", "-duplicates", "-extractEmbedded",
 | 
				
			||||||
 | 
							"--binary", "-quiet", "--", path, NULL);
 | 
				
			||||||
 | 
						g_object_unref(launcher);
 | 
				
			||||||
 | 
						if (error) {
 | 
				
			||||||
 | 
							info_redirect_error(dialog, error);
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GCancellable *cancellable = g_object_get_qdata(
 | 
				
			||||||
 | 
							G_OBJECT(dialog), fiv_context_menu_cancellable_quark());
 | 
				
			||||||
 | 
						g_subprocess_communicate_async(
 | 
				
			||||||
 | 
							subprocess, bytes_in, cancellable, on_info_finished, dialog);
 | 
				
			||||||
 | 
						g_object_unref(subprocess);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_info_loaded(GObject *source_object, GAsyncResult *res, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gchar *file_data = NULL;
 | 
				
			||||||
 | 
						gsize file_len = 0;
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						if (!g_file_load_contents_finish(
 | 
				
			||||||
 | 
								G_FILE(source_object), res, &file_data, &file_len, NULL, &error)) {
 | 
				
			||||||
 | 
							info_redirect_error(user_data, error);
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *dialog = GTK_WIDGET(user_data);
 | 
				
			||||||
 | 
						GBytes *bytes_in = g_bytes_new_take(file_data, file_len);
 | 
				
			||||||
 | 
						info_spawn(dialog, "-", bytes_in);
 | 
				
			||||||
 | 
						g_bytes_unref(bytes_in);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_info_queried(GObject *source_object, GAsyncResult *res, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFile *file = G_FILE(source_object);
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						GFileInfo *info = g_file_query_info_finish(file, res, &error);
 | 
				
			||||||
 | 
						gboolean cancelled =
 | 
				
			||||||
 | 
							error && g_error_matches(error, G_IO_ERROR, G_IO_ERROR_CANCELLED);
 | 
				
			||||||
 | 
						g_clear_error(&error);
 | 
				
			||||||
 | 
						if (cancelled)
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *path = NULL;
 | 
				
			||||||
 | 
						const char *target_uri = g_file_info_get_attribute_string(
 | 
				
			||||||
 | 
							info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
 | 
				
			||||||
 | 
						if (target_uri) {
 | 
				
			||||||
 | 
							GFile *target = g_file_new_for_uri(target_uri);
 | 
				
			||||||
 | 
							path = g_file_get_path(target);
 | 
				
			||||||
 | 
							g_object_unref(target);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						g_object_unref(info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *dialog = GTK_WIDGET(user_data);
 | 
				
			||||||
 | 
						GCancellable *cancellable = g_object_get_qdata(
 | 
				
			||||||
 | 
							G_OBJECT(dialog), fiv_context_menu_cancellable_quark());
 | 
				
			||||||
 | 
						if (path) {
 | 
				
			||||||
 | 
							info_spawn(dialog, path, NULL);
 | 
				
			||||||
 | 
							g_free(path);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							g_file_load_contents_async(file, cancellable, on_info_loaded, dialog);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_context_menu_information(GtkWindow *parent, const char *uri)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkWidget *dialog = gtk_widget_new(GTK_TYPE_DIALOG,
 | 
				
			||||||
 | 
							"use-header-bar", TRUE,
 | 
				
			||||||
 | 
							"title", "Information",
 | 
				
			||||||
 | 
							"transient-for", parent,
 | 
				
			||||||
 | 
							"destroy-with-parent", TRUE, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// When the window closes, we cancel all asynchronous calls.
 | 
				
			||||||
 | 
						GCancellable *cancellable = g_cancellable_new();
 | 
				
			||||||
 | 
						g_object_set_qdata_full(G_OBJECT(dialog),
 | 
				
			||||||
 | 
							fiv_context_menu_cancellable_quark(), cancellable, g_object_unref);
 | 
				
			||||||
 | 
						g_signal_connect_swapped(
 | 
				
			||||||
 | 
							dialog, "destroy", G_CALLBACK(g_cancellable_cancel), cancellable);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *spinner = gtk_spinner_new();
 | 
				
			||||||
 | 
						gtk_spinner_start(GTK_SPINNER(spinner));
 | 
				
			||||||
 | 
						gtk_box_pack_start(GTK_BOX(gtk_dialog_get_content_area(GTK_DIALOG(dialog))),
 | 
				
			||||||
 | 
							spinner, TRUE, TRUE, 12);
 | 
				
			||||||
 | 
						gtk_window_set_default_size(GTK_WINDOW(dialog), 600, 800);
 | 
				
			||||||
 | 
						gtk_widget_show_all(dialog);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Mostly to identify URIs with no local path--we pipe these into ExifTool.
 | 
				
			||||||
 | 
						GFile *file = g_file_new_for_uri(uri);
 | 
				
			||||||
 | 
						gchar *parse_name = g_file_get_parse_name(file);
 | 
				
			||||||
 | 
						gtk_header_bar_set_subtitle(
 | 
				
			||||||
 | 
							GTK_HEADER_BAR(gtk_dialog_get_header_bar(GTK_DIALOG(dialog))),
 | 
				
			||||||
 | 
							parse_name);
 | 
				
			||||||
 | 
						g_free(parse_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *path = g_file_get_path(file);
 | 
				
			||||||
 | 
						if (path) {
 | 
				
			||||||
 | 
							info_spawn(dialog, path, NULL);
 | 
				
			||||||
 | 
							g_free(path);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							// Several GVfs schemes contain pseudo-symlinks
 | 
				
			||||||
 | 
							// that don't give out filesystem paths directly.
 | 
				
			||||||
 | 
							g_file_query_info_async(file, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI,
 | 
				
			||||||
 | 
								G_FILE_QUERY_INFO_NONE, G_PRIORITY_DEFAULT, cancellable,
 | 
				
			||||||
 | 
								on_info_queried, dialog);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						g_object_unref(file);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct _OpenContext {
 | 
				
			||||||
 | 
						GWeakRef window;                    ///< Parent window for any dialogs
 | 
				
			||||||
 | 
						GFile *file;                        ///< The file in question
 | 
				
			||||||
 | 
						gchar *content_type;
 | 
				
			||||||
 | 
						GAppInfo *app_info;
 | 
				
			||||||
 | 
					} OpenContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					open_context_finalize(gpointer data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						OpenContext *self = data;
 | 
				
			||||||
 | 
						g_weak_ref_clear(&self->window);
 | 
				
			||||||
 | 
						g_clear_object(&self->app_info);
 | 
				
			||||||
 | 
						g_clear_object(&self->file);
 | 
				
			||||||
 | 
						g_free(self->content_type);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					open_context_unref(gpointer data, G_GNUC_UNUSED GClosure *closure)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_rc_box_release_full(data, open_context_finalize);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					show_error_dialog(GtkWindow *parent, GError *error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkWidget *dialog =
 | 
				
			||||||
 | 
							gtk_message_dialog_new(GTK_WINDOW(parent), GTK_DIALOG_MODAL,
 | 
				
			||||||
 | 
								GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
 | 
				
			||||||
 | 
						gtk_dialog_run(GTK_DIALOG(dialog));
 | 
				
			||||||
 | 
						gtk_widget_destroy(dialog);
 | 
				
			||||||
 | 
						g_error_free(error);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					open_context_launch(GtkWidget *widget, OpenContext *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GdkAppLaunchContext *context =
 | 
				
			||||||
 | 
							gdk_display_get_app_launch_context(gtk_widget_get_display(widget));
 | 
				
			||||||
 | 
						gdk_app_launch_context_set_screen(context, gtk_widget_get_screen(widget));
 | 
				
			||||||
 | 
						gdk_app_launch_context_set_timestamp(context, gtk_get_current_event_time());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GList *files = g_list_append(NULL, self->file);
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						if (g_app_info_launch(
 | 
				
			||||||
 | 
								self->app_info, files, G_APP_LAUNCH_CONTEXT(context), &error)) {
 | 
				
			||||||
 | 
							(void) g_app_info_set_as_last_used_for_type(
 | 
				
			||||||
 | 
								self->app_info, self->content_type, NULL);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							GtkWindow *window = g_weak_ref_get(&self->window);
 | 
				
			||||||
 | 
							show_error_dialog(window, error);
 | 
				
			||||||
 | 
							g_clear_object(&window);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						g_list_free(files);
 | 
				
			||||||
 | 
						g_object_unref(context);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					append_opener(GtkWidget *menu, GAppInfo *opener, const OpenContext *template)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx);
 | 
				
			||||||
 | 
						g_weak_ref_init(&ctx->window, NULL);
 | 
				
			||||||
 | 
						ctx->file = g_object_ref(template->file);
 | 
				
			||||||
 | 
						ctx->content_type = g_strdup(template->content_type);
 | 
				
			||||||
 | 
						ctx->app_info = opener;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// On Linux, this prefers the obsoleted X-GNOME-FullName.
 | 
				
			||||||
 | 
						gchar *name =
 | 
				
			||||||
 | 
							g_strdup_printf("Open With %s", g_app_info_get_display_name(opener));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// It's documented that we can touch the child, if we want to use markup.
 | 
				
			||||||
 | 
					#if 0
 | 
				
			||||||
 | 
						GtkWidget *item = gtk_menu_item_new_with_label(name);
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
						// GtkImageMenuItem overrides the toggle_size_request class method
 | 
				
			||||||
 | 
						// to get the image shown in the "margin"--too much work to duplicate.
 | 
				
			||||||
 | 
						G_GNUC_BEGIN_IGNORE_DEPRECATIONS;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *item = gtk_image_menu_item_new_with_label(name);
 | 
				
			||||||
 | 
						GIcon *icon = g_app_info_get_icon(opener);
 | 
				
			||||||
 | 
						if (icon) {
 | 
				
			||||||
 | 
							GtkWidget *image = gtk_image_new_from_gicon(icon, GTK_ICON_SIZE_MENU);
 | 
				
			||||||
 | 
							gtk_image_menu_item_set_image(GTK_IMAGE_MENU_ITEM(item), image);
 | 
				
			||||||
 | 
							gtk_image_menu_item_set_always_show_image(
 | 
				
			||||||
 | 
								GTK_IMAGE_MENU_ITEM(item), TRUE);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						G_GNUC_END_IGNORE_DEPRECATIONS;
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_free(name);
 | 
				
			||||||
 | 
						g_signal_connect_data(item, "activate", G_CALLBACK(open_context_launch),
 | 
				
			||||||
 | 
							ctx, open_context_unref, 0);
 | 
				
			||||||
 | 
						gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_chooser_activate(GtkMenuItem *item, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						OpenContext *ctx = user_data;
 | 
				
			||||||
 | 
						GtkWindow *window = g_weak_ref_get(&ctx->window);
 | 
				
			||||||
 | 
						GtkWidget *dialog = gtk_app_chooser_dialog_new_for_content_type(window,
 | 
				
			||||||
 | 
							GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL, ctx->content_type);
 | 
				
			||||||
 | 
						g_clear_object(&window);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#if 0
 | 
				
			||||||
 | 
						// This exists as a concept in mimeapps.list, but GNOME infuriatingly
 | 
				
			||||||
 | 
						// infers it from the last used application if missing.
 | 
				
			||||||
 | 
						gtk_app_chooser_widget_set_show_default(
 | 
				
			||||||
 | 
							GTK_APP_CHOOSER_WIDGET(gtk_app_chooser_dialog_get_widget(
 | 
				
			||||||
 | 
								GTK_APP_CHOOSER_DIALOG(dialog))), TRUE);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_OK) {
 | 
				
			||||||
 | 
							ctx->app_info = gtk_app_chooser_get_app_info(GTK_APP_CHOOSER(dialog));
 | 
				
			||||||
 | 
							open_context_launch(GTK_WIDGET(item), ctx);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						gtk_widget_destroy(dialog);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_info_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						OpenContext *ctx = user_data;
 | 
				
			||||||
 | 
						GtkWindow *window = g_weak_ref_get(&ctx->window);
 | 
				
			||||||
 | 
						gchar *uri = g_file_get_uri(ctx->file);
 | 
				
			||||||
 | 
						fiv_context_menu_information(window, uri);
 | 
				
			||||||
 | 
						g_clear_object(&window);
 | 
				
			||||||
 | 
						g_free(uri);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_context_menu_remove(GtkWindow *parent, GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// TODO(p): Use g_file_trash_async(), for which we need a task manager.
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						if (!g_file_trash(file, NULL, &error))
 | 
				
			||||||
 | 
							show_error_dialog(parent, error);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_trash_activate(G_GNUC_UNUSED GtkMenuItem *item, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						OpenContext *ctx = user_data;
 | 
				
			||||||
 | 
						GtkWindow *window = g_weak_ref_get(&ctx->window);
 | 
				
			||||||
 | 
						fiv_context_menu_remove(window, ctx->file);
 | 
				
			||||||
 | 
						g_clear_object(&window);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					destroy_widget_idle_source_func(GtkWidget *widget)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// The whole menu is deactivated /before/ any item is activated,
 | 
				
			||||||
 | 
						// and a destroyed child item will not activate.
 | 
				
			||||||
 | 
						gtk_widget_destroy(widget);
 | 
				
			||||||
 | 
						return FALSE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GtkMenu *
 | 
				
			||||||
 | 
					fiv_context_menu_new(GtkWidget *widget, GFile *file)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFileInfo *info = g_file_query_info(file,
 | 
				
			||||||
 | 
							G_FILE_ATTRIBUTE_STANDARD_TYPE
 | 
				
			||||||
 | 
							"," G_FILE_ATTRIBUTE_STANDARD_NAME
 | 
				
			||||||
 | 
							"," G_FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE
 | 
				
			||||||
 | 
							"," G_FILE_ATTRIBUTE_STANDARD_TARGET_URI,
 | 
				
			||||||
 | 
							G_FILE_QUERY_INFO_NONE, NULL, NULL);
 | 
				
			||||||
 | 
						if (!info)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWindow *window = NULL;
 | 
				
			||||||
 | 
						if (widget && GTK_IS_WINDOW((widget = gtk_widget_get_toplevel(widget))))
 | 
				
			||||||
 | 
							window = GTK_WINDOW(widget);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// This will have no application pre-assigned, for use with GTK+'s dialog.
 | 
				
			||||||
 | 
						OpenContext *ctx = g_rc_box_alloc0(sizeof *ctx);
 | 
				
			||||||
 | 
						g_weak_ref_init(&ctx->window, window);
 | 
				
			||||||
 | 
						if (!(ctx->content_type = g_strdup(g_file_info_get_content_type(info))))
 | 
				
			||||||
 | 
							ctx->content_type = g_content_type_guess(NULL, NULL, 0, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFileType type = g_file_info_get_file_type(info);
 | 
				
			||||||
 | 
						const char *target_uri = g_file_info_get_attribute_string(
 | 
				
			||||||
 | 
							info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
 | 
				
			||||||
 | 
						ctx->file = target_uri && g_file_has_uri_scheme(file, FIV_COLLECTION_SCHEME)
 | 
				
			||||||
 | 
							? g_file_new_for_uri(target_uri)
 | 
				
			||||||
 | 
							: g_object_ref(file);
 | 
				
			||||||
 | 
						g_object_unref(info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GAppInfo *default_ =
 | 
				
			||||||
 | 
							g_app_info_get_default_for_type(ctx->content_type, FALSE);
 | 
				
			||||||
 | 
						GList *recommended = g_app_info_get_recommended_for_type(ctx->content_type);
 | 
				
			||||||
 | 
						GList *fallback = g_app_info_get_fallback_for_type(ctx->content_type);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *menu = gtk_menu_new();
 | 
				
			||||||
 | 
						if (default_) {
 | 
				
			||||||
 | 
							append_opener(menu, default_, ctx);
 | 
				
			||||||
 | 
							gtk_menu_shell_append(
 | 
				
			||||||
 | 
								GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (GList *iter = recommended; iter; iter = iter->next) {
 | 
				
			||||||
 | 
							if (!default_ || !g_app_info_equal(iter->data, default_))
 | 
				
			||||||
 | 
								append_opener(menu, iter->data, ctx);
 | 
				
			||||||
 | 
							else
 | 
				
			||||||
 | 
								g_object_unref(iter->data);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (recommended) {
 | 
				
			||||||
 | 
							g_list_free(recommended);
 | 
				
			||||||
 | 
							gtk_menu_shell_append(
 | 
				
			||||||
 | 
								GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (GList *iter = fallback; iter; iter = iter->next) {
 | 
				
			||||||
 | 
							if (!default_ || !g_app_info_equal(iter->data, default_))
 | 
				
			||||||
 | 
								append_opener(menu, iter->data, ctx);
 | 
				
			||||||
 | 
							else
 | 
				
			||||||
 | 
								g_object_unref(iter->data);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (fallback) {
 | 
				
			||||||
 | 
							g_list_free(fallback);
 | 
				
			||||||
 | 
							gtk_menu_shell_append(
 | 
				
			||||||
 | 
								GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *item = gtk_menu_item_new_with_label("Open With...");
 | 
				
			||||||
 | 
						g_signal_connect_data(item, "activate", G_CALLBACK(on_chooser_activate),
 | 
				
			||||||
 | 
							ctx, open_context_unref, 0);
 | 
				
			||||||
 | 
						gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): Can we avoid using the "trash" string constant for this check?
 | 
				
			||||||
 | 
						if (!g_file_has_uri_scheme(file, "trash")) {
 | 
				
			||||||
 | 
							gtk_menu_shell_append(
 | 
				
			||||||
 | 
								GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							item = gtk_menu_item_new_with_mnemonic("Move to _Trash");
 | 
				
			||||||
 | 
							g_signal_connect_data(item, "activate", G_CALLBACK(on_trash_activate),
 | 
				
			||||||
 | 
								g_rc_box_acquire(ctx), open_context_unref, 0);
 | 
				
			||||||
 | 
							gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (type == G_FILE_TYPE_REGULAR) {
 | 
				
			||||||
 | 
							gtk_menu_shell_append(
 | 
				
			||||||
 | 
								GTK_MENU_SHELL(menu), gtk_separator_menu_item_new());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							item = gtk_menu_item_new_with_mnemonic("_Information");
 | 
				
			||||||
 | 
							g_signal_connect_data(item, "activate", G_CALLBACK(on_info_activate),
 | 
				
			||||||
 | 
								g_rc_box_acquire(ctx), open_context_unref, 0);
 | 
				
			||||||
 | 
							gtk_menu_shell_append(GTK_MENU_SHELL(menu), item);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// As per GTK+ 3 Common Questions, 1.5.
 | 
				
			||||||
 | 
						g_object_ref_sink(menu);
 | 
				
			||||||
 | 
						g_signal_connect_swapped(menu, "deactivate",
 | 
				
			||||||
 | 
							G_CALLBACK(g_idle_add), destroy_widget_idle_source_func);
 | 
				
			||||||
 | 
						g_signal_connect(menu, "destroy", G_CALLBACK(g_object_unref), NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_widget_show_all(menu);
 | 
				
			||||||
 | 
						return GTK_MENU(menu);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
// fastiv-sidebar.h: molesting GtkPlacesSidebar
 | 
					// fiv-context-menu.h: popup menu
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					// Copyright (c) 2022 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
@@ -15,14 +15,8 @@
 | 
				
			|||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#pragma once
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <gtk/gtk.h>
 | 
					#include <gtk/gtk.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define FASTIV_TYPE_SIDEBAR (fastiv_sidebar_get_type())
 | 
					void fiv_context_menu_information(GtkWindow *parent, const char *uri);
 | 
				
			||||||
G_DECLARE_FINAL_TYPE(
 | 
					void fiv_context_menu_remove(GtkWindow *parent, GFile *file);
 | 
				
			||||||
	FastivSidebar, fastiv_sidebar, FASTIV, SIDEBAR, GtkScrolledWindow)
 | 
					GtkMenu *fiv_context_menu_new(GtkWidget *widget, GFile *file);
 | 
				
			||||||
 | 
					 | 
				
			||||||
void fastiv_sidebar_set_location(FastivSidebar *self, GFile *location);
 | 
					 | 
				
			||||||
void fastiv_sidebar_show_enter_location(FastivSidebar *self);
 | 
					 | 
				
			||||||
GtkBox *fastiv_sidebar_get_toolbar(FastivSidebar *self);
 | 
					 | 
				
			||||||
							
								
								
									
										463
									
								
								fiv-io-cmm.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,463 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-io-cmm.c: colour management
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "config.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <glib.h>
 | 
				
			||||||
 | 
					#include <stdbool.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "fiv-io.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Colour management must be handled before RGB conversions.
 | 
				
			||||||
 | 
					// TODO(p): Make it also possible to use Skia's skcms.
 | 
				
			||||||
 | 
					#ifdef HAVE_LCMS2
 | 
				
			||||||
 | 
					#include <lcms2.h>
 | 
				
			||||||
 | 
					#endif  // HAVE_LCMS2
 | 
				
			||||||
 | 
					#ifdef HAVE_LCMS2_FAST_FLOAT
 | 
				
			||||||
 | 
					#include <lcms2_fast_float.h>
 | 
				
			||||||
 | 
					#endif  // HAVE_LCMS2_FAST_FLOAT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- CMM-independent transforms ----------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// CAIRO_STRIDE_ALIGNMENT is 4 bytes, so there will be no padding with
 | 
				
			||||||
 | 
					// ARGB/BGRA/XRGB/BGRX.
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					trivial_cmyk_to_host_byte_order_argb(unsigned char *p, int len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// This CMYK handling has been seen in gdk-pixbuf/JPEG, GIMP/JPEG, skcms.
 | 
				
			||||||
 | 
						// It will typically produce horribly oversaturated results.
 | 
				
			||||||
 | 
						// Assume that all YCCK/CMYK JPEG files use inverted CMYK, as Photoshop
 | 
				
			||||||
 | 
						// does, see https://bugzilla.gnome.org/show_bug.cgi?id=618096
 | 
				
			||||||
 | 
						while (len--) {
 | 
				
			||||||
 | 
							int c = p[0], m = p[1], y = p[2], k = p[3];
 | 
				
			||||||
 | 
					#if G_BYTE_ORDER == G_LITTLE_ENDIAN
 | 
				
			||||||
 | 
							p[0] = k * y / 255;
 | 
				
			||||||
 | 
							p[1] = k * m / 255;
 | 
				
			||||||
 | 
							p[2] = k * c / 255;
 | 
				
			||||||
 | 
							p[3] = 255;
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
							p[3] = k * y / 255;
 | 
				
			||||||
 | 
							p[2] = k * m / 255;
 | 
				
			||||||
 | 
							p[1] = k * c / 255;
 | 
				
			||||||
 | 
							p[0] = 255;
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
							p += 4;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// From libwebp, verified to exactly match [x * a / 255].
 | 
				
			||||||
 | 
					#define PREMULTIPLY8(a, x) (((uint32_t) (x) * (uint32_t) (a) * 32897U) >> 23)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_premultiply_argb32(FivIoImage *image)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (image->format != CAIRO_FORMAT_ARGB32)
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (uint32_t y = 0; y < image->height; y++) {
 | 
				
			||||||
 | 
							uint32_t *dstp = (uint32_t *) (image->data + image->stride * y);
 | 
				
			||||||
 | 
							for (uint32_t x = 0; x < image->width; x++) {
 | 
				
			||||||
 | 
								uint32_t argb = dstp[x], a = argb >> 24;
 | 
				
			||||||
 | 
								dstp[x] = a << 24 |
 | 
				
			||||||
 | 
									PREMULTIPLY8(a, 0xFF & (argb >> 16)) << 16 |
 | 
				
			||||||
 | 
									PREMULTIPLY8(a, 0xFF & (argb >>  8)) <<  8 |
 | 
				
			||||||
 | 
									PREMULTIPLY8(a, 0xFF &  argb);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Profiles ----------------------------------------------------------------
 | 
				
			||||||
 | 
					#ifdef HAVE_LCMS2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct _FivIoProfile {
 | 
				
			||||||
 | 
						FivIoCmm *cmm;
 | 
				
			||||||
 | 
						cmsHPROFILE profile;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GBytes *
 | 
				
			||||||
 | 
					fiv_io_profile_to_bytes(FivIoProfile *profile)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						cmsUInt32Number len = 0;
 | 
				
			||||||
 | 
						(void) cmsSaveProfileToMem(profile, NULL, &len);
 | 
				
			||||||
 | 
						gchar *data = g_malloc0(len);
 | 
				
			||||||
 | 
						if (!cmsSaveProfileToMem(profile, data, &len)) {
 | 
				
			||||||
 | 
							g_free(data);
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return g_bytes_new_take(data, len);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_profile_new(FivIoCmm *cmm, cmsHPROFILE profile)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivIoProfile *self = g_new0(FivIoProfile, 1);
 | 
				
			||||||
 | 
						self->cmm = g_object_ref(cmm);
 | 
				
			||||||
 | 
						self->profile = profile;
 | 
				
			||||||
 | 
						return self;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_profile_free(FivIoProfile *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						cmsCloseProfile(self->profile);
 | 
				
			||||||
 | 
						g_clear_object(&self->cmm);
 | 
				
			||||||
 | 
						g_free(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#else  // ! HAVE_LCMS2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GBytes *fiv_io_profile_to_bytes(FivIoProfile *) { return NULL; }
 | 
				
			||||||
 | 
					void fiv_io_profile_free(FivIoProfile *) {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif  // ! HAVE_LCMS2
 | 
				
			||||||
 | 
					// --- Contexts ----------------------------------------------------------------
 | 
				
			||||||
 | 
					#ifdef HAVE_LCMS2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct _FivIoCmm {
 | 
				
			||||||
 | 
						GObject parent_instance;
 | 
				
			||||||
 | 
						cmsContext context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// https://github.com/mm2/Little-CMS/issues/430
 | 
				
			||||||
 | 
						gboolean broken_premul;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					G_DEFINE_TYPE(FivIoCmm, fiv_io_cmm, G_TYPE_OBJECT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_cmm_finalize(GObject *gobject)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivIoCmm *self = FIV_IO_CMM(gobject);
 | 
				
			||||||
 | 
						cmsDeleteContext(self->context);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						G_OBJECT_CLASS(fiv_io_cmm_parent_class)->finalize(gobject);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_cmm_class_init(FivIoCmmClass *klass)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
				
			||||||
 | 
						object_class->finalize = fiv_io_cmm_finalize;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_cmm_init(FivIoCmm *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						self->context = cmsCreateContext(NULL, self);
 | 
				
			||||||
 | 
					#ifdef HAVE_LCMS2_FAST_FLOAT
 | 
				
			||||||
 | 
						if (cmsPluginTHR(self->context, cmsFastFloatExtensions()))
 | 
				
			||||||
 | 
							self->broken_premul = LCMS_VERSION <= 2160;
 | 
				
			||||||
 | 
					#endif  // HAVE_LCMS2_FAST_FLOAT
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoCmm *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_default(void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						static gsize initialization_value = 0;
 | 
				
			||||||
 | 
						static FivIoCmm *default_ = NULL;
 | 
				
			||||||
 | 
						if (g_once_init_enter(&initialization_value)) {
 | 
				
			||||||
 | 
							gsize setup_value = 1;
 | 
				
			||||||
 | 
							default_ = g_object_new(FIV_TYPE_IO_CMM, NULL);
 | 
				
			||||||
 | 
							g_once_init_leave(&initialization_value, setup_value);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return default_;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_profile(FivIoCmm *self, const void *data, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(self != NULL, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fiv_io_profile_new(self,
 | 
				
			||||||
 | 
							cmsOpenProfileFromMemTHR(self->context, data, len));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_profile_sRGB(FivIoCmm *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(self != NULL, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return fiv_io_profile_new(self,
 | 
				
			||||||
 | 
							cmsCreate_sRGBProfileTHR(self->context));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_profile_parametric(FivIoCmm *self,
 | 
				
			||||||
 | 
						double gamma, double whitepoint[2], double primaries[6])
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(self != NULL, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const cmsCIExyY cmsWP = {whitepoint[0], whitepoint[1], 1.0};
 | 
				
			||||||
 | 
						const cmsCIExyYTRIPLE cmsP = {
 | 
				
			||||||
 | 
							{primaries[0], primaries[1], 1.0},
 | 
				
			||||||
 | 
							{primaries[2], primaries[3], 1.0},
 | 
				
			||||||
 | 
							{primaries[4], primaries[5], 1.0},
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmsToneCurve *curve = cmsBuildGamma(self->context, gamma);
 | 
				
			||||||
 | 
						if (!curve)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmsHPROFILE profile = cmsCreateRGBProfileTHR(self->context,
 | 
				
			||||||
 | 
							&cmsWP, &cmsP, (cmsToneCurve *[3]){curve, curve, curve});
 | 
				
			||||||
 | 
						cmsFreeToneCurve(curve);
 | 
				
			||||||
 | 
						return fiv_io_profile_new(self, profile);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#else  // ! HAVE_LCMS2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoCmm *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_default()
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_profile(FivIoCmm *, const void *, size_t)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_profile_sRGB(FivIoCmm *)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_profile_parametric(FivIoCmm *, double, double[2], double[6])
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif  // ! HAVE_LCMS2
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return fiv_io_cmm_get_profile_parametric(self, gamma,
 | 
				
			||||||
 | 
							(double[2]){0.3127, 0.3290},
 | 
				
			||||||
 | 
							(double[6]){0.6400, 0.3300, 0.3000, 0.6000, 0.1500, 0.0600});
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *
 | 
				
			||||||
 | 
					fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gsize len = 0;
 | 
				
			||||||
 | 
						gconstpointer p = g_bytes_get_data(bytes, &len);
 | 
				
			||||||
 | 
						return fiv_io_cmm_get_profile(self, p, len);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Image loading -----------------------------------------------------------
 | 
				
			||||||
 | 
					#ifdef HAVE_LCMS2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(p): In general, try to use CAIRO_FORMAT_RGB30 or CAIRO_FORMAT_RGBA128F.
 | 
				
			||||||
 | 
					#define FIV_IO_PROFILE_ARGB32 \
 | 
				
			||||||
 | 
						(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8 : TYPE_ARGB_8)
 | 
				
			||||||
 | 
					#define FIV_IO_PROFILE_4X16LE \
 | 
				
			||||||
 | 
						(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_16 : TYPE_BGRA_16_SE)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_cmm_cmyk(FivIoCmm *self,
 | 
				
			||||||
 | 
						FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_if_fail(target == NULL || self != NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmsHTRANSFORM transform = NULL;
 | 
				
			||||||
 | 
						if (source && target) {
 | 
				
			||||||
 | 
							transform = cmsCreateTransformTHR(self->context,
 | 
				
			||||||
 | 
								source->profile, TYPE_CMYK_8_REV,
 | 
				
			||||||
 | 
								target->profile, FIV_IO_PROFILE_ARGB32, INTENT_PERCEPTUAL, 0);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (transform) {
 | 
				
			||||||
 | 
							cmsDoTransform(
 | 
				
			||||||
 | 
								transform, image->data, image->data, image->width * image->height);
 | 
				
			||||||
 | 
							cmsDeleteTransform(transform);
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						trivial_cmyk_to_host_byte_order_argb(
 | 
				
			||||||
 | 
							image->data, image->width * image->height);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					fiv_io_cmm_rgb_direct(FivIoCmm *self, unsigned char *data, int w, int h,
 | 
				
			||||||
 | 
						FivIoProfile *source, FivIoProfile *target,
 | 
				
			||||||
 | 
						uint32_t source_format, uint32_t target_format)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(target == NULL || self != NULL, false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): We should make this optional.
 | 
				
			||||||
 | 
						FivIoProfile *src_fallback = NULL;
 | 
				
			||||||
 | 
						if (target && !source)
 | 
				
			||||||
 | 
							source = src_fallback = fiv_io_cmm_get_profile_sRGB(self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cmsHTRANSFORM transform = NULL;
 | 
				
			||||||
 | 
						if (source && target) {
 | 
				
			||||||
 | 
							transform = cmsCreateTransformTHR(self->context,
 | 
				
			||||||
 | 
								source->profile, source_format,
 | 
				
			||||||
 | 
								target->profile, target_format, INTENT_PERCEPTUAL, 0);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (transform) {
 | 
				
			||||||
 | 
							cmsDoTransform(transform, data, data, w * h);
 | 
				
			||||||
 | 
							cmsDeleteTransform(transform);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (src_fallback)
 | 
				
			||||||
 | 
							fiv_io_profile_free(src_fallback);
 | 
				
			||||||
 | 
						return transform != NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_cmm_xrgb32(FivIoCmm *self,
 | 
				
			||||||
 | 
						FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
 | 
				
			||||||
 | 
							source, target, FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
 | 
				
			||||||
 | 
						int w, int h, FivIoProfile *source, FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						fiv_io_cmm_rgb_direct(self, data, w, h, source, target,
 | 
				
			||||||
 | 
							FIV_IO_PROFILE_4X16LE, FIV_IO_PROFILE_4X16LE);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#else  // ! HAVE_LCMS2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_cmm_cmyk(FivIoCmm *, FivIoImage *image, FivIoProfile *, FivIoProfile *)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						trivial_cmyk_to_host_byte_order_argb(
 | 
				
			||||||
 | 
							image->data, image->width * image->height);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_cmm_xrgb32(FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_cmm_4x16le_direct(
 | 
				
			||||||
 | 
						FivIoCmm *, unsigned char *, int, int, FivIoProfile *, FivIoProfile *)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif  // ! HAVE_LCMS2
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					#if defined HAVE_LCMS2 && LCMS_VERSION >= 2130
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define FIV_IO_PROFILE_ARGB32_PREMUL \
 | 
				
			||||||
 | 
						(G_BYTE_ORDER == G_LITTLE_ENDIAN ? TYPE_BGRA_8_PREMUL : TYPE_ARGB_8_PREMUL)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_cmm_argb32(FivIoCmm *self, FivIoImage *image,
 | 
				
			||||||
 | 
						FivIoProfile *source, FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_if_fail(image->format == CAIRO_FORMAT_ARGB32);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO: With self->broken_premul,
 | 
				
			||||||
 | 
						// this probably also needs to be wrapped in un-premultiplication.
 | 
				
			||||||
 | 
						fiv_io_cmm_rgb_direct(self, image->data, image->width, image->height,
 | 
				
			||||||
 | 
							source, target,
 | 
				
			||||||
 | 
							FIV_IO_PROFILE_ARGB32_PREMUL, FIV_IO_PROFILE_ARGB32_PREMUL);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
 | 
				
			||||||
 | 
						FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_if_fail(target == NULL || self != NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (image->format != CAIRO_FORMAT_ARGB32) {
 | 
				
			||||||
 | 
							fiv_io_cmm_xrgb32(self, image, source, target);
 | 
				
			||||||
 | 
						} else if (!target || self->broken_premul) {
 | 
				
			||||||
 | 
							fiv_io_cmm_xrgb32(self, image, source, target);
 | 
				
			||||||
 | 
							fiv_io_premultiply_argb32(image);
 | 
				
			||||||
 | 
						} else if (!fiv_io_cmm_rgb_direct(self, image->data,
 | 
				
			||||||
 | 
								image->width, image->height, source, target,
 | 
				
			||||||
 | 
								FIV_IO_PROFILE_ARGB32, FIV_IO_PROFILE_ARGB32_PREMUL)) {
 | 
				
			||||||
 | 
							g_debug("failed to create a premultiplying transform");
 | 
				
			||||||
 | 
							fiv_io_premultiply_argb32(image);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#else  // ! HAVE_LCMS2 || LCMS_VERSION < 2130
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_cmm_argb32(G_GNUC_UNUSED FivIoCmm *self, G_GNUC_UNUSED FivIoImage *image,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED FivIoProfile *source, G_GNUC_UNUSED FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// TODO(p): Unpremultiply, transform, repremultiply. Or require lcms2>=2.13.
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
 | 
				
			||||||
 | 
						FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						fiv_io_cmm_xrgb32(self, image, source, target);
 | 
				
			||||||
 | 
						fiv_io_premultiply_argb32(image);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#endif  // ! HAVE_LCMS2 || LCMS_VERSION < 2130
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
 | 
				
			||||||
 | 
						void (*frame_cb) (FivIoCmm *, FivIoImage *, FivIoProfile *, FivIoProfile *))
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivIoProfile *source = NULL;
 | 
				
			||||||
 | 
						if (page->icc)
 | 
				
			||||||
 | 
							source = fiv_io_cmm_get_profile_from_bytes(self, page->icc);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): All animations need to be composited in a linear colour space.
 | 
				
			||||||
 | 
						for (FivIoImage *frame = page; frame != NULL; frame = frame->frame_next)
 | 
				
			||||||
 | 
							frame_cb(self, frame, source, target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (source)
 | 
				
			||||||
 | 
							fiv_io_profile_free(source);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_cmm_any(FivIoCmm *self,
 | 
				
			||||||
 | 
						FivIoImage *image, FivIoProfile *source, FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// TODO(p): Ensure we do colour management early enough, so that
 | 
				
			||||||
 | 
						// no avoidable increase of quantization error occurs beforehands,
 | 
				
			||||||
 | 
						// and also for correct alpha compositing.
 | 
				
			||||||
 | 
						switch (image->format) {
 | 
				
			||||||
 | 
						break; case CAIRO_FORMAT_RGB24:
 | 
				
			||||||
 | 
							fiv_io_cmm_xrgb32(self, image, source, target);
 | 
				
			||||||
 | 
						break; case CAIRO_FORMAT_ARGB32:
 | 
				
			||||||
 | 
							fiv_io_cmm_argb32(self, image, source, target);
 | 
				
			||||||
 | 
						break; default:
 | 
				
			||||||
 | 
							g_debug("CM attempted on an unsupported surface format");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(p): Offer better integration, upgrade the bit depth if appropriate.
 | 
				
			||||||
 | 
					FivIoImage *
 | 
				
			||||||
 | 
					fiv_io_cmm_finish(FivIoCmm *self, FivIoImage *image, FivIoProfile *target)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!target)
 | 
				
			||||||
 | 
							return image;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (FivIoImage *page = image; page != NULL; page = page->page_next)
 | 
				
			||||||
 | 
							fiv_io_cmm_page(self, page, target, fiv_io_cmm_any);
 | 
				
			||||||
 | 
						return image;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										742
									
								
								fiv-io-model.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,742 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-io-model.c: filesystem
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "fiv-io.h"
 | 
				
			||||||
 | 
					#include "fiv-io-model.h"
 | 
				
			||||||
 | 
					#include "xdg.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GType
 | 
				
			||||||
 | 
					fiv_io_model_sort_get_type(void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						static gsize guard;
 | 
				
			||||||
 | 
						if (g_once_init_enter(&guard)) {
 | 
				
			||||||
 | 
					#define XX(name) {FIV_IO_MODEL_SORT_ ## name, \
 | 
				
			||||||
 | 
						"FIV_IO_MODEL_SORT_" #name, #name},
 | 
				
			||||||
 | 
							static const GEnumValue values[] = {FIV_IO_MODEL_SORTS(XX) {}};
 | 
				
			||||||
 | 
					#undef XX
 | 
				
			||||||
 | 
							GType type = g_enum_register_static(
 | 
				
			||||||
 | 
								g_intern_static_string("FivIoModelSort"), values);
 | 
				
			||||||
 | 
							g_once_init_leave(&guard, type);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return guard;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					G_DEFINE_BOXED_TYPE(FivIoModelEntry, fiv_io_model_entry,
 | 
				
			||||||
 | 
						fiv_io_model_entry_ref, fiv_io_model_entry_unref)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoModelEntry *
 | 
				
			||||||
 | 
					fiv_io_model_entry_ref(FivIoModelEntry *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return g_rc_box_acquire(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_io_model_entry_unref(FivIoModelEntry *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_rc_box_release(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static size_t
 | 
				
			||||||
 | 
					entry_strsize(const char *string)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!string)
 | 
				
			||||||
 | 
							return 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return strlen(string) + 1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static char *
 | 
				
			||||||
 | 
					entry_strappend(char **p, const char *string, size_t size)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!string)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						char *destination = memcpy(*p, string, size);
 | 
				
			||||||
 | 
						*p += size;
 | 
				
			||||||
 | 
						return destination;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// See model_load_attributes for a (superset of a) list of required attributes.
 | 
				
			||||||
 | 
					static FivIoModelEntry *
 | 
				
			||||||
 | 
					entry_new(GFile *file, GFileInfo *info)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gchar *uri = g_file_get_uri(file);
 | 
				
			||||||
 | 
						const gchar *target_uri = g_file_info_get_attribute_string(
 | 
				
			||||||
 | 
							info, G_FILE_ATTRIBUTE_STANDARD_TARGET_URI);
 | 
				
			||||||
 | 
						const gchar *display_name = g_file_info_get_display_name(info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): Make it possible to use g_utf8_collate_key() instead,
 | 
				
			||||||
 | 
						// which does not use natural sorting.
 | 
				
			||||||
 | 
						gchar *parse_name = g_file_get_parse_name(file);
 | 
				
			||||||
 | 
						gchar *collate_key = g_utf8_collate_key_for_filename(parse_name, -1);
 | 
				
			||||||
 | 
						g_free(parse_name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The entries are immutable. Packing them into the structure
 | 
				
			||||||
 | 
						// should help memory usage as well as performance.
 | 
				
			||||||
 | 
						size_t size_uri          = entry_strsize(uri);
 | 
				
			||||||
 | 
						size_t size_target_uri   = entry_strsize(target_uri);
 | 
				
			||||||
 | 
						size_t size_display_name = entry_strsize(display_name);
 | 
				
			||||||
 | 
						size_t size_collate_key  = entry_strsize(collate_key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivIoModelEntry *entry = g_rc_box_alloc0(sizeof *entry +
 | 
				
			||||||
 | 
							size_uri +
 | 
				
			||||||
 | 
							size_target_uri +
 | 
				
			||||||
 | 
							size_display_name +
 | 
				
			||||||
 | 
							size_collate_key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *p = (gchar *) entry + sizeof *entry;
 | 
				
			||||||
 | 
						entry->uri          = entry_strappend(&p, uri, size_uri);
 | 
				
			||||||
 | 
						entry->target_uri   = entry_strappend(&p, target_uri, size_target_uri);
 | 
				
			||||||
 | 
						entry->display_name = entry_strappend(&p, display_name, size_display_name);
 | 
				
			||||||
 | 
						entry->collate_key  = entry_strappend(&p, collate_key, size_collate_key);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						entry->filesize = (guint64) g_file_info_get_size(info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GDateTime *mtime = g_file_info_get_modification_date_time(info);
 | 
				
			||||||
 | 
						if (mtime) {
 | 
				
			||||||
 | 
							entry->mtime_msec = g_date_time_to_unix(mtime) * 1000 +
 | 
				
			||||||
 | 
								g_date_time_get_microsecond(mtime) / 1000;
 | 
				
			||||||
 | 
							g_date_time_unref(mtime);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_free(uri);
 | 
				
			||||||
 | 
						g_free(collate_key);
 | 
				
			||||||
 | 
						return entry;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct _FivIoModel {
 | 
				
			||||||
 | 
						GObject parent_instance;
 | 
				
			||||||
 | 
						GPatternSpec **supported_patterns;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *directory;                   ///< Currently loaded directory
 | 
				
			||||||
 | 
						GFileMonitor *monitor;              ///< "directory" monitoring
 | 
				
			||||||
 | 
						GPtrArray *subdirs;                 ///< "directory" contents
 | 
				
			||||||
 | 
						GPtrArray *files;                   ///< "directory" contents
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivIoModelSort sort_field;          ///< How to sort
 | 
				
			||||||
 | 
						gboolean sort_descending;           ///< Whether to sort in reverse
 | 
				
			||||||
 | 
						gboolean filtering;                 ///< Only show non-hidden, supported
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					G_DEFINE_TYPE(FivIoModel, fiv_io_model, G_TYPE_OBJECT)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum {
 | 
				
			||||||
 | 
						PROP_FILTERING = 1,
 | 
				
			||||||
 | 
						PROP_SORT_FIELD,
 | 
				
			||||||
 | 
						PROP_SORT_DESCENDING,
 | 
				
			||||||
 | 
						N_PROPERTIES
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GParamSpec *model_properties[N_PROPERTIES];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum {
 | 
				
			||||||
 | 
						RELOADED,
 | 
				
			||||||
 | 
						FILES_CHANGED,
 | 
				
			||||||
 | 
						SUBDIRECTORIES_CHANGED,
 | 
				
			||||||
 | 
						LAST_SIGNAL,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Globals are, sadly, the canonical way of storing signal numbers.
 | 
				
			||||||
 | 
					static guint model_signals[LAST_SIGNAL];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GPtrArray *
 | 
				
			||||||
 | 
					model_entry_array_new(void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return g_ptr_array_new_with_free_func(
 | 
				
			||||||
 | 
							(GDestroyNotify) fiv_io_model_entry_unref);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#if !GLIB_CHECK_VERSION(2, 70, 0)
 | 
				
			||||||
 | 
					#define g_pattern_spec_match g_pattern_match
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					model_supports(FivIoModel *self, const char *filename)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gchar *utf8 = g_filename_to_utf8(filename, -1, NULL, NULL, NULL);
 | 
				
			||||||
 | 
						if (!utf8)
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *lc = g_utf8_strdown(utf8, -1);
 | 
				
			||||||
 | 
						gsize lc_length = strlen(lc);
 | 
				
			||||||
 | 
						gchar *reversed = g_utf8_strreverse(lc, lc_length);
 | 
				
			||||||
 | 
						g_free(utf8);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// fnmatch() uses the /locale encoding/, and isn't present on Windows.
 | 
				
			||||||
 | 
						// TODO(p): Consider using g_file_info_get_display_name() for direct UTF-8.
 | 
				
			||||||
 | 
						gboolean result = FALSE;
 | 
				
			||||||
 | 
						for (GPatternSpec **p = self->supported_patterns; *p; p++)
 | 
				
			||||||
 | 
							if ((result = g_pattern_spec_match(*p, lc_length, lc, reversed)))
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_free(lc);
 | 
				
			||||||
 | 
						g_free(reversed);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static inline int
 | 
				
			||||||
 | 
					model_compare_entries(FivIoModel *self,
 | 
				
			||||||
 | 
						const FivIoModelEntry *entry1, GFile *file1,
 | 
				
			||||||
 | 
						const FivIoModelEntry *entry2, GFile *file2)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (g_file_has_prefix(file1, file2))
 | 
				
			||||||
 | 
							return +1;
 | 
				
			||||||
 | 
						if (g_file_has_prefix(file2, file1))
 | 
				
			||||||
 | 
							return -1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						int result = 0;
 | 
				
			||||||
 | 
						switch (self->sort_field) {
 | 
				
			||||||
 | 
						case FIV_IO_MODEL_SORT_MTIME:
 | 
				
			||||||
 | 
							result -= entry1->mtime_msec < entry2->mtime_msec;
 | 
				
			||||||
 | 
							result += entry1->mtime_msec > entry2->mtime_msec;
 | 
				
			||||||
 | 
							if (result != 0)
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Fall-through
 | 
				
			||||||
 | 
						case FIV_IO_MODEL_SORT_NAME:
 | 
				
			||||||
 | 
						case FIV_IO_MODEL_SORT_COUNT:
 | 
				
			||||||
 | 
							result = strcmp(entry1->collate_key, entry2->collate_key);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return self->sort_descending ? -result : +result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gint
 | 
				
			||||||
 | 
					model_compare(gconstpointer a, gconstpointer b, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						const FivIoModelEntry *entry1 = *(const FivIoModelEntry **) a;
 | 
				
			||||||
 | 
						const FivIoModelEntry *entry2 = *(const FivIoModelEntry **) b;
 | 
				
			||||||
 | 
						GFile *file1 = g_file_new_for_uri(entry1->uri);
 | 
				
			||||||
 | 
						GFile *file2 = g_file_new_for_uri(entry2->uri);
 | 
				
			||||||
 | 
						int result = model_compare_entries(user_data, entry1, file1, entry2, file2);
 | 
				
			||||||
 | 
						g_object_unref(file1);
 | 
				
			||||||
 | 
						g_object_unref(file2);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *model_load_attributes =
 | 
				
			||||||
 | 
						G_FILE_ATTRIBUTE_STANDARD_TYPE ","
 | 
				
			||||||
 | 
						G_FILE_ATTRIBUTE_STANDARD_NAME ","
 | 
				
			||||||
 | 
						G_FILE_ATTRIBUTE_STANDARD_SIZE ","
 | 
				
			||||||
 | 
						G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME ","
 | 
				
			||||||
 | 
						G_FILE_ATTRIBUTE_STANDARD_TARGET_URI ","
 | 
				
			||||||
 | 
						G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN ","
 | 
				
			||||||
 | 
						G_FILE_ATTRIBUTE_TIME_MODIFIED ","
 | 
				
			||||||
 | 
						G_FILE_ATTRIBUTE_TIME_MODIFIED_USEC;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GPtrArray *
 | 
				
			||||||
 | 
					model_decide_placement(
 | 
				
			||||||
 | 
						FivIoModel *self, GFileInfo *info, GPtrArray *subdirs, GPtrArray *files)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (self->filtering &&
 | 
				
			||||||
 | 
							g_file_info_has_attribute(info, G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
 | 
				
			||||||
 | 
							g_file_info_get_is_hidden(info))
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						if (g_file_info_get_file_type(info) == G_FILE_TYPE_DIRECTORY)
 | 
				
			||||||
 | 
							return subdirs;
 | 
				
			||||||
 | 
						if (!self->filtering ||
 | 
				
			||||||
 | 
							model_supports(self, g_file_info_get_name(info)))
 | 
				
			||||||
 | 
							return files;
 | 
				
			||||||
 | 
						return NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					model_reload_to(FivIoModel *self, GFile *directory,
 | 
				
			||||||
 | 
						GPtrArray *subdirs, GPtrArray *files, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (subdirs)
 | 
				
			||||||
 | 
							g_ptr_array_set_size(subdirs, 0);
 | 
				
			||||||
 | 
						if (files)
 | 
				
			||||||
 | 
							g_ptr_array_set_size(files, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFileEnumerator *enumerator = g_file_enumerate_children(
 | 
				
			||||||
 | 
							directory, model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, error);
 | 
				
			||||||
 | 
						if (!enumerator)
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFileInfo *info = NULL;
 | 
				
			||||||
 | 
						GFile *child = NULL;
 | 
				
			||||||
 | 
						GError *e = NULL;
 | 
				
			||||||
 | 
						while (TRUE) {
 | 
				
			||||||
 | 
							if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, &e) &&
 | 
				
			||||||
 | 
								e) {
 | 
				
			||||||
 | 
								g_warning("%s", e->message);
 | 
				
			||||||
 | 
								g_clear_error(&e);
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							if (!info)
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							GPtrArray *target =
 | 
				
			||||||
 | 
								model_decide_placement(self, info, subdirs, files);
 | 
				
			||||||
 | 
							if (target)
 | 
				
			||||||
 | 
								g_ptr_array_add(target, entry_new(child, info));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						g_object_unref(enumerator);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (subdirs)
 | 
				
			||||||
 | 
							g_ptr_array_sort_with_data(subdirs, model_compare, self);
 | 
				
			||||||
 | 
						if (files)
 | 
				
			||||||
 | 
							g_ptr_array_sort_with_data(files, model_compare, self);
 | 
				
			||||||
 | 
						return TRUE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					model_reload(FivIoModel *self, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// Note that this will clear all entries on failure.
 | 
				
			||||||
 | 
						gboolean result = model_reload_to(
 | 
				
			||||||
 | 
							self, self->directory, self->subdirs, self->files, error);
 | 
				
			||||||
 | 
						g_signal_emit(self, model_signals[RELOADED], 0);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					model_resort(FivIoModel *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_ptr_array_sort_with_data(self->subdirs, model_compare, self);
 | 
				
			||||||
 | 
						g_ptr_array_sort_with_data(self->files, model_compare, self);
 | 
				
			||||||
 | 
						g_signal_emit(self, model_signals[RELOADED], 0);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gint
 | 
				
			||||||
 | 
					model_find(const GPtrArray *target, GFile *file, FivIoModelEntry **entry)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						for (guint i = 0; i < target->len; i++) {
 | 
				
			||||||
 | 
							FivIoModelEntry *e = target->pdata[i];
 | 
				
			||||||
 | 
							GFile *f = g_file_new_for_uri(e->uri);
 | 
				
			||||||
 | 
							gboolean match = g_file_equal(f, file);
 | 
				
			||||||
 | 
							g_object_unref(f);
 | 
				
			||||||
 | 
							if (match) {
 | 
				
			||||||
 | 
								*entry = e;
 | 
				
			||||||
 | 
								return i;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return -1;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum monitor_event {
 | 
				
			||||||
 | 
						MONITOR_NONE,
 | 
				
			||||||
 | 
						MONITOR_CHANGING,
 | 
				
			||||||
 | 
						MONITOR_RENAMING,
 | 
				
			||||||
 | 
						MONITOR_REMOVING,
 | 
				
			||||||
 | 
						MONITOR_ADDING
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					monitor_apply(enum monitor_event event, GPtrArray *target, int index,
 | 
				
			||||||
 | 
						FivIoModelEntry *new_entry)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_if_fail(event != MONITOR_CHANGING || index >= 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (event == MONITOR_RENAMING && index < 0)
 | 
				
			||||||
 | 
							// The file used to be filtered out but isn't anymore.
 | 
				
			||||||
 | 
							event = MONITOR_ADDING;
 | 
				
			||||||
 | 
						else if (!new_entry && index >= 0)
 | 
				
			||||||
 | 
							// The file wasn't filtered out but now it is.
 | 
				
			||||||
 | 
							event = MONITOR_REMOVING;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (event == MONITOR_CHANGING) {
 | 
				
			||||||
 | 
							fiv_io_model_entry_unref(target->pdata[index]);
 | 
				
			||||||
 | 
							target->pdata[index] = fiv_io_model_entry_ref(new_entry);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (event == MONITOR_REMOVING || event == MONITOR_RENAMING)
 | 
				
			||||||
 | 
							g_ptr_array_remove_index(target, index);
 | 
				
			||||||
 | 
						if (event == MONITOR_RENAMING || event == MONITOR_ADDING)
 | 
				
			||||||
 | 
							g_ptr_array_add(target, fiv_io_model_entry_ref(new_entry));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_monitor_changed(G_GNUC_UNUSED GFileMonitor *monitor, GFile *file,
 | 
				
			||||||
 | 
						GFile *other_file, GFileMonitorEvent event_type, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivIoModel *self = user_data;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivIoModelEntry *old_entry = NULL;
 | 
				
			||||||
 | 
						gint files_index = model_find(self->files, file, &old_entry);
 | 
				
			||||||
 | 
						gint subdirs_index = model_find(self->subdirs, file, &old_entry);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						enum monitor_event event = MONITOR_NONE;
 | 
				
			||||||
 | 
						GFile *new_entry_file = NULL;
 | 
				
			||||||
 | 
						switch (event_type) {
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_CHANGED:
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_ATTRIBUTE_CHANGED:
 | 
				
			||||||
 | 
							event = MONITOR_CHANGING;
 | 
				
			||||||
 | 
							new_entry_file = file;
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_RENAMED:
 | 
				
			||||||
 | 
							event = MONITOR_RENAMING;
 | 
				
			||||||
 | 
							new_entry_file = other_file;
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_DELETED:
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_MOVED_OUT:
 | 
				
			||||||
 | 
							event = MONITOR_REMOVING;
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_CREATED:
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_MOVED_IN:
 | 
				
			||||||
 | 
							event = MONITOR_ADDING;
 | 
				
			||||||
 | 
							old_entry = NULL;
 | 
				
			||||||
 | 
							new_entry_file = file;
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_CHANGES_DONE_HINT:
 | 
				
			||||||
 | 
							// TODO(p): Figure out if we can't make use of _CHANGES_DONE_HINT.
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_PRE_UNMOUNT:
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_UNMOUNTED:
 | 
				
			||||||
 | 
							// TODO(p): Figure out how to handle _UNMOUNTED sensibly.
 | 
				
			||||||
 | 
						case G_FILE_MONITOR_EVENT_MOVED:
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivIoModelEntry *new_entry = NULL;
 | 
				
			||||||
 | 
						GPtrArray *new_target = NULL;
 | 
				
			||||||
 | 
						if (new_entry_file) {
 | 
				
			||||||
 | 
							GError *error = NULL;
 | 
				
			||||||
 | 
							GFileInfo *info = g_file_query_info(new_entry_file,
 | 
				
			||||||
 | 
								model_load_attributes, G_FILE_QUERY_INFO_NONE, NULL, &error);
 | 
				
			||||||
 | 
							if (error) {
 | 
				
			||||||
 | 
								g_debug("monitor: %s", error->message);
 | 
				
			||||||
 | 
								g_error_free(error);
 | 
				
			||||||
 | 
								goto run;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ((new_target =
 | 
				
			||||||
 | 
								model_decide_placement(self, info, self->subdirs, self->files)))
 | 
				
			||||||
 | 
								new_entry = entry_new(new_entry_file, info);
 | 
				
			||||||
 | 
							g_object_unref(info);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ((files_index != -1 && new_target == self->subdirs) ||
 | 
				
			||||||
 | 
								(subdirs_index != -1 && new_target == self->files)) {
 | 
				
			||||||
 | 
								g_debug("monitor: ignoring transfer between files and subdirs");
 | 
				
			||||||
 | 
								goto out;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					run:
 | 
				
			||||||
 | 
						// Keep a reference alive so that signal handlers see the new arrays.
 | 
				
			||||||
 | 
						if (old_entry)
 | 
				
			||||||
 | 
							fiv_io_model_entry_ref(old_entry);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (files_index != -1 || new_target == self->files) {
 | 
				
			||||||
 | 
							monitor_apply(event, self->files, files_index, new_entry);
 | 
				
			||||||
 | 
							g_signal_emit(self, model_signals[FILES_CHANGED],
 | 
				
			||||||
 | 
								0, old_entry, new_entry);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (subdirs_index != -1 || new_target == self->subdirs) {
 | 
				
			||||||
 | 
							monitor_apply(event, self->subdirs, subdirs_index, new_entry);
 | 
				
			||||||
 | 
							g_signal_emit(self, model_signals[SUBDIRECTORIES_CHANGED],
 | 
				
			||||||
 | 
								0, old_entry, new_entry);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// NOTE: It would make sense to do
 | 
				
			||||||
 | 
						//   g_ptr_array_sort_with_data(self->{files,subdirs}, model_compare, self);
 | 
				
			||||||
 | 
						// but then the iteration behaviour of fiv.c would differ from what's shown
 | 
				
			||||||
 | 
						// in the browser. Perhaps we need to use an index-based, fully-synchronized
 | 
				
			||||||
 | 
						// interface similar to GListModel::items-changed.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (old_entry)
 | 
				
			||||||
 | 
							fiv_io_model_entry_unref(old_entry);
 | 
				
			||||||
 | 
					out:
 | 
				
			||||||
 | 
						if (new_entry)
 | 
				
			||||||
 | 
							fiv_io_model_entry_unref(new_entry);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This would be more efficient iteratively, but it's not that important.
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					model_last_deep_subdirectory(FivIoModel *self, GFile *directory)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFile *result = NULL;
 | 
				
			||||||
 | 
						GPtrArray *subdirs = model_entry_array_new();
 | 
				
			||||||
 | 
						if (!model_reload_to(self, directory, subdirs, NULL, NULL))
 | 
				
			||||||
 | 
							goto out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (subdirs->len) {
 | 
				
			||||||
 | 
							FivIoModelEntry *entry = g_ptr_array_index(subdirs, subdirs->len - 1);
 | 
				
			||||||
 | 
							GFile *last = g_file_new_for_uri(entry->uri);
 | 
				
			||||||
 | 
							result = model_last_deep_subdirectory(self, last);
 | 
				
			||||||
 | 
							g_object_unref(last);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							result = g_object_ref(directory);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out:
 | 
				
			||||||
 | 
						g_ptr_array_free(subdirs, TRUE);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GFile *
 | 
				
			||||||
 | 
					fiv_io_model_get_previous_directory(FivIoModel *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *parent_directory = g_file_get_parent(self->directory);
 | 
				
			||||||
 | 
						if (!parent_directory)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *result = NULL;
 | 
				
			||||||
 | 
						GPtrArray *subdirs = model_entry_array_new();
 | 
				
			||||||
 | 
						if (!model_reload_to(self, parent_directory, subdirs, NULL, NULL))
 | 
				
			||||||
 | 
							goto out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						for (gsize i = 0; i < subdirs->len; i++) {
 | 
				
			||||||
 | 
							FivIoModelEntry *entry = g_ptr_array_index(subdirs, i);
 | 
				
			||||||
 | 
							GFile *file = g_file_new_for_uri(entry->uri);
 | 
				
			||||||
 | 
							if (g_file_equal(file, self->directory)) {
 | 
				
			||||||
 | 
								g_object_unref(file);
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							g_clear_object(&result);
 | 
				
			||||||
 | 
							result = file;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (result) {
 | 
				
			||||||
 | 
							GFile *last = model_last_deep_subdirectory(self, result);
 | 
				
			||||||
 | 
							g_object_unref(result);
 | 
				
			||||||
 | 
							result = last;
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							result = g_object_ref(parent_directory);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out:
 | 
				
			||||||
 | 
						g_object_unref(parent_directory);
 | 
				
			||||||
 | 
						g_ptr_array_free(subdirs, TRUE);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// This would be more efficient iteratively, but it's not that important.
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					model_next_directory_within_parents(FivIoModel *self, GFile *directory)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFile *parent_directory = g_file_get_parent(directory);
 | 
				
			||||||
 | 
						if (!parent_directory)
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *result = NULL;
 | 
				
			||||||
 | 
						GPtrArray *subdirs = model_entry_array_new();
 | 
				
			||||||
 | 
						if (!model_reload_to(self, parent_directory, subdirs, NULL, NULL))
 | 
				
			||||||
 | 
							goto out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gboolean found_self = FALSE;
 | 
				
			||||||
 | 
						for (gsize i = 0; i < subdirs->len; i++) {
 | 
				
			||||||
 | 
							FivIoModelEntry *entry = g_ptr_array_index(subdirs, i);
 | 
				
			||||||
 | 
							result = g_file_new_for_uri(entry->uri);
 | 
				
			||||||
 | 
							if (found_self)
 | 
				
			||||||
 | 
								goto out;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							found_self = g_file_equal(result, directory);
 | 
				
			||||||
 | 
							g_clear_object(&result);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (!result)
 | 
				
			||||||
 | 
							result = model_next_directory_within_parents(self, parent_directory);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					out:
 | 
				
			||||||
 | 
						g_object_unref(parent_directory);
 | 
				
			||||||
 | 
						g_ptr_array_free(subdirs, TRUE);
 | 
				
			||||||
 | 
						return result;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GFile *
 | 
				
			||||||
 | 
					fiv_io_model_get_next_directory(FivIoModel *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (self->subdirs->len) {
 | 
				
			||||||
 | 
							FivIoModelEntry *entry = g_ptr_array_index(self->subdirs, 0);
 | 
				
			||||||
 | 
							return g_file_new_for_uri(entry->uri);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return model_next_directory_within_parents(self, self->directory);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_model_finalize(GObject *gobject)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivIoModel *self = FIV_IO_MODEL(gobject);
 | 
				
			||||||
 | 
						for (GPatternSpec **p = self->supported_patterns; *p; p++)
 | 
				
			||||||
 | 
							g_pattern_spec_free(*p);
 | 
				
			||||||
 | 
						g_free(self->supported_patterns);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_clear_object(&self->directory);
 | 
				
			||||||
 | 
						g_clear_object(&self->monitor);
 | 
				
			||||||
 | 
						g_ptr_array_free(self->subdirs, TRUE);
 | 
				
			||||||
 | 
						g_ptr_array_free(self->files, TRUE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						G_OBJECT_CLASS(fiv_io_model_parent_class)->finalize(gobject);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_model_get_property(
 | 
				
			||||||
 | 
						GObject *object, guint property_id, GValue *value, GParamSpec *pspec)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivIoModel *self = FIV_IO_MODEL(object);
 | 
				
			||||||
 | 
						switch (property_id) {
 | 
				
			||||||
 | 
						case PROP_FILTERING:
 | 
				
			||||||
 | 
							g_value_set_boolean(value, self->filtering);
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case PROP_SORT_FIELD:
 | 
				
			||||||
 | 
							g_value_set_enum(value, self->sort_field);
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case PROP_SORT_DESCENDING:
 | 
				
			||||||
 | 
							g_value_set_boolean(value, self->sort_descending);
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_model_set_property(
 | 
				
			||||||
 | 
						GObject *object, guint property_id, const GValue *value, GParamSpec *pspec)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivIoModel *self = FIV_IO_MODEL(object);
 | 
				
			||||||
 | 
						switch (property_id) {
 | 
				
			||||||
 | 
						case PROP_FILTERING:
 | 
				
			||||||
 | 
							if (self->filtering != g_value_get_boolean(value)) {
 | 
				
			||||||
 | 
								self->filtering = !self->filtering;
 | 
				
			||||||
 | 
								g_object_notify_by_pspec(object, model_properties[property_id]);
 | 
				
			||||||
 | 
								(void) model_reload(self, NULL /* error */);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case PROP_SORT_FIELD:
 | 
				
			||||||
 | 
							if ((int) self->sort_field != g_value_get_enum(value)) {
 | 
				
			||||||
 | 
								self->sort_field = g_value_get_enum(value);
 | 
				
			||||||
 | 
								g_object_notify_by_pspec(object, model_properties[property_id]);
 | 
				
			||||||
 | 
								model_resort(self);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						case PROP_SORT_DESCENDING:
 | 
				
			||||||
 | 
							if (self->sort_descending != g_value_get_boolean(value)) {
 | 
				
			||||||
 | 
								self->sort_descending = !self->sort_descending;
 | 
				
			||||||
 | 
								g_object_notify_by_pspec(object, model_properties[property_id]);
 | 
				
			||||||
 | 
								model_resort(self);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							break;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_model_class_init(FivIoModelClass *klass)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
				
			||||||
 | 
						object_class->get_property = fiv_io_model_get_property;
 | 
				
			||||||
 | 
						object_class->set_property = fiv_io_model_set_property;
 | 
				
			||||||
 | 
						object_class->finalize = fiv_io_model_finalize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						model_properties[PROP_FILTERING] = g_param_spec_boolean(
 | 
				
			||||||
 | 
							"filtering", "Filtering", "Only show non-hidden, supported entries",
 | 
				
			||||||
 | 
							TRUE, G_PARAM_READWRITE);
 | 
				
			||||||
 | 
						model_properties[PROP_SORT_FIELD] = g_param_spec_enum(
 | 
				
			||||||
 | 
							"sort-field", "Sort field", "Sort order",
 | 
				
			||||||
 | 
							FIV_TYPE_IO_MODEL_SORT, FIV_IO_MODEL_SORT_NAME, G_PARAM_READWRITE);
 | 
				
			||||||
 | 
						model_properties[PROP_SORT_DESCENDING] = g_param_spec_boolean(
 | 
				
			||||||
 | 
							"sort-descending", "Sort descending", "Use reverse sort order",
 | 
				
			||||||
 | 
							FALSE, G_PARAM_READWRITE);
 | 
				
			||||||
 | 
						g_object_class_install_properties(
 | 
				
			||||||
 | 
							object_class, N_PROPERTIES, model_properties);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// All entries might have changed.
 | 
				
			||||||
 | 
						model_signals[RELOADED] =
 | 
				
			||||||
 | 
							g_signal_new("reloaded", G_TYPE_FROM_CLASS(klass), 0, 0,
 | 
				
			||||||
 | 
								NULL, NULL, NULL, G_TYPE_NONE, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						model_signals[FILES_CHANGED] =
 | 
				
			||||||
 | 
							g_signal_new("files-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
 | 
				
			||||||
 | 
								NULL, NULL, NULL,
 | 
				
			||||||
 | 
								G_TYPE_NONE, 2, FIV_TYPE_IO_MODEL_ENTRY, FIV_TYPE_IO_MODEL_ENTRY);
 | 
				
			||||||
 | 
						model_signals[SUBDIRECTORIES_CHANGED] =
 | 
				
			||||||
 | 
							g_signal_new("subdirectories-changed", G_TYPE_FROM_CLASS(klass), 0, 0,
 | 
				
			||||||
 | 
								NULL, NULL, NULL,
 | 
				
			||||||
 | 
								G_TYPE_NONE, 2, FIV_TYPE_IO_MODEL_ENTRY, FIV_TYPE_IO_MODEL_ENTRY);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_io_model_init(FivIoModel *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						self->filtering = TRUE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						char **types = fiv_io_all_supported_media_types();
 | 
				
			||||||
 | 
						char **globs = extract_mime_globs((const char **) types);
 | 
				
			||||||
 | 
						g_strfreev(types);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gsize n = g_strv_length(globs);
 | 
				
			||||||
 | 
						self->supported_patterns =
 | 
				
			||||||
 | 
							g_malloc0_n(n + 1, sizeof *self->supported_patterns);
 | 
				
			||||||
 | 
						while (n--)
 | 
				
			||||||
 | 
							self->supported_patterns[n] = g_pattern_spec_new(globs[n]);
 | 
				
			||||||
 | 
						g_strfreev(globs);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						self->files = model_entry_array_new();
 | 
				
			||||||
 | 
						self->subdirs = model_entry_array_new();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gboolean
 | 
				
			||||||
 | 
					fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(FIV_IS_IO_MODEL(self), FALSE);
 | 
				
			||||||
 | 
						g_return_val_if_fail(G_IS_FILE(directory), FALSE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_clear_object(&self->directory);
 | 
				
			||||||
 | 
						g_clear_object(&self->monitor);
 | 
				
			||||||
 | 
						self->directory = g_object_ref(directory);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GError *e = NULL;
 | 
				
			||||||
 | 
						if ((self->monitor = g_file_monitor_directory(
 | 
				
			||||||
 | 
								directory, G_FILE_MONITOR_WATCH_MOVES, NULL, &e))) {
 | 
				
			||||||
 | 
							g_signal_connect(self->monitor, "changed",
 | 
				
			||||||
 | 
								G_CALLBACK(on_monitor_changed), self);
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							g_debug("directory monitoring failed: %s", e->message);
 | 
				
			||||||
 | 
							g_error_free(e);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return model_reload(self, error);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GFile *
 | 
				
			||||||
 | 
					fiv_io_model_get_location(FivIoModel *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(FIV_IS_IO_MODEL(self), NULL);
 | 
				
			||||||
 | 
						return self->directory;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoModelEntry *const *
 | 
				
			||||||
 | 
					fiv_io_model_get_files(FivIoModel *self, gsize *len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						*len = self->files->len;
 | 
				
			||||||
 | 
						return (FivIoModelEntry *const *) self->files->pdata;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoModelEntry *const *
 | 
				
			||||||
 | 
					fiv_io_model_get_subdirs(FivIoModel *self, gsize *len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						*len = self->subdirs->len;
 | 
				
			||||||
 | 
						return (FivIoModelEntry *const *) self->subdirs->pdata;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										72
									
								
								fiv-io-model.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,72 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-io-model.h: filesystem
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <gio/gio.h>
 | 
				
			||||||
 | 
					#include <glib.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Avoid glib-mkenums.
 | 
				
			||||||
 | 
					typedef enum _FivIoModelSort {
 | 
				
			||||||
 | 
					#define FIV_IO_MODEL_SORTS(XX) \
 | 
				
			||||||
 | 
						XX(NAME) \
 | 
				
			||||||
 | 
						XX(MTIME)
 | 
				
			||||||
 | 
					#define XX(name) FIV_IO_MODEL_SORT_ ## name,
 | 
				
			||||||
 | 
						FIV_IO_MODEL_SORTS(XX)
 | 
				
			||||||
 | 
					#undef XX
 | 
				
			||||||
 | 
						FIV_IO_MODEL_SORT_COUNT
 | 
				
			||||||
 | 
					} FivIoModelSort;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GType fiv_io_model_sort_get_type(void) G_GNUC_CONST;
 | 
				
			||||||
 | 
					#define FIV_TYPE_IO_MODEL_SORT (fiv_io_model_sort_get_type())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
						const char *uri;                    ///< GIO URI
 | 
				
			||||||
 | 
						const char *target_uri;             ///< GIO URI for any target
 | 
				
			||||||
 | 
						const char *display_name;           ///< Label for the file
 | 
				
			||||||
 | 
						const char *collate_key;            ///< Collate key for the filename
 | 
				
			||||||
 | 
						guint64 filesize;                   ///< Filesize in bytes
 | 
				
			||||||
 | 
						gint64 mtime_msec;                  ///< Modification time in milliseconds
 | 
				
			||||||
 | 
					} FivIoModelEntry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GType fiv_io_model_entry_get_type(void) G_GNUC_CONST;
 | 
				
			||||||
 | 
					#define FIV_TYPE_IO_MODEL_ENTRY (fiv_io_model_entry_get_type())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoModelEntry *fiv_io_model_entry_ref(FivIoModelEntry *self);
 | 
				
			||||||
 | 
					void fiv_io_model_entry_unref(FivIoModelEntry *self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define FIV_TYPE_IO_MODEL (fiv_io_model_get_type())
 | 
				
			||||||
 | 
					G_DECLARE_FINAL_TYPE(FivIoModel, fiv_io_model, FIV, IO_MODEL, GObject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Loads a directory. Clears itself even on failure.
 | 
				
			||||||
 | 
					gboolean fiv_io_model_open(FivIoModel *self, GFile *directory, GError **error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns the current location as a GFile.
 | 
				
			||||||
 | 
					/// There is no ownership transfer, and the object may be NULL.
 | 
				
			||||||
 | 
					GFile *fiv_io_model_get_location(FivIoModel *self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns the previous VFS directory in order, or NULL.
 | 
				
			||||||
 | 
					GFile *fiv_io_model_get_previous_directory(FivIoModel *self);
 | 
				
			||||||
 | 
					/// Returns the next VFS directory in order, or NULL.
 | 
				
			||||||
 | 
					GFile *fiv_io_model_get_next_directory(FivIoModel *self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoModelEntry *const *fiv_io_model_get_files(FivIoModel *self, gsize *len);
 | 
				
			||||||
 | 
					FivIoModelEntry *const *fiv_io_model_get_subdirs(FivIoModel *self, gsize *len);
 | 
				
			||||||
							
								
								
									
										219
									
								
								fiv-io.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,219 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-io.h: image operations
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2024, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <cairo.h>
 | 
				
			||||||
 | 
					#include <gio/gio.h>
 | 
				
			||||||
 | 
					#include <glib.h>
 | 
				
			||||||
 | 
					#include <webp/encode.h>  // WebPConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef enum _FivIoOrientation FivIoOrientation;
 | 
				
			||||||
 | 
					typedef struct _FivIoRenderClosure FivIoRenderClosure;
 | 
				
			||||||
 | 
					typedef struct _FivIoImage FivIoImage;
 | 
				
			||||||
 | 
					typedef struct _FivIoProfile FivIoProfile;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Colour management -------------------------------------------------------
 | 
				
			||||||
 | 
					// Note that without a CMM, all FivIoCmm and FivIoProfile will be returned NULL.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GBytes *fiv_io_profile_to_bytes(FivIoProfile *profile);
 | 
				
			||||||
 | 
					void fiv_io_profile_free(FivIoProfile *self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define FIV_TYPE_IO_CMM (fiv_io_cmm_get_type())
 | 
				
			||||||
 | 
					G_DECLARE_FINAL_TYPE(FivIoCmm, fiv_io_cmm, FIV, IO_CMM, GObject)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoCmm *fiv_io_cmm_get_default(void);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoProfile *fiv_io_cmm_get_profile(
 | 
				
			||||||
 | 
						FivIoCmm *self, const void *data, size_t len);
 | 
				
			||||||
 | 
					FivIoProfile *fiv_io_cmm_get_profile_from_bytes(FivIoCmm *self, GBytes *bytes);
 | 
				
			||||||
 | 
					FivIoProfile *fiv_io_cmm_get_profile_sRGB(FivIoCmm *self);
 | 
				
			||||||
 | 
					FivIoProfile *fiv_io_cmm_get_profile_sRGB_gamma(FivIoCmm *self, double gamma);
 | 
				
			||||||
 | 
					FivIoProfile *fiv_io_cmm_get_profile_parametric(
 | 
				
			||||||
 | 
						FivIoCmm *self, double gamma, double whitepoint[2], double primaries[6]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void fiv_io_premultiply_argb32(FivIoImage *image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void fiv_io_cmm_cmyk(FivIoCmm *self,
 | 
				
			||||||
 | 
					    FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
 | 
				
			||||||
 | 
					void fiv_io_cmm_4x16le_direct(FivIoCmm *self, unsigned char *data,
 | 
				
			||||||
 | 
					    int w, int h, FivIoProfile *source, FivIoProfile *target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void fiv_io_cmm_argb32_premultiply(FivIoCmm *self,
 | 
				
			||||||
 | 
						FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
 | 
				
			||||||
 | 
					#define fiv_io_cmm_argb32_premultiply_page(cmm, page, target) \
 | 
				
			||||||
 | 
						fiv_io_cmm_page((cmm), (page), (target), fiv_io_cmm_argb32_premultiply)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void fiv_io_cmm_page(FivIoCmm *self, FivIoImage *page, FivIoProfile *target,
 | 
				
			||||||
 | 
						void (*frame_cb) (FivIoCmm *,
 | 
				
			||||||
 | 
							FivIoImage *, FivIoProfile *, FivIoProfile *));
 | 
				
			||||||
 | 
					void fiv_io_cmm_any(FivIoCmm *self,
 | 
				
			||||||
 | 
						FivIoImage *image, FivIoProfile *source, FivIoProfile *target);
 | 
				
			||||||
 | 
					FivIoImage *fiv_io_cmm_finish(FivIoCmm *self,
 | 
				
			||||||
 | 
						FivIoImage *image, FivIoProfile *target);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Loading -----------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extern const char *fiv_io_supported_media_types[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					gchar **fiv_io_all_supported_media_types(void);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf Table 6
 | 
				
			||||||
 | 
					enum _FivIoOrientation {
 | 
				
			||||||
 | 
						FivIoOrientationUnknown   = 0,
 | 
				
			||||||
 | 
						FivIoOrientation0         = 1,
 | 
				
			||||||
 | 
						FivIoOrientationMirror0   = 2,
 | 
				
			||||||
 | 
						FivIoOrientation180       = 3,
 | 
				
			||||||
 | 
						FivIoOrientationMirror180 = 4,
 | 
				
			||||||
 | 
						FivIoOrientationMirror270 = 5,
 | 
				
			||||||
 | 
						FivIoOrientation90        = 6,
 | 
				
			||||||
 | 
						FivIoOrientationMirror90  = 7,
 | 
				
			||||||
 | 
						FivIoOrientation270       = 8
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// TODO(p): Maybe make FivIoProfile a referencable type,
 | 
				
			||||||
 | 
					// then loaders could store it in their closures.
 | 
				
			||||||
 | 
					struct _FivIoRenderClosure {
 | 
				
			||||||
 | 
						/// The rendering is allowed to fail, returning NULL.
 | 
				
			||||||
 | 
						FivIoImage *(*render)(
 | 
				
			||||||
 | 
							FivIoRenderClosure *, FivIoCmm *, FivIoProfile *, double scale);
 | 
				
			||||||
 | 
						void (*destroy)(FivIoRenderClosure *);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Metadata are typically attached to all Cairo surfaces in an animation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct _FivIoImage {
 | 
				
			||||||
 | 
						uint8_t *data;                      ///< Raw image data
 | 
				
			||||||
 | 
						cairo_format_t format;              ///< Data format
 | 
				
			||||||
 | 
						uint32_t width;                     ///< Width of the image in pixels
 | 
				
			||||||
 | 
						uint32_t stride;                    ///< Row stride in bytes
 | 
				
			||||||
 | 
						uint32_t height;                    ///< Height of the image in pixels
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivIoOrientation orientation;       ///< Orientation to use for display
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GBytes *exif;                       ///< Raw Exif/TIFF segment
 | 
				
			||||||
 | 
						GBytes *icc;                        ///< Raw ICC profile data
 | 
				
			||||||
 | 
						GBytes *xmp;                        ///< Raw XMP data
 | 
				
			||||||
 | 
						GBytes *thum;                       ///< WebP THUM chunk, for our thumbnails
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// GHashTable with key-value pairs from PNG's tEXt, zTXt, iTXt chunks.
 | 
				
			||||||
 | 
						/// Currently only read by fiv_io_open_png_thumbnail().
 | 
				
			||||||
 | 
						GHashTable *text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// A FivIoRenderClosure for parametrized re-rendering of vector formats.
 | 
				
			||||||
 | 
						/// This is attached at the page level.
 | 
				
			||||||
 | 
						FivIoRenderClosure *render;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// The first frame of the next page, in a chain.
 | 
				
			||||||
 | 
						/// There is no wrap-around.
 | 
				
			||||||
 | 
						FivIoImage *page_next;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// The first frame of the previous page, in a chain.
 | 
				
			||||||
 | 
						/// There is no wrap-around. This is a weak pointer.
 | 
				
			||||||
 | 
						FivIoImage *page_previous;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// The next frame in a sequence, in a chain, pre-composited.
 | 
				
			||||||
 | 
						/// There is no wrap-around.
 | 
				
			||||||
 | 
						FivIoImage *frame_next;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// The previous frame in a sequence, in a chain, pre-composited.
 | 
				
			||||||
 | 
						/// This is a weak pointer that wraps around,
 | 
				
			||||||
 | 
						/// and needn't be present for static images.
 | 
				
			||||||
 | 
						FivIoImage *frame_previous;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// Frame duration in milliseconds.
 | 
				
			||||||
 | 
						int64_t frame_duration;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						/// How many times to repeat the animation, or zero for +inf.
 | 
				
			||||||
 | 
						uint64_t loops;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoImage *fiv_io_image_ref(FivIoImage *image);
 | 
				
			||||||
 | 
					void fiv_io_image_unref(FivIoImage *image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Analogous to cairo_image_surface_create(). May return NULL.
 | 
				
			||||||
 | 
					FivIoImage *fiv_io_image_new(
 | 
				
			||||||
 | 
						cairo_format_t format, uint32_t width, uint32_t height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Return a new Cairo image surface referencing the same data as the image,
 | 
				
			||||||
 | 
					/// eating the reference to it.
 | 
				
			||||||
 | 
					cairo_surface_t *fiv_io_image_to_surface(FivIoImage *image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Return a new Cairo image surface referencing the same data as the image,
 | 
				
			||||||
 | 
					/// without eating the image's reference.
 | 
				
			||||||
 | 
					cairo_surface_t *fiv_io_image_to_surface_noref(const FivIoImage *image);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct {
 | 
				
			||||||
 | 
						const char *uri;                    ///< Source URI
 | 
				
			||||||
 | 
						FivIoCmm *cmm;                      ///< Colour management module or NULL
 | 
				
			||||||
 | 
						FivIoProfile *screen_profile;       ///< Target colour space or NULL
 | 
				
			||||||
 | 
						int screen_dpi;                     ///< Target DPI
 | 
				
			||||||
 | 
						gboolean enhance;                   ///< Enhance JPEG (currently)
 | 
				
			||||||
 | 
						gboolean first_frame_only;          ///< Only interested in the 1st frame
 | 
				
			||||||
 | 
						GPtrArray *warnings;                ///< String vector for non-fatal errors
 | 
				
			||||||
 | 
					} FivIoOpenContext;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoImage *fiv_io_open(const FivIoOpenContext *ctx, GError **error);
 | 
				
			||||||
 | 
					FivIoImage *fiv_io_open_from_data(
 | 
				
			||||||
 | 
						const char *data, size_t len, const FivIoOpenContext *ctx, GError **error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					FivIoImage *fiv_io_open_png_thumbnail(const char *path, GError **error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Metadata ----------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Returns a rendering matrix for an image (user space to pattern space),
 | 
				
			||||||
 | 
					/// and its target dimensions.
 | 
				
			||||||
 | 
					cairo_matrix_t fiv_io_orientation_apply(const FivIoImage *image,
 | 
				
			||||||
 | 
						FivIoOrientation orientation, double *width, double *height);
 | 
				
			||||||
 | 
					cairo_matrix_t fiv_io_orientation_matrix(
 | 
				
			||||||
 | 
						FivIoOrientation orientation, double width, double height);
 | 
				
			||||||
 | 
					void fiv_io_orientation_dimensions(const FivIoImage *image,
 | 
				
			||||||
 | 
						FivIoOrientation orientation, double *width, double *height);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Extracts the orientation field from Exif, if there's any.
 | 
				
			||||||
 | 
					FivIoOrientation fiv_io_exif_orientation(const guint8 *exif, gsize len);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Save metadata attached by this module in Exiv2 format.
 | 
				
			||||||
 | 
					gboolean fiv_io_save_metadata(
 | 
				
			||||||
 | 
						const FivIoImage *page, const char *path, GError **error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Thumbnail passing utilities ---------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum { FIV_IO_SERIALIZE_LOW_QUALITY = 1 << 0 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void fiv_io_serialize_to_stdout(cairo_surface_t *surface, guint64 user_data);
 | 
				
			||||||
 | 
					cairo_surface_t *fiv_io_deserialize(GBytes *bytes, guint64 *user_data);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GBytes *fiv_io_serialize_for_search(cairo_surface_t *surface, GError **error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Export ------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Encodes an image as a WebP bitstream, following the configuration.
 | 
				
			||||||
 | 
					/// The result needs to be freed using WebPFree/WebPDataClear().
 | 
				
			||||||
 | 
					unsigned char *fiv_io_encode_webp(
 | 
				
			||||||
 | 
						FivIoImage *image, const WebPConfig *config, size_t *len);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Saves the page as a lossless WebP still picture or animation.
 | 
				
			||||||
 | 
					/// If no exact frame is specified, this potentially creates an animation.
 | 
				
			||||||
 | 
					gboolean fiv_io_save(FivIoImage *page, FivIoImage *frame,
 | 
				
			||||||
 | 
						FivIoProfile *target, const char *path, GError **error);
 | 
				
			||||||
							
								
								
									
										511
									
								
								fiv-jpegcrop.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,511 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-jpegcrop.c: lossless JPEG cropper
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <gtk/gtk.h>
 | 
				
			||||||
 | 
					#include <turbojpeg.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <stdlib.h>
 | 
				
			||||||
 | 
					#include <string.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "config.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Utilities ---------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void exit_fatal(const char *format, ...) G_GNUC_PRINTF(1, 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					exit_fatal(const char *format, ...)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						va_list ap;
 | 
				
			||||||
 | 
						va_start(ap, format);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *format_nl = g_strdup_printf("%s\n", format);
 | 
				
			||||||
 | 
						vfprintf(stderr, format_nl, ap);
 | 
				
			||||||
 | 
						free(format_nl);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						va_end(ap);
 | 
				
			||||||
 | 
						exit(EXIT_FAILURE);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Main --------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct {
 | 
				
			||||||
 | 
						GFile *location;
 | 
				
			||||||
 | 
						gchar *data;
 | 
				
			||||||
 | 
						gsize len;
 | 
				
			||||||
 | 
						int width, height, subsampling, colorspace;
 | 
				
			||||||
 | 
						int mcu_width, mcu_height;
 | 
				
			||||||
 | 
						cairo_surface_t *surface;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						int top, left, right, bottom;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *label;
 | 
				
			||||||
 | 
						GtkWidget *window;
 | 
				
			||||||
 | 
						GtkWidget *scrolled;
 | 
				
			||||||
 | 
						GtkWidget *view;
 | 
				
			||||||
 | 
					} g;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					show_error_dialog(GError *error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkWidget *dialog =
 | 
				
			||||||
 | 
							gtk_message_dialog_new(GTK_WINDOW(g.window), GTK_DIALOG_MODAL,
 | 
				
			||||||
 | 
								GTK_MESSAGE_ERROR, GTK_BUTTONS_CLOSE, "%s", error->message);
 | 
				
			||||||
 | 
						gtk_dialog_run(GTK_DIALOG(dialog));
 | 
				
			||||||
 | 
						gtk_widget_destroy(dialog);
 | 
				
			||||||
 | 
						g_error_free(error);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					on_draw(G_GNUC_UNUSED GtkWidget *widget, cairo_t *cr,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						cairo_set_source_surface(cr, g.surface, 1, 1);
 | 
				
			||||||
 | 
						cairo_paint(cr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cairo_rectangle(cr,
 | 
				
			||||||
 | 
							1 + g.left - 0.5,
 | 
				
			||||||
 | 
							1 + g.top - 0.5,
 | 
				
			||||||
 | 
							g.right - g.left + 1,
 | 
				
			||||||
 | 
							g.bottom - g.top + 1);
 | 
				
			||||||
 | 
						cairo_set_source_rgb(cr, 1, 1, 1);
 | 
				
			||||||
 | 
						cairo_set_line_width(cr, 1);
 | 
				
			||||||
 | 
						cairo_set_operator(cr, CAIRO_OPERATOR_DIFFERENCE);
 | 
				
			||||||
 | 
						cairo_stroke(cr);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD);
 | 
				
			||||||
 | 
						cairo_rectangle(cr, 1, 1, g.width, g.height);
 | 
				
			||||||
 | 
						cairo_rectangle(
 | 
				
			||||||
 | 
							cr, g.left, g.top, g.right - g.left + 2, g.bottom - g.top + 2);
 | 
				
			||||||
 | 
						cairo_clip(cr);
 | 
				
			||||||
 | 
						cairo_set_source_rgba(cr, 0, 0, 0, 0.5);
 | 
				
			||||||
 | 
						cairo_set_operator(cr, CAIRO_OPERATOR_OVER);
 | 
				
			||||||
 | 
						cairo_paint(cr);
 | 
				
			||||||
 | 
						return TRUE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					run_chooser(GtkWidget *dialog)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
 | 
				
			||||||
 | 
						gtk_file_chooser_set_local_only(chooser, FALSE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkFileFilter *jpeg = gtk_file_filter_new();
 | 
				
			||||||
 | 
						gtk_file_filter_add_mime_type(jpeg, "image/jpeg");
 | 
				
			||||||
 | 
						gtk_file_filter_add_pattern(jpeg, "*.jpg");
 | 
				
			||||||
 | 
						gtk_file_filter_add_pattern(jpeg, "*.jpeg");
 | 
				
			||||||
 | 
						gtk_file_filter_add_pattern(jpeg, "*.jpe");
 | 
				
			||||||
 | 
						gtk_file_filter_set_name(jpeg, "JPEG");
 | 
				
			||||||
 | 
						gtk_file_chooser_add_filter(chooser, jpeg);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkFileFilter *all = gtk_file_filter_new();
 | 
				
			||||||
 | 
						gtk_file_filter_add_pattern(all, "*");
 | 
				
			||||||
 | 
						gtk_file_filter_set_name(all, "All files");
 | 
				
			||||||
 | 
						gtk_file_chooser_add_filter(chooser, all);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *file = NULL;
 | 
				
			||||||
 | 
						switch (gtk_dialog_run(GTK_DIALOG(dialog))) {
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							gtk_widget_destroy(dialog);
 | 
				
			||||||
 | 
							// Fall-through.
 | 
				
			||||||
 | 
						case GTK_RESPONSE_NONE:
 | 
				
			||||||
 | 
							return file;
 | 
				
			||||||
 | 
						case GTK_RESPONSE_ACCEPT:
 | 
				
			||||||
 | 
							file = gtk_file_chooser_get_file(chooser);
 | 
				
			||||||
 | 
							gtk_widget_destroy(dialog);
 | 
				
			||||||
 | 
							return file;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					choose_file_to_open(void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkWidget *dialog = gtk_file_chooser_dialog_new("Open image",
 | 
				
			||||||
 | 
							NULL, GTK_FILE_CHOOSER_ACTION_OPEN,
 | 
				
			||||||
 | 
							"_Cancel", GTK_RESPONSE_CANCEL,
 | 
				
			||||||
 | 
							"_Open", GTK_RESPONSE_ACCEPT, NULL);
 | 
				
			||||||
 | 
						return run_chooser(dialog);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					choose_file_to_save(void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkWidget *dialog = gtk_file_chooser_dialog_new("Saved cropped image as",
 | 
				
			||||||
 | 
							GTK_WINDOW(g.window), GTK_FILE_CHOOSER_ACTION_SAVE,
 | 
				
			||||||
 | 
							"_Cancel", GTK_RESPONSE_CANCEL,
 | 
				
			||||||
 | 
							"_Save", GTK_RESPONSE_ACCEPT, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkFileChooser *chooser = GTK_FILE_CHOOSER(dialog);
 | 
				
			||||||
 | 
						gtk_file_chooser_set_do_overwrite_confirmation(chooser, TRUE);
 | 
				
			||||||
 | 
						(void) gtk_file_chooser_set_file(chooser, g.location, NULL);
 | 
				
			||||||
 | 
						return run_chooser(dialog);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_save_as(G_GNUC_UNUSED GtkButton *button, G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						tjhandle h = tjInitTransform();
 | 
				
			||||||
 | 
						if (!h) {
 | 
				
			||||||
 | 
							show_error_dialog(g_error_new_literal(
 | 
				
			||||||
 | 
								G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Convert up front, because the target is in memory.
 | 
				
			||||||
 | 
						tjtransform t = {
 | 
				
			||||||
 | 
							.r.x = g.left,
 | 
				
			||||||
 | 
							.r.y = g.top,
 | 
				
			||||||
 | 
							.r.w = g.right - g.left,
 | 
				
			||||||
 | 
							.r.h = g.bottom - g.top,
 | 
				
			||||||
 | 
							.op = TJXOP_NONE,
 | 
				
			||||||
 | 
							.options = TJXOPT_CROP | TJXOPT_PROGRESSIVE | TJXOPT_PERFECT,
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						guchar *data = NULL;
 | 
				
			||||||
 | 
						gulong len = 0;
 | 
				
			||||||
 | 
						if (tjTransform(h, (const guchar *) g.data, g.len, 1, &data, &len, &t, 0)) {
 | 
				
			||||||
 | 
							show_error_dialog(g_error_new_literal(
 | 
				
			||||||
 | 
								G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
 | 
				
			||||||
 | 
							goto out;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *file = choose_file_to_save();
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						if (file &&
 | 
				
			||||||
 | 
							!g_file_replace_contents(file, (const char *) data, len, NULL, FALSE,
 | 
				
			||||||
 | 
								G_FILE_CREATE_NONE, NULL, NULL, &error)) {
 | 
				
			||||||
 | 
							show_error_dialog(error);
 | 
				
			||||||
 | 
							goto out;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_clear_object(&file);
 | 
				
			||||||
 | 
						tjFree(data);
 | 
				
			||||||
 | 
					out:
 | 
				
			||||||
 | 
						if (tjDestroy(h)) {
 | 
				
			||||||
 | 
							show_error_dialog(g_error_new_literal(
 | 
				
			||||||
 | 
								G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h)));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					update_label(void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gchar *text = g_strdup_printf("(%d, %d) × (%d, %d)", g.left, g.top,
 | 
				
			||||||
 | 
							g.right - g.left, g.bottom - g.top);
 | 
				
			||||||
 | 
						gtk_label_set_label(GTK_LABEL(g.label), text);
 | 
				
			||||||
 | 
						g_free(text);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					update(void)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						update_label();
 | 
				
			||||||
 | 
						gtk_widget_queue_draw(g.view);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_reset(G_GNUC_UNUSED GtkButton *button, G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g.top = 0;
 | 
				
			||||||
 | 
						g.left = 0;
 | 
				
			||||||
 | 
						g.right = g.width;
 | 
				
			||||||
 | 
						g.bottom = g.height;
 | 
				
			||||||
 | 
						update();
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					on_mouse(guint state, guint button, gdouble x, gdouble y)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (state != 0)
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						switch (button) {
 | 
				
			||||||
 | 
						case GDK_BUTTON_PRIMARY:
 | 
				
			||||||
 | 
							g.left = CLAMP((int) (x - 1), 0, g.right) / g.mcu_width * g.mcu_width;
 | 
				
			||||||
 | 
							g.top = CLAMP((int) (y - 1), 0, g.bottom) / g.mcu_height * g.mcu_height;
 | 
				
			||||||
 | 
							update();
 | 
				
			||||||
 | 
							return TRUE;
 | 
				
			||||||
 | 
						case GDK_BUTTON_SECONDARY:
 | 
				
			||||||
 | 
							// Inclusive of pointer position.
 | 
				
			||||||
 | 
							g.right = CLAMP(x, g.left, g.width);
 | 
				
			||||||
 | 
							g.bottom = CLAMP(y, g.top, g.height);
 | 
				
			||||||
 | 
							update();
 | 
				
			||||||
 | 
							return TRUE;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					on_press(G_GNUC_UNUSED GtkWidget *self, GdkEventButton *event,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return on_mouse(event->state, event->button, event->x, event->y);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					on_motion(G_GNUC_UNUSED GtkWidget *self, GdkEventMotion *event,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						switch (event->state) {
 | 
				
			||||||
 | 
						case GDK_BUTTON1_MASK:
 | 
				
			||||||
 | 
							return on_mouse(0, GDK_BUTTON_PRIMARY, event->x, event->y);
 | 
				
			||||||
 | 
						case GDK_BUTTON3_MASK:
 | 
				
			||||||
 | 
							return on_mouse(0, GDK_BUTTON_SECONDARY, event->x, event->y);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return FALSE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_drag_begin(
 | 
				
			||||||
 | 
						GtkGestureDrag *self, gdouble start_x, gdouble start_y, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// The middle mouse button will never be triggered by touch screens,
 | 
				
			||||||
 | 
						// so there is only the NULL sequence to care about.
 | 
				
			||||||
 | 
						gtk_gesture_set_state(GTK_GESTURE(self), GTK_EVENT_SEQUENCE_CLAIMED);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GdkWindow *window = gtk_widget_get_window(g.view);
 | 
				
			||||||
 | 
						GdkCursor *cursor =
 | 
				
			||||||
 | 
							gdk_cursor_new_from_name(gdk_window_get_display(window), "grabbing");
 | 
				
			||||||
 | 
						gdk_window_set_cursor(window, cursor);
 | 
				
			||||||
 | 
						g_object_unref(cursor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						double *last = user_data;
 | 
				
			||||||
 | 
						last[0] = start_x;
 | 
				
			||||||
 | 
						last[1] = start_y;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_drag_update(GtkGestureDrag *self, gdouble offset_x, gdouble offset_y,
 | 
				
			||||||
 | 
						gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						double start_x = 0, start_y = 0;
 | 
				
			||||||
 | 
						gtk_gesture_drag_get_start_point(self, &start_x, &start_y);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						double *last = user_data,
 | 
				
			||||||
 | 
							diff_x = (start_x + offset_x) - last[0],
 | 
				
			||||||
 | 
							diff_y = (start_y + offset_y) - last[1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						last[0] = start_x + offset_x;
 | 
				
			||||||
 | 
						last[1] = start_y + offset_y;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkScrolledWindow *sw = GTK_SCROLLED_WINDOW(g.scrolled);
 | 
				
			||||||
 | 
						GtkAdjustment *h = gtk_scrolled_window_get_hadjustment(sw);
 | 
				
			||||||
 | 
						GtkAdjustment *v = gtk_scrolled_window_get_vadjustment(sw);
 | 
				
			||||||
 | 
						if (diff_x)
 | 
				
			||||||
 | 
							gtk_adjustment_set_value(h, gtk_adjustment_get_value(h) - diff_x);
 | 
				
			||||||
 | 
						if (diff_y)
 | 
				
			||||||
 | 
							gtk_adjustment_set_value(v, gtk_adjustment_get_value(v) - diff_y);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_drag_end(G_GNUC_UNUSED GtkGestureDrag *self, G_GNUC_UNUSED gdouble start_x,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gdouble start_y, G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// Cursors follow the widget hierarchy.
 | 
				
			||||||
 | 
						gdk_window_set_cursor(gtk_widget_get_window(g.view), NULL);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					open_jpeg(const char *data, gsize len, GError **error)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						tjhandle h = tjInitDecompress();
 | 
				
			||||||
 | 
						if (!h) {
 | 
				
			||||||
 | 
							g_set_error_literal(
 | 
				
			||||||
 | 
								error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (tjDecompressHeader3(h, (const guint8 *) data, len, &g.width, &g.height,
 | 
				
			||||||
 | 
								&g.subsampling, &g.colorspace)) {
 | 
				
			||||||
 | 
							g_set_error_literal(
 | 
				
			||||||
 | 
								error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
 | 
				
			||||||
 | 
							tjDestroy(h);
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.top = 0;
 | 
				
			||||||
 | 
						g.left = 0;
 | 
				
			||||||
 | 
						g.right = g.width;
 | 
				
			||||||
 | 
						g.bottom = g.height;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.mcu_width = tjMCUWidth[g.subsampling];
 | 
				
			||||||
 | 
						g.mcu_height = tjMCUHeight[g.subsampling];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (tjDestroy(h)) {
 | 
				
			||||||
 | 
							g_set_error_literal(
 | 
				
			||||||
 | 
								error, G_IO_ERROR, G_IO_ERROR_FAILED, tjGetErrorStr2(h));
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): Eventually, convert to using fiv-io.c directly,
 | 
				
			||||||
 | 
						// which will pull in most of fiv's dependencies,
 | 
				
			||||||
 | 
						// but also enable correct color management, even for CMYK.
 | 
				
			||||||
 | 
						// NOTE: It's possible to include this as a mode of the main binary.
 | 
				
			||||||
 | 
						GInputStream *is = g_memory_input_stream_new_from_data(data, len, NULL);
 | 
				
			||||||
 | 
						GdkPixbuf *pixbuf = gdk_pixbuf_new_from_stream(is, NULL, error);
 | 
				
			||||||
 | 
						g_object_unref(is);
 | 
				
			||||||
 | 
						if (!pixbuf)
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const char *orientation = gdk_pixbuf_get_option(pixbuf, "orientation");
 | 
				
			||||||
 | 
						if (orientation && strlen(orientation) == 1) {
 | 
				
			||||||
 | 
							int n = *orientation - '0';
 | 
				
			||||||
 | 
							if (n >= 1 && n <= 8) {
 | 
				
			||||||
 | 
								// TODO(p): Apply this to the view, somehow.
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.surface = gdk_cairo_surface_create_from_pixbuf(pixbuf, 1, NULL);
 | 
				
			||||||
 | 
						cairo_status_t surface_status = cairo_surface_status(g.surface);
 | 
				
			||||||
 | 
						if (surface_status != CAIRO_STATUS_SUCCESS) {
 | 
				
			||||||
 | 
							g_set_error_literal(error, G_IO_ERROR, G_IO_ERROR_FAILED,
 | 
				
			||||||
 | 
								cairo_status_to_string(surface_status));
 | 
				
			||||||
 | 
							g_clear_pointer(&g.surface, cairo_surface_destroy);
 | 
				
			||||||
 | 
							g_object_unref(pixbuf);
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return TRUE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int
 | 
				
			||||||
 | 
					main(int argc, char *argv[])
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gboolean show_version = FALSE;
 | 
				
			||||||
 | 
						gchar **args = NULL;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const GOptionEntry options[] = {
 | 
				
			||||||
 | 
							{G_OPTION_REMAINING, 0, 0, G_OPTION_ARG_FILENAME_ARRAY, &args,
 | 
				
			||||||
 | 
								NULL, "[FILE | URI]"},
 | 
				
			||||||
 | 
							{"version", 'V', G_OPTION_FLAG_IN_MAIN, G_OPTION_ARG_NONE,
 | 
				
			||||||
 | 
								&show_version, "Output version information and exit", NULL},
 | 
				
			||||||
 | 
							{},
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						gboolean initialized = gtk_init_with_args(
 | 
				
			||||||
 | 
							&argc, &argv, " - Lossless JPEG cropper", options, NULL, &error);
 | 
				
			||||||
 | 
						if (show_version) {
 | 
				
			||||||
 | 
							const char *version = PROJECT_VERSION;
 | 
				
			||||||
 | 
							printf("%s %s\n", "fiv-jpegcrop", &version[*version == 'v']);
 | 
				
			||||||
 | 
							return 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (!initialized)
 | 
				
			||||||
 | 
							exit_fatal("%s", error->message);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_window_set_default_icon_name(PROJECT_NAME);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): Rather use G_OPTION_ARG_CALLBACK with G_OPTION_FLAG_FILENAME.
 | 
				
			||||||
 | 
						// Alternatively, GOptionContext with gtk_get_option_group(TRUE).
 | 
				
			||||||
 | 
						// Then we can show the help string here instead (in fiv as well).
 | 
				
			||||||
 | 
						if (args && args[1])
 | 
				
			||||||
 | 
							exit_fatal("Too many arguments");
 | 
				
			||||||
 | 
						else if (args)
 | 
				
			||||||
 | 
							g.location = g_file_new_for_commandline_arg(args[0]);
 | 
				
			||||||
 | 
						else if (!(g.location = choose_file_to_open()))
 | 
				
			||||||
 | 
							exit(EXIT_SUCCESS);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.window = gtk_window_new(GTK_WINDOW_TOPLEVEL);
 | 
				
			||||||
 | 
						g_signal_connect(g.window, "destroy", G_CALLBACK(gtk_main_quit), NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFileInfo *info = g_file_query_info(g.location,
 | 
				
			||||||
 | 
							G_FILE_ATTRIBUTE_STANDARD_NAME
 | 
				
			||||||
 | 
							"," G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
 | 
				
			||||||
 | 
							G_FILE_QUERY_INFO_NONE, NULL, &error);
 | 
				
			||||||
 | 
						if (!info ||
 | 
				
			||||||
 | 
							!g_file_load_contents(
 | 
				
			||||||
 | 
								g.location, NULL, &g.data, &g.len, NULL, &error) ||
 | 
				
			||||||
 | 
							!open_jpeg(g.data, g.len, &error)) {
 | 
				
			||||||
 | 
							show_error_dialog(error);
 | 
				
			||||||
 | 
							exit(EXIT_FAILURE);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *header = gtk_header_bar_new();
 | 
				
			||||||
 | 
						gtk_window_set_titlebar(GTK_WINDOW(g.window), header);
 | 
				
			||||||
 | 
						gtk_header_bar_set_title(
 | 
				
			||||||
 | 
							GTK_HEADER_BAR(header), g_file_info_get_display_name(info));
 | 
				
			||||||
 | 
						gtk_header_bar_set_subtitle(GTK_HEADER_BAR(header),
 | 
				
			||||||
 | 
							"Use L/R mouse buttons to adjust the crop region.");
 | 
				
			||||||
 | 
						gtk_header_bar_set_show_close_button(GTK_HEADER_BAR(header), TRUE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.label = gtk_label_new(NULL);
 | 
				
			||||||
 | 
						gtk_header_bar_pack_start(GTK_HEADER_BAR(header), g.label);
 | 
				
			||||||
 | 
						update_label();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *save = gtk_button_new_from_icon_name(
 | 
				
			||||||
 | 
							"document-save-as-symbolic", GTK_ICON_SIZE_BUTTON);
 | 
				
			||||||
 | 
						gtk_widget_set_tooltip_text(save, "Save as...");
 | 
				
			||||||
 | 
						g_signal_connect(save, "clicked", G_CALLBACK(on_save_as), NULL);
 | 
				
			||||||
 | 
						gtk_header_bar_pack_end(GTK_HEADER_BAR(header), save);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *reset = gtk_button_new_with_mnemonic("_Reset");
 | 
				
			||||||
 | 
						gtk_widget_set_tooltip_text(reset, "Reset the crop region");
 | 
				
			||||||
 | 
						g_signal_connect(reset, "clicked", G_CALLBACK(on_reset), NULL);
 | 
				
			||||||
 | 
						gtk_header_bar_pack_end(GTK_HEADER_BAR(header), reset);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.view = gtk_drawing_area_new();
 | 
				
			||||||
 | 
						gtk_widget_set_size_request(g.view, g.width + 2, g.height + 2);
 | 
				
			||||||
 | 
						gtk_widget_add_events(g.view,
 | 
				
			||||||
 | 
							GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK |
 | 
				
			||||||
 | 
								GDK_POINTER_MOTION_MASK);
 | 
				
			||||||
 | 
						g_signal_connect(g.view, "draw",
 | 
				
			||||||
 | 
							G_CALLBACK(on_draw), NULL);
 | 
				
			||||||
 | 
						g_signal_connect(g.view, "button-press-event",
 | 
				
			||||||
 | 
							G_CALLBACK(on_press), NULL);
 | 
				
			||||||
 | 
						g_signal_connect(g.view, "motion-notify-event",
 | 
				
			||||||
 | 
							G_CALLBACK(on_motion), NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g.scrolled = gtk_scrolled_window_new(NULL, NULL);
 | 
				
			||||||
 | 
						gtk_scrolled_window_set_overlay_scrolling(
 | 
				
			||||||
 | 
							GTK_SCROLLED_WINDOW(g.scrolled), FALSE);
 | 
				
			||||||
 | 
						gtk_scrolled_window_set_propagate_natural_width(
 | 
				
			||||||
 | 
							GTK_SCROLLED_WINDOW(g.scrolled), TRUE);
 | 
				
			||||||
 | 
						gtk_scrolled_window_set_propagate_natural_height(
 | 
				
			||||||
 | 
							GTK_SCROLLED_WINDOW(g.scrolled), TRUE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkGesture *drag = gtk_gesture_drag_new(g.scrolled);
 | 
				
			||||||
 | 
						gtk_event_controller_set_propagation_phase(
 | 
				
			||||||
 | 
							GTK_EVENT_CONTROLLER(drag), GTK_PHASE_CAPTURE);
 | 
				
			||||||
 | 
						gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_MIDDLE);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						double last_drag_point[2] = {};
 | 
				
			||||||
 | 
						g_signal_connect(drag, "drag-begin",
 | 
				
			||||||
 | 
							G_CALLBACK(on_drag_begin), last_drag_point);
 | 
				
			||||||
 | 
						g_signal_connect(drag, "drag-update",
 | 
				
			||||||
 | 
							G_CALLBACK(on_drag_update), last_drag_point);
 | 
				
			||||||
 | 
						g_signal_connect(drag, "drag-end",
 | 
				
			||||||
 | 
							G_CALLBACK(on_drag_end), last_drag_point);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(g.scrolled), g.view);
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(g.window), g.scrolled);
 | 
				
			||||||
 | 
						gtk_window_set_default_size(GTK_WINDOW(g.window), 800, 600);
 | 
				
			||||||
 | 
						gtk_widget_show_all(g.window);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// It probably needs to be realized.
 | 
				
			||||||
 | 
						GdkWindow *window = gtk_widget_get_window(g.scrolled);
 | 
				
			||||||
 | 
						GdkCursor *cursor =
 | 
				
			||||||
 | 
							gdk_cursor_new_from_name(gdk_window_get_display(window), "crosshair");
 | 
				
			||||||
 | 
						gdk_window_set_cursor(window, cursor);
 | 
				
			||||||
 | 
						g_object_unref(cursor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_main();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_free(g.data);
 | 
				
			||||||
 | 
						g_object_unref(g.location);
 | 
				
			||||||
 | 
						g_object_unref(info);
 | 
				
			||||||
 | 
						return 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								fiv-jpegcrop.desktop
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[Desktop Entry]
 | 
				
			||||||
 | 
					Type=Application
 | 
				
			||||||
 | 
					Name=fiv JPEG Cropper
 | 
				
			||||||
 | 
					GenericName=Lossless JPEG Cropper
 | 
				
			||||||
 | 
					Icon=fiv
 | 
				
			||||||
 | 
					Exec=fiv-jpegcrop %u
 | 
				
			||||||
 | 
					NoDisplay=true
 | 
				
			||||||
 | 
					Terminal=false
 | 
				
			||||||
 | 
					StartupNotify=true
 | 
				
			||||||
 | 
					Categories=Graphics;2DGraphics;
 | 
				
			||||||
 | 
					MimeType=image/jpeg;
 | 
				
			||||||
							
								
								
									
										9
									
								
								fiv-reverse-search
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					#!/bin/sh -e
 | 
				
			||||||
 | 
					if [ "$#" -ne 2 ]; then
 | 
				
			||||||
 | 
						echo "Usage: $0 SEARCH-ENGINE-URI-PREFIX {PATH | URI}" >&2
 | 
				
			||||||
 | 
						exit 1
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					xdg-open "$1$(fiv --thumbnail-for-search large "$2" \
 | 
				
			||||||
 | 
						| curl --silent --show-error --form 'files[]=@-' https://uguu.se/upload \
 | 
				
			||||||
 | 
						| jq --raw-output '.files[] | .url | @uri')"
 | 
				
			||||||
							
								
								
									
										10
									
								
								fiv-reverse-search.desktop.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,10 @@
 | 
				
			|||||||
 | 
					[Desktop Entry]
 | 
				
			||||||
 | 
					Type=Application
 | 
				
			||||||
 | 
					Name=fiv @NAME@ Reverse Image Search
 | 
				
			||||||
 | 
					GenericName=@NAME@ Reverse Image Search
 | 
				
			||||||
 | 
					Icon=fiv
 | 
				
			||||||
 | 
					Exec=fiv-reverse-search "@URL@" %u
 | 
				
			||||||
 | 
					NoDisplay=true
 | 
				
			||||||
 | 
					Terminal=false
 | 
				
			||||||
 | 
					Categories=Graphics;2DGraphics;
 | 
				
			||||||
 | 
					MimeType=image/png;image/bmp;image/gif;image/x-tga;image/jpeg;image/webp;
 | 
				
			||||||
							
								
								
									
										659
									
								
								fiv-sidebar.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,659 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-sidebar.c: molesting GtkPlacesSidebar
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <gtk/gtk.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "fiv-collection.h"
 | 
				
			||||||
 | 
					#include "fiv-context-menu.h"
 | 
				
			||||||
 | 
					#include "fiv-io.h"
 | 
				
			||||||
 | 
					#include "fiv-sidebar.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct _FivSidebar {
 | 
				
			||||||
 | 
						GtkScrolledWindow parent_instance;
 | 
				
			||||||
 | 
						GtkPlacesSidebar *places;
 | 
				
			||||||
 | 
						GtkWidget *listbox;
 | 
				
			||||||
 | 
						FivIoModel *model;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					G_DEFINE_TYPE(FivSidebar, fiv_sidebar, GTK_TYPE_SCROLLED_WINDOW)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					G_DEFINE_QUARK(fiv-sidebar-drag-gesture-quark, fiv_sidebar_drag_gesture)
 | 
				
			||||||
 | 
					G_DEFINE_QUARK(fiv-sidebar-location-quark, fiv_sidebar_location)
 | 
				
			||||||
 | 
					G_DEFINE_QUARK(fiv-sidebar-self-quark, fiv_sidebar_self)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum {
 | 
				
			||||||
 | 
						OPEN_LOCATION,
 | 
				
			||||||
 | 
						LAST_SIGNAL,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Globals are, sadly, the canonical way of storing signal numbers.
 | 
				
			||||||
 | 
					static guint sidebar_signals[LAST_SIGNAL];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_sidebar_dispose(GObject *gobject)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivSidebar *self = FIV_SIDEBAR(gobject);
 | 
				
			||||||
 | 
						if (self->model) {
 | 
				
			||||||
 | 
							g_signal_handlers_disconnect_by_data(self->model, self);
 | 
				
			||||||
 | 
							g_clear_object(&self->model);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						G_OBJECT_CLASS(fiv_sidebar_parent_class)->dispose(gobject);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_sidebar_realize(GtkWidget *widget)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GTK_WIDGET_CLASS(fiv_sidebar_parent_class)->realize(widget);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fucking GTK+. With no bookmarks, the revealer takes up space anyway.
 | 
				
			||||||
 | 
						FivSidebar *self = FIV_SIDEBAR(widget);
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_drop_targets_visible(self->places, TRUE, NULL);
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_drop_targets_visible(self->places, FALSE, NULL);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_sidebar_class_init(FivSidebarClass *klass)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GObjectClass *object_class = G_OBJECT_CLASS(klass);
 | 
				
			||||||
 | 
						object_class->dispose = fiv_sidebar_dispose;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidgetClass *widget_class = GTK_WIDGET_CLASS(klass);
 | 
				
			||||||
 | 
						widget_class->realize = fiv_sidebar_realize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// You're giving me no choice, Adwaita.
 | 
				
			||||||
 | 
						// Your style is hardcoded to match against the class' CSS name.
 | 
				
			||||||
 | 
						// And I need to replicate the internal widget structure.
 | 
				
			||||||
 | 
						gtk_widget_class_set_css_name(widget_class, "placessidebar");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): Consider a return value, and using it.
 | 
				
			||||||
 | 
						sidebar_signals[OPEN_LOCATION] =
 | 
				
			||||||
 | 
							g_signal_new("open-location", G_TYPE_FROM_CLASS(klass), 0, 0,
 | 
				
			||||||
 | 
								NULL, NULL, NULL, G_TYPE_NONE,
 | 
				
			||||||
 | 
								2, G_TYPE_FILE, GTK_TYPE_PLACES_OPEN_FLAGS);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					on_rowlabel_query_tooltip(GtkWidget *widget,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gint x, G_GNUC_UNUSED gint y,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gboolean keyboard_tooltip, GtkTooltip *tooltip)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkLabel *label = GTK_LABEL(widget);
 | 
				
			||||||
 | 
						if (!pango_layout_is_ellipsized(gtk_label_get_layout(label)))
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_tooltip_set_text(tooltip, gtk_label_get_text(label));
 | 
				
			||||||
 | 
						return TRUE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					on_breadcrumb_button_release(
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GtkWidget *widget, GdkEventButton *event, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// This also prevents unwanted primary button click handling in GtkListBox.
 | 
				
			||||||
 | 
						if (event->x > gdk_window_get_width(event->window) ||
 | 
				
			||||||
 | 
							event->y > gdk_window_get_height(event->window))
 | 
				
			||||||
 | 
							return TRUE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						guint state = event->state & gtk_accelerator_get_default_mod_mask();
 | 
				
			||||||
 | 
						if (event->button != GDK_BUTTON_MIDDLE || state != 0)
 | 
				
			||||||
 | 
							return FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkListBoxRow *row = GTK_LIST_BOX_ROW(user_data);
 | 
				
			||||||
 | 
						g_signal_emit(g_object_get_qdata(G_OBJECT(row), fiv_sidebar_self_quark()),
 | 
				
			||||||
 | 
							sidebar_signals[OPEN_LOCATION], 0,
 | 
				
			||||||
 | 
							g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark()),
 | 
				
			||||||
 | 
							GTK_PLACES_OPEN_NEW_WINDOW);
 | 
				
			||||||
 | 
						return TRUE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_breadcrumb_gesture_drag_begin(GtkGestureDrag *drag,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gdouble start_x, G_GNUC_UNUSED gdouble start_y,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// Touch screen dragging is how you scroll the parent GtkScrolledWindow,
 | 
				
			||||||
 | 
						// don't steal that gesture. Moreover, touch screen dragging fails
 | 
				
			||||||
 | 
						// in the middle, without ever invoking drag-end.
 | 
				
			||||||
 | 
						GtkGesture *gesture = GTK_GESTURE(drag);
 | 
				
			||||||
 | 
						if (gdk_device_get_source(
 | 
				
			||||||
 | 
								gdk_event_get_source_device(gtk_gesture_get_last_event(
 | 
				
			||||||
 | 
									gesture, gtk_gesture_get_last_updated_sequence(gesture)))) ==
 | 
				
			||||||
 | 
							GDK_SOURCE_TOUCHSCREEN)
 | 
				
			||||||
 | 
							gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_DENIED);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_breadcrumb_gesture_drag_update(GtkGestureDrag *drag,
 | 
				
			||||||
 | 
						gdouble offset_x, gdouble offset_y, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkWidget *widget = GTK_WIDGET(user_data);
 | 
				
			||||||
 | 
						gdouble start_x = 0, start_y = 0;
 | 
				
			||||||
 | 
						if (!gtk_gesture_drag_get_start_point(drag, &start_x, &start_y) ||
 | 
				
			||||||
 | 
							!gtk_drag_check_threshold(widget, start_x, start_y,
 | 
				
			||||||
 | 
								start_x + offset_x, start_y + offset_y))
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkGesture *gesture = GTK_GESTURE(drag);
 | 
				
			||||||
 | 
						gtk_gesture_set_state(gesture, GTK_EVENT_SEQUENCE_CLAIMED);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GdkEvent *event = gdk_event_copy(gtk_gesture_get_last_event(
 | 
				
			||||||
 | 
							gesture, gtk_gesture_get_last_updated_sequence(gesture)));
 | 
				
			||||||
 | 
						GtkTargetList *target_list = gtk_target_list_new(NULL, 0);
 | 
				
			||||||
 | 
						gtk_target_list_add_uri_targets(target_list, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_drag_begin_with_coordinates(
 | 
				
			||||||
 | 
							widget, target_list, GDK_ACTION_LINK, 1, event, start_x, start_y);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_target_list_unref(target_list);
 | 
				
			||||||
 | 
						gdk_event_free(event);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_breadcrumb_drag_data_get(G_GNUC_UNUSED GtkWidget *widget,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GdkDragContext *context, GtkSelectionData *selection_data,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED guint info, G_GNUC_UNUSED guint time_, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GtkListBoxRow *row = GTK_LIST_BOX_ROW(user_data);
 | 
				
			||||||
 | 
						GFile *location =
 | 
				
			||||||
 | 
							g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *uris[] = {g_file_get_uri(location), NULL};
 | 
				
			||||||
 | 
						gtk_selection_data_set_uris(selection_data, uris);
 | 
				
			||||||
 | 
						g_free(*uris);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_breadcrumb_drag_begin(G_GNUC_UNUSED GtkWidget *widget,
 | 
				
			||||||
 | 
						GdkDragContext *context, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gtk_drag_set_icon_name(context, "inode-directory-symbolic", 0, 0);
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_drop_targets_visible(user_data, TRUE, context);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_breadcrumb_drag_end(G_GNUC_UNUSED GtkWidget *widget,
 | 
				
			||||||
 | 
						GdkDragContext *context, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_drop_targets_visible(user_data, FALSE, context);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					on_breadcrumb_button_press(GtkWidget *widget, GdkEventButton *event,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!gdk_event_triggers_context_menu((const GdkEvent *) event))
 | 
				
			||||||
 | 
							return GDK_EVENT_PROPAGATE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *location =
 | 
				
			||||||
 | 
							g_object_get_qdata(G_OBJECT(widget), fiv_sidebar_location_quark());
 | 
				
			||||||
 | 
						gtk_menu_popup_at_pointer(fiv_context_menu_new(widget, location), NULL);
 | 
				
			||||||
 | 
						return GDK_EVENT_STOP;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static gboolean
 | 
				
			||||||
 | 
					on_breadcrumb_popup_menu(GtkWidget *widget, G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFile *location =
 | 
				
			||||||
 | 
							g_object_get_qdata(G_OBJECT(widget), fiv_sidebar_location_quark());
 | 
				
			||||||
 | 
						gtk_menu_popup_at_widget(fiv_context_menu_new(widget, location), widget,
 | 
				
			||||||
 | 
							GDK_GRAVITY_SOUTH_WEST, GDK_GRAVITY_NORTH_WEST, NULL);
 | 
				
			||||||
 | 
						return TRUE;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GtkWidget *
 | 
				
			||||||
 | 
					create_row(FivSidebar *self, GFile *file, const char *icon_name)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GError *error = NULL;
 | 
				
			||||||
 | 
						GFileInfo *info =
 | 
				
			||||||
 | 
							g_file_query_info(file, G_FILE_ATTRIBUTE_STANDARD_DISPLAY_NAME,
 | 
				
			||||||
 | 
								G_FILE_QUERY_INFO_NOFOLLOW_SYMLINKS, NULL, &error);
 | 
				
			||||||
 | 
						if (!info) {
 | 
				
			||||||
 | 
							g_debug("%s", error->message);
 | 
				
			||||||
 | 
							g_error_free(error);
 | 
				
			||||||
 | 
							return NULL;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const char *name = g_file_info_get_display_name(info);
 | 
				
			||||||
 | 
						GtkWidget *rowbox = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *rowimage =
 | 
				
			||||||
 | 
							gtk_image_new_from_icon_name(icon_name, GTK_ICON_SIZE_MENU);
 | 
				
			||||||
 | 
						gtk_style_context_add_class(
 | 
				
			||||||
 | 
							gtk_widget_get_style_context(rowimage), "sidebar-icon");
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(rowbox), rowimage);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *rowlabel = gtk_label_new(name);
 | 
				
			||||||
 | 
						gtk_label_set_ellipsize(GTK_LABEL(rowlabel), PANGO_ELLIPSIZE_END);
 | 
				
			||||||
 | 
						gtk_widget_set_has_tooltip(rowlabel, TRUE);
 | 
				
			||||||
 | 
						g_signal_connect(rowlabel, "query-tooltip",
 | 
				
			||||||
 | 
							G_CALLBACK(on_rowlabel_query_tooltip), NULL);
 | 
				
			||||||
 | 
						gtk_style_context_add_class(
 | 
				
			||||||
 | 
							gtk_widget_get_style_context(rowlabel), "sidebar-label");
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(rowbox), rowlabel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// The revealer is primarily necessary to match Adwaita CSS rules,
 | 
				
			||||||
 | 
						// but it conveniently also has its own GdkWindow to hook events on.
 | 
				
			||||||
 | 
						GtkWidget *revealer = gtk_revealer_new();
 | 
				
			||||||
 | 
						gtk_widget_add_events(
 | 
				
			||||||
 | 
							revealer, GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK);
 | 
				
			||||||
 | 
						gtk_revealer_set_reveal_child(
 | 
				
			||||||
 | 
							GTK_REVEALER(revealer), TRUE);
 | 
				
			||||||
 | 
						gtk_revealer_set_transition_type(
 | 
				
			||||||
 | 
							GTK_REVEALER(revealer), GTK_REVEALER_TRANSITION_TYPE_NONE);
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(revealer), rowbox);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkGesture *drag = gtk_gesture_drag_new(revealer);
 | 
				
			||||||
 | 
						gtk_gesture_single_set_button(GTK_GESTURE_SINGLE(drag), GDK_BUTTON_PRIMARY);
 | 
				
			||||||
 | 
						gtk_event_controller_set_propagation_phase(
 | 
				
			||||||
 | 
							GTK_EVENT_CONTROLLER(drag), GTK_PHASE_BUBBLE);
 | 
				
			||||||
 | 
						g_object_set_qdata_full(G_OBJECT(revealer),
 | 
				
			||||||
 | 
							fiv_sidebar_drag_gesture_quark(),
 | 
				
			||||||
 | 
							drag, (GDestroyNotify) g_object_unref);
 | 
				
			||||||
 | 
						g_signal_connect(drag, "drag-begin",
 | 
				
			||||||
 | 
							G_CALLBACK(on_breadcrumb_gesture_drag_begin), revealer);
 | 
				
			||||||
 | 
						g_signal_connect(drag, "drag-update",
 | 
				
			||||||
 | 
							G_CALLBACK(on_breadcrumb_gesture_drag_update), revealer);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *row = gtk_list_box_row_new();
 | 
				
			||||||
 | 
						g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_location_quark(),
 | 
				
			||||||
 | 
							g_object_ref(file), (GDestroyNotify) g_object_unref);
 | 
				
			||||||
 | 
						g_object_set_qdata_full(G_OBJECT(row), fiv_sidebar_self_quark(),
 | 
				
			||||||
 | 
							g_object_ref(self), (GDestroyNotify) g_object_unref);
 | 
				
			||||||
 | 
						g_signal_connect(row, "button-press-event",
 | 
				
			||||||
 | 
							G_CALLBACK(on_breadcrumb_button_press), NULL);
 | 
				
			||||||
 | 
						g_signal_connect(row, "popup-menu",
 | 
				
			||||||
 | 
							G_CALLBACK(on_breadcrumb_popup_menu), NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Drag signals need to be hooked to a widget with its own GdkWindow.
 | 
				
			||||||
 | 
						g_signal_connect(revealer, "button-release-event",
 | 
				
			||||||
 | 
							G_CALLBACK(on_breadcrumb_button_release), row);
 | 
				
			||||||
 | 
						g_signal_connect(revealer, "drag-data-get",
 | 
				
			||||||
 | 
							G_CALLBACK(on_breadcrumb_drag_data_get), row);
 | 
				
			||||||
 | 
						g_signal_connect(revealer, "drag-begin",
 | 
				
			||||||
 | 
							G_CALLBACK(on_breadcrumb_drag_begin), self->places);
 | 
				
			||||||
 | 
						g_signal_connect(revealer, "drag-end",
 | 
				
			||||||
 | 
							G_CALLBACK(on_breadcrumb_drag_end), self->places);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(row), revealer);
 | 
				
			||||||
 | 
						gtk_widget_show_all(row);
 | 
				
			||||||
 | 
						g_object_unref(info);
 | 
				
			||||||
 | 
						return row;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_update_task(GTask *task, G_GNUC_UNUSED gpointer source_object,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gpointer task_data, G_GNUC_UNUSED GCancellable *cancellable)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_task_return_boolean(task, TRUE);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_update_task_done(GObject *source_object, G_GNUC_UNUSED GAsyncResult *res,
 | 
				
			||||||
 | 
						G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivSidebar *self = FIV_SIDEBAR(source_object);
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_location(
 | 
				
			||||||
 | 
							self->places, fiv_io_model_get_location(self->model));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					reload_directories(FivSidebar *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFile *location = fiv_io_model_get_location(self->model);
 | 
				
			||||||
 | 
						gtk_container_foreach(GTK_CONTAINER(self->listbox),
 | 
				
			||||||
 | 
							(GtkCallback) gtk_widget_destroy, NULL);
 | 
				
			||||||
 | 
						if (!location)
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *iter = g_object_ref(location);
 | 
				
			||||||
 | 
						GtkWidget *row = NULL;
 | 
				
			||||||
 | 
						while (TRUE) {
 | 
				
			||||||
 | 
							GFile *parent = g_file_get_parent(iter);
 | 
				
			||||||
 | 
							g_object_unref(iter);
 | 
				
			||||||
 | 
							if (!(iter = parent))
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if ((row = create_row(self, parent, "go-up-symbolic")))
 | 
				
			||||||
 | 
								gtk_list_box_prepend(GTK_LIST_BOX(self->listbox), row);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Other options are "folder-{visiting,open}-symbolic", though the former
 | 
				
			||||||
 | 
						// is mildly inappropriate (means: open in another window).
 | 
				
			||||||
 | 
						if ((row = create_row(self, location, "circle-filled-symbolic")))
 | 
				
			||||||
 | 
							gtk_container_add(GTK_CONTAINER(self->listbox), row);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gsize len = 0;
 | 
				
			||||||
 | 
						FivIoModelEntry *const *subdirs =
 | 
				
			||||||
 | 
							fiv_io_model_get_subdirs(self->model, &len);
 | 
				
			||||||
 | 
						for (gsize i = 0; i < len; i++) {
 | 
				
			||||||
 | 
							GFile *file = g_file_new_for_uri(subdirs[i]->uri);
 | 
				
			||||||
 | 
							if ((row = create_row(self, file, "go-down-symbolic")))
 | 
				
			||||||
 | 
								gtk_container_add(GTK_CONTAINER(self->listbox), row);
 | 
				
			||||||
 | 
							g_object_unref(file);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_model_subdirectories_changed(G_GNUC_UNUSED FivIoModel *model,
 | 
				
			||||||
 | 
						FivIoModelEntry *old, FivIoModelEntry *new, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivSidebar *self = FIV_SIDEBAR(user_data);
 | 
				
			||||||
 | 
						// TODO(p): Optimize: there's no need to update parent directories.
 | 
				
			||||||
 | 
						if (!old || !new || strcmp(old->uri, new->uri))
 | 
				
			||||||
 | 
							reload_directories(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					update_location(FivSidebar *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						GFile *location = fiv_io_model_get_location(self->model);
 | 
				
			||||||
 | 
						GFile *collection = g_file_new_for_uri(FIV_COLLECTION_SCHEME ":/");
 | 
				
			||||||
 | 
						gtk_places_sidebar_remove_shortcut(self->places, collection);
 | 
				
			||||||
 | 
						if (location && g_file_has_uri_scheme(location, FIV_COLLECTION_SCHEME)) {
 | 
				
			||||||
 | 
							// add_shortcut() asynchronously requests GFileInfo, and only fills in
 | 
				
			||||||
 | 
							// the new row's "uri" data field once that's finished, resulting in
 | 
				
			||||||
 | 
							// the immediate set_location() call below failing to find it.
 | 
				
			||||||
 | 
							gtk_places_sidebar_add_shortcut(self->places, collection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// Queue up a callback using the same mechanism that GFile uses.
 | 
				
			||||||
 | 
							GTask *task = g_task_new(self, NULL, on_update_task_done, NULL);
 | 
				
			||||||
 | 
							g_task_set_name(task, __func__);
 | 
				
			||||||
 | 
							g_task_set_priority(task, G_PRIORITY_LOW);
 | 
				
			||||||
 | 
							g_task_run_in_thread(task, on_update_task);
 | 
				
			||||||
 | 
							g_object_unref(task);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						g_object_unref(collection);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_location(self->places, location);
 | 
				
			||||||
 | 
						reload_directories(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_open_breadcrumb(
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GtkListBox *listbox, GtkListBoxRow *row, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivSidebar *self = FIV_SIDEBAR(user_data);
 | 
				
			||||||
 | 
						GFile *location =
 | 
				
			||||||
 | 
							g_object_get_qdata(G_OBJECT(row), fiv_sidebar_location_quark());
 | 
				
			||||||
 | 
						g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0,
 | 
				
			||||||
 | 
							location, GTK_PLACES_OPEN_NORMAL);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_open_location(G_GNUC_UNUSED GtkPlacesSidebar *sidebar, GFile *location,
 | 
				
			||||||
 | 
						GtkPlacesOpenFlags flags, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivSidebar *self = FIV_SIDEBAR(user_data);
 | 
				
			||||||
 | 
						g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0, location, flags);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Deselect the item in GtkPlacesSidebar, if unsuccessful.
 | 
				
			||||||
 | 
						update_location(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					complete_path(GFile *location, GtkListStore *model)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// TODO(p): Do not enter directories unless followed by '/'.
 | 
				
			||||||
 | 
						// This information has already been stripped from `location`.
 | 
				
			||||||
 | 
						// TODO(p): Try out GFileCompleter.
 | 
				
			||||||
 | 
						GFile *parent = G_FILE_TYPE_DIRECTORY ==
 | 
				
			||||||
 | 
								g_file_query_file_type(location, G_FILE_QUERY_INFO_NONE, NULL)
 | 
				
			||||||
 | 
							? g_object_ref(location)
 | 
				
			||||||
 | 
							: g_file_get_parent(location);
 | 
				
			||||||
 | 
						if (!parent)
 | 
				
			||||||
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFileEnumerator *enumerator = g_file_enumerate_children(parent,
 | 
				
			||||||
 | 
							G_FILE_ATTRIBUTE_STANDARD_NAME
 | 
				
			||||||
 | 
							"," G_FILE_ATTRIBUTE_STANDARD_TYPE
 | 
				
			||||||
 | 
							"," G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN,
 | 
				
			||||||
 | 
							G_FILE_QUERY_INFO_NONE, NULL, NULL);
 | 
				
			||||||
 | 
						if (!enumerator)
 | 
				
			||||||
 | 
							goto fail_enumerator;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						while (TRUE) {
 | 
				
			||||||
 | 
							GFileInfo *info = NULL;
 | 
				
			||||||
 | 
							GFile *child = NULL;
 | 
				
			||||||
 | 
							if (!g_file_enumerator_iterate(enumerator, &info, &child, NULL, NULL) ||
 | 
				
			||||||
 | 
								!info)
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (g_file_info_get_file_type(info) != G_FILE_TYPE_DIRECTORY)
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							if (g_file_info_has_attribute(info,
 | 
				
			||||||
 | 
									G_FILE_ATTRIBUTE_STANDARD_IS_HIDDEN) &&
 | 
				
			||||||
 | 
								g_file_info_get_is_hidden(info))
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							gchar *parse_name = g_file_get_parse_name(child);
 | 
				
			||||||
 | 
							if (!g_str_has_suffix(parse_name, G_DIR_SEPARATOR_S)) {
 | 
				
			||||||
 | 
								gchar *save = parse_name;
 | 
				
			||||||
 | 
								parse_name = g_strdup_printf("%s%c", parse_name, G_DIR_SEPARATOR);
 | 
				
			||||||
 | 
								g_free(save);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							gtk_list_store_insert_with_values(model, NULL, -1, 0, parse_name, -1);
 | 
				
			||||||
 | 
							g_free(parse_name);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_object_unref(enumerator);
 | 
				
			||||||
 | 
					fail_enumerator:
 | 
				
			||||||
 | 
						g_object_unref(parent);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static GFile *
 | 
				
			||||||
 | 
					resolve_location(FivSidebar *self, const char *text)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// Relative paths produce invalid GFile objects with this function.
 | 
				
			||||||
 | 
						// And even if they didn't, we have our own root for them.
 | 
				
			||||||
 | 
						GFile *file = g_file_parse_name(text);
 | 
				
			||||||
 | 
						if (g_file_peek_path(file))
 | 
				
			||||||
 | 
							return file;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Neither branch looks like a particularly good solution.
 | 
				
			||||||
 | 
						// Though in general, false positives are preferred over negatives.
 | 
				
			||||||
 | 
					#if GLIB_CHECK_VERSION(2, 66, 0)
 | 
				
			||||||
 | 
						if (g_uri_is_valid(text, G_URI_FLAGS_PARSE_RELAXED, NULL))
 | 
				
			||||||
 | 
							return file;
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
 | 
						gchar *scheme = g_uri_parse_scheme(text);
 | 
				
			||||||
 | 
						g_free(scheme);
 | 
				
			||||||
 | 
						if (scheme)
 | 
				
			||||||
 | 
							return file;
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GFile *absolute = g_file_get_child_for_display_name(
 | 
				
			||||||
 | 
							fiv_io_model_get_location(self->model), text, NULL);
 | 
				
			||||||
 | 
						if (!absolute)
 | 
				
			||||||
 | 
							return file;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						g_object_unref(file);
 | 
				
			||||||
 | 
						return absolute;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_enter_location_changed(GtkEntry *entry, gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivSidebar *self = FIV_SIDEBAR(user_data);
 | 
				
			||||||
 | 
						const char *text = gtk_entry_get_text(entry);
 | 
				
			||||||
 | 
						GFile *location = resolve_location(self, text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Don't touch the network anywhere around here, URIs are a no-no.
 | 
				
			||||||
 | 
						GtkStyleContext *style = gtk_widget_get_style_context(GTK_WIDGET(entry));
 | 
				
			||||||
 | 
						if (!g_file_peek_path(location) || g_file_query_exists(location, NULL))
 | 
				
			||||||
 | 
							gtk_style_context_remove_class(style, GTK_STYLE_CLASS_WARNING);
 | 
				
			||||||
 | 
						else
 | 
				
			||||||
 | 
							gtk_style_context_add_class(style, GTK_STYLE_CLASS_WARNING);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// XXX: For some reason, this jumps around with longer lists.
 | 
				
			||||||
 | 
						GtkEntryCompletion *completion = gtk_entry_get_completion(entry);
 | 
				
			||||||
 | 
						GtkTreeModel *model = gtk_entry_completion_get_model(completion);
 | 
				
			||||||
 | 
						gtk_list_store_clear(GTK_LIST_STORE(model));
 | 
				
			||||||
 | 
						if (g_file_peek_path(location))
 | 
				
			||||||
 | 
							complete_path(location, GTK_LIST_STORE(model));
 | 
				
			||||||
 | 
						g_object_unref(location);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					on_show_enter_location(
 | 
				
			||||||
 | 
						G_GNUC_UNUSED GtkPlacesSidebar *sidebar, G_GNUC_UNUSED gpointer user_data)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FivSidebar *self = FIV_SIDEBAR(user_data);
 | 
				
			||||||
 | 
						GtkWidget *dialog = gtk_dialog_new_with_buttons("Enter location",
 | 
				
			||||||
 | 
							GTK_WINDOW(gtk_widget_get_toplevel(GTK_WIDGET(self))),
 | 
				
			||||||
 | 
							GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_MODAL |
 | 
				
			||||||
 | 
								GTK_DIALOG_USE_HEADER_BAR,
 | 
				
			||||||
 | 
							"_Open", GTK_RESPONSE_ACCEPT, "_Cancel", GTK_RESPONSE_CANCEL, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkListStore *model = gtk_list_store_new(1, G_TYPE_STRING);
 | 
				
			||||||
 | 
						gtk_tree_sortable_set_sort_column_id(
 | 
				
			||||||
 | 
							GTK_TREE_SORTABLE(model), 0, GTK_SORT_ASCENDING);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkEntryCompletion *completion = gtk_entry_completion_new();
 | 
				
			||||||
 | 
						gtk_entry_completion_set_model(completion, GTK_TREE_MODEL(model));
 | 
				
			||||||
 | 
						gtk_entry_completion_set_text_column(completion, 0);
 | 
				
			||||||
 | 
						// TODO(p): Complete ~ paths so that they start with ~, then we can filter.
 | 
				
			||||||
 | 
						gtk_entry_completion_set_match_func(
 | 
				
			||||||
 | 
							completion, (GtkEntryCompletionMatchFunc) gtk_true, NULL, NULL);
 | 
				
			||||||
 | 
						g_object_unref(model);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *entry = gtk_entry_new();
 | 
				
			||||||
 | 
						gtk_entry_set_completion(GTK_ENTRY(entry), completion);
 | 
				
			||||||
 | 
						gtk_entry_set_activates_default(GTK_ENTRY(entry), TRUE);
 | 
				
			||||||
 | 
						g_signal_connect(entry, "changed",
 | 
				
			||||||
 | 
							G_CALLBACK(on_enter_location_changed), self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Can't have it ellipsized and word-wrapped at the same time.
 | 
				
			||||||
 | 
						GtkWidget *protocols = gtk_label_new("");
 | 
				
			||||||
 | 
						gtk_label_set_ellipsize(GTK_LABEL(protocols), PANGO_ELLIPSIZE_END);
 | 
				
			||||||
 | 
						gtk_label_set_xalign(GTK_LABEL(protocols), 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gchar *protos = g_strjoinv(
 | 
				
			||||||
 | 
							", ", (gchar **) g_vfs_get_supported_uri_schemes(g_vfs_get_default()));
 | 
				
			||||||
 | 
						gchar *label = g_strdup_printf("<i>Available protocols:</i> %s", protos);
 | 
				
			||||||
 | 
						g_free(protos);
 | 
				
			||||||
 | 
						gtk_label_set_markup(GTK_LABEL(protocols), label);
 | 
				
			||||||
 | 
						g_free(label);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						GtkWidget *content = gtk_dialog_get_content_area(GTK_DIALOG(dialog));
 | 
				
			||||||
 | 
						g_object_set(content, "margin", 12, NULL);
 | 
				
			||||||
 | 
						gtk_box_set_spacing(GTK_BOX(content), 6);
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(content), entry);
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(content), protocols);
 | 
				
			||||||
 | 
						gtk_dialog_set_default_response(GTK_DIALOG(dialog), GTK_RESPONSE_ACCEPT);
 | 
				
			||||||
 | 
						gtk_window_set_skip_taskbar_hint(GTK_WINDOW(dialog), TRUE);
 | 
				
			||||||
 | 
						gtk_window_set_default_size(GTK_WINDOW(dialog), 800, -1);
 | 
				
			||||||
 | 
						gtk_window_set_geometry_hints(GTK_WINDOW(dialog), NULL,
 | 
				
			||||||
 | 
							&(GdkGeometry) {.max_width = G_MAXSHORT, .max_height = -1},
 | 
				
			||||||
 | 
							GDK_HINT_MAX_SIZE);
 | 
				
			||||||
 | 
						gtk_widget_show_all(dialog);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (gtk_dialog_run(GTK_DIALOG(dialog)) == GTK_RESPONSE_ACCEPT) {
 | 
				
			||||||
 | 
							const char *text = gtk_entry_get_text(GTK_ENTRY(entry));
 | 
				
			||||||
 | 
							GFile *location = resolve_location(self, text);
 | 
				
			||||||
 | 
							g_signal_emit(self, sidebar_signals[OPEN_LOCATION], 0,
 | 
				
			||||||
 | 
								location, GTK_PLACES_OPEN_NORMAL);
 | 
				
			||||||
 | 
							g_object_unref(location);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						gtk_widget_destroy(dialog);
 | 
				
			||||||
 | 
						g_object_unref(completion);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Deselect the item in GtkPlacesSidebar, if unsuccessful.
 | 
				
			||||||
 | 
						update_location(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					fiv_sidebar_init(FivSidebar *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// TODO(p): Transplant functionality from the shitty GtkPlacesSidebar.
 | 
				
			||||||
 | 
						// We cannot reasonably place any new items within its own GtkListBox,
 | 
				
			||||||
 | 
						// so we need to replicate the style hierarchy to some extent.
 | 
				
			||||||
 | 
						GtkWidget *places = gtk_places_sidebar_new();
 | 
				
			||||||
 | 
						self->places = GTK_PLACES_SIDEBAR(places);
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_show_recent(self->places, FALSE);
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_show_trash(self->places, FALSE);
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_open_flags(self->places,
 | 
				
			||||||
 | 
							GTK_PLACES_OPEN_NORMAL | GTK_PLACES_OPEN_NEW_WINDOW);
 | 
				
			||||||
 | 
						g_signal_connect(self->places, "open-location",
 | 
				
			||||||
 | 
							G_CALLBACK(on_open_location), self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gint minimum_width = -1;
 | 
				
			||||||
 | 
						gtk_widget_get_size_request(places, &minimum_width, NULL);
 | 
				
			||||||
 | 
						gtk_widget_set_size_request(places, minimum_width, -1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_places_sidebar_set_show_enter_location(self->places, TRUE);
 | 
				
			||||||
 | 
						g_signal_connect(self->places, "show-enter-location",
 | 
				
			||||||
 | 
							G_CALLBACK(on_show_enter_location), self);
 | 
				
			||||||
 | 
						gtk_scrolled_window_set_policy(GTK_SCROLLED_WINDOW(self->places),
 | 
				
			||||||
 | 
							GTK_POLICY_NEVER, GTK_POLICY_NEVER);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						self->listbox = gtk_list_box_new();
 | 
				
			||||||
 | 
						gtk_list_box_set_selection_mode(
 | 
				
			||||||
 | 
							GTK_LIST_BOX(self->listbox), GTK_SELECTION_NONE);
 | 
				
			||||||
 | 
						g_signal_connect(self->listbox, "row-activated",
 | 
				
			||||||
 | 
							G_CALLBACK(on_open_breadcrumb), self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Fill up what would otherwise be wasted space,
 | 
				
			||||||
 | 
						// as it is in the examples of Nautilus and Thunar.
 | 
				
			||||||
 | 
						GtkWidget *superbox = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
 | 
				
			||||||
 | 
						gtk_container_add(
 | 
				
			||||||
 | 
							GTK_CONTAINER(superbox), GTK_WIDGET(self->places));
 | 
				
			||||||
 | 
						gtk_container_add(
 | 
				
			||||||
 | 
							GTK_CONTAINER(superbox), gtk_separator_new(GTK_ORIENTATION_VERTICAL));
 | 
				
			||||||
 | 
						gtk_container_add(
 | 
				
			||||||
 | 
							GTK_CONTAINER(superbox), self->listbox);
 | 
				
			||||||
 | 
						gtk_container_add(GTK_CONTAINER(self), superbox);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						gtk_scrolled_window_set_policy(
 | 
				
			||||||
 | 
							GTK_SCROLLED_WINDOW(self), GTK_POLICY_NEVER, GTK_POLICY_AUTOMATIC);
 | 
				
			||||||
 | 
						gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)),
 | 
				
			||||||
 | 
							GTK_STYLE_CLASS_SIDEBAR);
 | 
				
			||||||
 | 
						gtk_style_context_add_class(gtk_widget_get_style_context(GTK_WIDGET(self)),
 | 
				
			||||||
 | 
							"fiv");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Public interface --------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GtkWidget *
 | 
				
			||||||
 | 
					fiv_sidebar_new(FivIoModel *model)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_val_if_fail(FIV_IS_IO_MODEL(model), NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivSidebar *self = g_object_new(FIV_TYPE_SIDEBAR, NULL);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// This doesn't work from the init function.
 | 
				
			||||||
 | 
						GtkWidget *sidebar_port = gtk_bin_get_child(GTK_BIN(self));
 | 
				
			||||||
 | 
						gtk_container_set_focus_hadjustment(GTK_CONTAINER(sidebar_port),
 | 
				
			||||||
 | 
							gtk_scrolled_window_get_hadjustment(GTK_SCROLLED_WINDOW(self)));
 | 
				
			||||||
 | 
						gtk_container_set_focus_vadjustment(GTK_CONTAINER(sidebar_port),
 | 
				
			||||||
 | 
							gtk_scrolled_window_get_vadjustment(GTK_SCROLLED_WINDOW(self)));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						self->model = g_object_ref(model);
 | 
				
			||||||
 | 
						g_signal_connect_swapped(self->model, "reloaded",
 | 
				
			||||||
 | 
							G_CALLBACK(update_location), self);
 | 
				
			||||||
 | 
						g_signal_connect(self->model, "subdirectories-changed",
 | 
				
			||||||
 | 
							G_CALLBACK(on_model_subdirectories_changed), self);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						return GTK_WIDGET(self);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					void
 | 
				
			||||||
 | 
					fiv_sidebar_show_enter_location(FivSidebar *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						g_return_if_fail(FIV_IS_SIDEBAR(self));
 | 
				
			||||||
 | 
						g_signal_emit_by_name(self->places, "show-enter-location");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
// fastiv-browser.h: fast image viewer - filesystem browser widget
 | 
					// fiv-sidebar.h: molesting GtkPlacesSidebar
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
@@ -17,12 +17,12 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#pragma once
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "fiv-io-model.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <gtk/gtk.h>
 | 
					#include <gtk/gtk.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#define FASTIV_TYPE_BROWSER (fastiv_browser_get_type())
 | 
					#define FIV_TYPE_SIDEBAR (fiv_sidebar_get_type())
 | 
				
			||||||
G_DECLARE_FINAL_TYPE(FastivBrowser, fastiv_browser, FASTIV, BROWSER, GtkWidget)
 | 
					G_DECLARE_FINAL_TYPE(FivSidebar, fiv_sidebar, FIV, SIDEBAR, GtkScrolledWindow)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
typedef gboolean (*FastivBrowserFilterCallback) (const char *);
 | 
					GtkWidget *fiv_sidebar_new(FivIoModel *model);
 | 
				
			||||||
 | 
					void fiv_sidebar_show_enter_location(FivSidebar *self);
 | 
				
			||||||
void fastiv_browser_load(
 | 
					 | 
				
			||||||
	FastivBrowser *self, FastivBrowserFilterCallback cb, const char *path);
 | 
					 | 
				
			||||||
							
								
								
									
										1134
									
								
								fiv-thumbnail.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										75
									
								
								fiv-thumbnail.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-thumbnail.h: thumbnail management
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <cairo.h>
 | 
				
			||||||
 | 
					#include <gio/gio.h>
 | 
				
			||||||
 | 
					#include <glib.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Avoid glib-mkenums.
 | 
				
			||||||
 | 
					typedef enum _FivThumbnailSize {
 | 
				
			||||||
 | 
					#define FIV_THUMBNAIL_SIZES(XX) \
 | 
				
			||||||
 | 
						XX(SMALL,  128, "normal") \
 | 
				
			||||||
 | 
						XX(NORMAL, 256, "large") \
 | 
				
			||||||
 | 
						XX(LARGE,  512, "x-large") \
 | 
				
			||||||
 | 
						XX(HUGE,  1024, "xx-large")
 | 
				
			||||||
 | 
					#define XX(name, value, dir) FIV_THUMBNAIL_SIZE_ ## name,
 | 
				
			||||||
 | 
						FIV_THUMBNAIL_SIZES(XX)
 | 
				
			||||||
 | 
					#undef XX
 | 
				
			||||||
 | 
						FIV_THUMBNAIL_SIZE_COUNT,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FIV_THUMBNAIL_SIZE_MIN = 0,
 | 
				
			||||||
 | 
						FIV_THUMBNAIL_SIZE_MAX = FIV_THUMBNAIL_SIZE_COUNT - 1
 | 
				
			||||||
 | 
					} FivThumbnailSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GType fiv_thumbnail_size_get_type(void) G_GNUC_CONST;
 | 
				
			||||||
 | 
					#define FIV_TYPE_THUMBNAIL_SIZE (fiv_thumbnail_size_get_type())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					typedef struct _FivThumbnailSizeInfo {
 | 
				
			||||||
 | 
						int size;                           ///< Nominal size in pixels
 | 
				
			||||||
 | 
						const char *thumbnail_spec_name;    ///< thumbnail-spec directory name
 | 
				
			||||||
 | 
					} FivThumbnailSizeInfo;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum { FIV_THUMBNAIL_WIDE_COEFFICIENT = 2 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extern FivThumbnailSizeInfo fiv_thumbnail_sizes[FIV_THUMBNAIL_SIZE_COUNT];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// If non-NULL, indicates a thumbnail of insufficient quality.
 | 
				
			||||||
 | 
					extern cairo_user_data_key_t fiv_thumbnail_key_lq;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Attempts to extract any low-quality thumbnail from fast targets.
 | 
				
			||||||
 | 
					/// If `max_size` is a valid value, the image will be downscaled as appropriate.
 | 
				
			||||||
 | 
					cairo_surface_t *fiv_thumbnail_extract(
 | 
				
			||||||
 | 
						GFile *target, FivThumbnailSize max_size, GError **error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Generates wide thumbnails of up to the specified size, saves them in cache.
 | 
				
			||||||
 | 
					/// Returns the surface used for the maximum size, or an error.
 | 
				
			||||||
 | 
					cairo_surface_t *fiv_thumbnail_produce(
 | 
				
			||||||
 | 
						GFile *target, FivThumbnailSize max_size, GError **error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Like fiv_thumbnail_produce(), but skips the cache.
 | 
				
			||||||
 | 
					cairo_surface_t *fiv_thumbnail_produce_for_search(
 | 
				
			||||||
 | 
						GFile *target, FivThumbnailSize max_size, GError **error);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Retrieves a thumbnail of the most appropriate quality and resolution
 | 
				
			||||||
 | 
					/// for the target file.
 | 
				
			||||||
 | 
					cairo_surface_t *fiv_thumbnail_lookup(const char *uri,
 | 
				
			||||||
 | 
						gint64 mtime_msec, guint64 filesize, FivThumbnailSize size);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Invalidate the wide thumbnail cache. May write to standard streams.
 | 
				
			||||||
 | 
					void fiv_thumbnail_invalidate(void);
 | 
				
			||||||
							
								
								
									
										8
									
								
								fiv-update-desktop-files.in
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					#!/bin/sh -e
 | 
				
			||||||
 | 
					fiv=${DESTDIR:+$DESTDIR/}'@FIV@'
 | 
				
			||||||
 | 
					desktopdir=${DESTDIR:+$DESTDIR/}'@DESKTOPDIR@'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					types=$("$fiv" --list-supported-media-types | tr '\n' ';')
 | 
				
			||||||
 | 
					for desktop in @DESKTOPS@
 | 
				
			||||||
 | 
					do sed -i "s|^MimeType=.*|MimeType=$types|" "$desktopdir"/"$desktop"
 | 
				
			||||||
 | 
					done
 | 
				
			||||||
							
								
								
									
										2043
									
								
								fiv-view.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						
							
								
								
									
										75
									
								
								fiv-view.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,75 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// fiv-view.h: image viewing widget
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2022, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#pragma once
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <gtk/gtk.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#define FIV_TYPE_VIEW (fiv_view_get_type())
 | 
				
			||||||
 | 
					G_DECLARE_FINAL_TYPE(FivView, fiv_view, FIV, VIEW, GtkWidget)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Try to open the given file, synchronously, to be displayed by the widget.
 | 
				
			||||||
 | 
					/// The current image is cleared on failure.
 | 
				
			||||||
 | 
					gboolean fiv_view_set_uri(FivView *self, const char *uri);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// And this is how you avoid glib-mkenums.
 | 
				
			||||||
 | 
					typedef enum _FivViewCommand {
 | 
				
			||||||
 | 
					#define FIV_VIEW_COMMANDS(XX)                                                  \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_RELOAD,              "reload")                         \
 | 
				
			||||||
 | 
						\
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_ROTATE_LEFT,         "rotate-left")                    \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_MIRROR,              "mirror")                         \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_ROTATE_RIGHT,        "rotate-right")                   \
 | 
				
			||||||
 | 
						\
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_PAGE_FIRST,          "page-first")                     \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_PAGE_PREVIOUS,       "page-previous")                  \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_PAGE_NEXT,           "page-next")                      \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_PAGE_LAST,           "page-last")                      \
 | 
				
			||||||
 | 
						\
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_FRAME_FIRST,         "frame-first")                    \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_FRAME_PREVIOUS,      "frame-previous")                 \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_FRAME_NEXT,          "frame-next")                     \
 | 
				
			||||||
 | 
						/* Going to the end frame makes no sense, wrap around if needed. */        \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_TOGGLE_PLAYBACK,     "toggle-playback")                \
 | 
				
			||||||
 | 
						\
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_TOGGLE_CMS,          "toggle-cms")                     \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_TOGGLE_FILTER,       "toggle-filter")                  \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_TOGGLE_CHECKERBOARD, "toggle-checkerboard")            \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_TOGGLE_ENHANCE,      "toggle-enhance")                 \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_COPY,                "copy")                           \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_PRINT,               "print")                          \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_SAVE_PAGE,           "save-page")                      \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_SAVE_FRAME,          "save-frame")                     \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_INFO,                "info")                           \
 | 
				
			||||||
 | 
						\
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_ZOOM_IN,             "zoom-in")                        \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_ZOOM_OUT,            "zoom-out")                       \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_ZOOM_1,              "zoom-1")                         \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_FIT_WIDTH,           "fit-width")                      \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_FIT_HEIGHT,          "fit-height")                     \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_TOGGLE_SCALE_TO_FIT, "toggle-scale-to-fit")            \
 | 
				
			||||||
 | 
						XX(FIV_VIEW_COMMAND_TOGGLE_FIXATE,       "toggle-fixate")
 | 
				
			||||||
 | 
					#define XX(constant, name) constant,
 | 
				
			||||||
 | 
						FIV_VIEW_COMMANDS(XX)
 | 
				
			||||||
 | 
					#undef XX
 | 
				
			||||||
 | 
					} FivViewCommand;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					GType fiv_view_command_get_type(void) G_GNUC_CONST;
 | 
				
			||||||
 | 
					#define FIV_TYPE_VIEW_COMMAND (fiv_view_command_get_type())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Execute a user action.
 | 
				
			||||||
 | 
					void fiv_view_command(FivView *self, FivViewCommand command);
 | 
				
			||||||
							
								
								
									
										11
									
								
								fiv.desktop
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					[Desktop Entry]
 | 
				
			||||||
 | 
					Type=Application
 | 
				
			||||||
 | 
					Name=fiv
 | 
				
			||||||
 | 
					GenericName=Image Viewer
 | 
				
			||||||
 | 
					X-GNOME-FullName=fiv Image Viewer
 | 
				
			||||||
 | 
					Icon=fiv
 | 
				
			||||||
 | 
					Exec=fiv -- %U
 | 
				
			||||||
 | 
					Terminal=false
 | 
				
			||||||
 | 
					StartupNotify=true
 | 
				
			||||||
 | 
					Categories=Graphics;2DGraphics;Viewer;
 | 
				
			||||||
 | 
					MimeType=image/png;image/bmp;image/gif;image/x-tga;image/jpeg;image/webp;
 | 
				
			||||||
							
								
								
									
										48
									
								
								fiv.gschema.xml
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,48 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="utf-8"?>
 | 
				
			||||||
 | 
					<schemalist>
 | 
				
			||||||
 | 
						<enum id="name.janouch.fiv.thumbnail-size">
 | 
				
			||||||
 | 
							<value nick='Small'  value='0'/>
 | 
				
			||||||
 | 
							<value nick='Normal' value='1'/>
 | 
				
			||||||
 | 
							<value nick='Large'  value='2'/>
 | 
				
			||||||
 | 
							<value nick='Huge'   value='3'/>
 | 
				
			||||||
 | 
						</enum>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<schema path="/name/janouch/fiv/" id="name.janouch.fiv">
 | 
				
			||||||
 | 
							<key name='native-view-window' type='b'>
 | 
				
			||||||
 | 
								<default>true</default>
 | 
				
			||||||
 | 
								<summary>Use a native window for the view</summary>
 | 
				
			||||||
 | 
								<description>
 | 
				
			||||||
 | 
									On X11, using native GdkWindows enables use of 30-bit Visuals
 | 
				
			||||||
 | 
									(that is, 10 bits per channel), at the cost of disabling
 | 
				
			||||||
 | 
									double buffering.
 | 
				
			||||||
 | 
								</description>
 | 
				
			||||||
 | 
							</key>
 | 
				
			||||||
 | 
							<key name='opengl' type='b'>
 | 
				
			||||||
 | 
								<default>false</default>
 | 
				
			||||||
 | 
								<summary>Use experimental OpenGL rendering</summary>
 | 
				
			||||||
 | 
								<description>
 | 
				
			||||||
 | 
									OpenGL within GTK+ is highly problematic--you don't want this.
 | 
				
			||||||
 | 
								</description>
 | 
				
			||||||
 | 
							</key>
 | 
				
			||||||
 | 
							<key name='dark-theme' type='b'>
 | 
				
			||||||
 | 
								<default>false</default>
 | 
				
			||||||
 | 
								<summary>Use a dark theme variant on start-up</summary>
 | 
				
			||||||
 | 
							</key>
 | 
				
			||||||
 | 
							<key name='show-browser-sidebar' type='b'>
 | 
				
			||||||
 | 
								<default>true</default>
 | 
				
			||||||
 | 
								<summary>Show the browser's sidebar</summary>
 | 
				
			||||||
 | 
							</key>
 | 
				
			||||||
 | 
							<key name='show-browser-toolbar' type='b'>
 | 
				
			||||||
 | 
								<default>true</default>
 | 
				
			||||||
 | 
								<summary>Show a toolbar in the browser view</summary>
 | 
				
			||||||
 | 
							</key>
 | 
				
			||||||
 | 
							<key name='show-view-toolbar' type='b'>
 | 
				
			||||||
 | 
								<default>true</default>
 | 
				
			||||||
 | 
								<summary>Show a toolbar in the image view</summary>
 | 
				
			||||||
 | 
							</key>
 | 
				
			||||||
 | 
							<key name='thumbnail-size' enum='name.janouch.fiv.thumbnail-size'>
 | 
				
			||||||
 | 
								<default>'Normal'</default>
 | 
				
			||||||
 | 
								<summary>Thumbnail size to assume on start-up</summary>
 | 
				
			||||||
 | 
							</key>
 | 
				
			||||||
 | 
						</schema>
 | 
				
			||||||
 | 
					</schemalist>
 | 
				
			||||||
							
								
								
									
										11
									
								
								fiv.manifest
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
 | 
				
			||||||
 | 
					<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
 | 
				
			||||||
 | 
						<assemblyIdentity name="fiv" version="1.0.0.0" type="win32" />
 | 
				
			||||||
 | 
						<dependency>
 | 
				
			||||||
 | 
							<dependentAssembly>
 | 
				
			||||||
 | 
								<assemblyIdentity name="Microsoft.Windows.Common-Controls"
 | 
				
			||||||
 | 
									version="6.0.0.0" type="win32" processorArchitecture="*"
 | 
				
			||||||
 | 
									publicKeyToken="6595b64144ccf1df" language="*" />
 | 
				
			||||||
 | 
							</dependentAssembly>
 | 
				
			||||||
 | 
						</dependency>
 | 
				
			||||||
 | 
					</assembly>
 | 
				
			||||||
							
								
								
									
										3
									
								
								fiv.rc
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					#include <windows.h>
 | 
				
			||||||
 | 
					LD_ICON ICON "fiv.ico"
 | 
				
			||||||
 | 
					CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST "fiv.manifest"
 | 
				
			||||||
							
								
								
									
										22
									
								
								fiv.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,22 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8" standalone="no"?>
 | 
				
			||||||
 | 
					<svg version="1.1" width="48" height="48" viewBox="0 0 48 48"
 | 
				
			||||||
 | 
					   xmlns:xlink="http://www.w3.org/1999/xlink"
 | 
				
			||||||
 | 
					   xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
					   xmlns:svg="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					  <defs>
 | 
				
			||||||
 | 
					    <linearGradient id="v" x1="0" y1="1" x2="0" y2="0">
 | 
				
			||||||
 | 
					      <stop stop-color="#f60" offset="0" />
 | 
				
			||||||
 | 
					      <stop stop-color="#fa0" offset="1" />
 | 
				
			||||||
 | 
					    </linearGradient>
 | 
				
			||||||
 | 
					    <filter id="shadow" color-interpolation-filters="sRGB">
 | 
				
			||||||
 | 
					      <feFlood flood-opacity="0.5" flood-color="#000" />
 | 
				
			||||||
 | 
					      <feComposite in2="SourceGraphic" operator="out" />
 | 
				
			||||||
 | 
					      <feGaussianBlur stdDeviation="1.4" />
 | 
				
			||||||
 | 
					      <feOffset dx="1.4" dy="1.4" />
 | 
				
			||||||
 | 
					      <feComposite in2="SourceGraphic" operator="atop" />
 | 
				
			||||||
 | 
					    </filter>
 | 
				
			||||||
 | 
					  </defs>
 | 
				
			||||||
 | 
					  <path fill="url(#v)" d="m 2,7 h 44 l -17,39 h -10 z" />
 | 
				
			||||||
 | 
					  <path d="M 12.5,20.5 h -4 v 5 h 4 v 15 h 9 v -15 h 2 v -5 h -2 v -8 c 0,-4 5,-4 5,0 c 0,6 9,6 9,0 c 0,-12 -23,-12 -23,0 z M 26.5,20.5 h 9 v 20 h -9 z"
 | 
				
			||||||
 | 
					     fill="#fff" stroke="#000" stroke-width="1" filter="url(#shadow)" />
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.0 KiB  | 
							
								
								
									
										71
									
								
								fiv.wxs.in
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,71 @@
 | 
				
			|||||||
 | 
					<?xml version='1.0' encoding='utf-8'?>
 | 
				
			||||||
 | 
					<Wix xmlns='http://schemas.microsoft.com/wix/2006/wi'>
 | 
				
			||||||
 | 
						<?define FullName = "@ProjectName@ @ProjectVersion@" ?>
 | 
				
			||||||
 | 
						<?if $(sys.BUILDARCH) = x64 ?>
 | 
				
			||||||
 | 
							<?define ProgramFilesFolder = "ProgramFiles64Folder" ?>
 | 
				
			||||||
 | 
						<?else?>
 | 
				
			||||||
 | 
							<?define ProgramFilesFolder = "ProgramFilesFolder" ?>
 | 
				
			||||||
 | 
						<?endif?>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						<Product Id='*'
 | 
				
			||||||
 | 
							Name='$(var.FullName)'
 | 
				
			||||||
 | 
							UpgradeCode='a3e64e2d-4310-4c5f-8562-bb0e0b3e0a53'
 | 
				
			||||||
 | 
							Language='1033'
 | 
				
			||||||
 | 
							Codepage='1252'
 | 
				
			||||||
 | 
							Version='@ProjectVersion@'
 | 
				
			||||||
 | 
							Manufacturer='Premysl Eric Janouch'>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<Package Id='*'
 | 
				
			||||||
 | 
								Keywords='Installer,Image,Viewer'
 | 
				
			||||||
 | 
								Description='$(var.FullName) Installer'
 | 
				
			||||||
 | 
								Manufacturer='Premysl Eric Janouch'
 | 
				
			||||||
 | 
								InstallerVersion='200'
 | 
				
			||||||
 | 
								Compressed='yes'
 | 
				
			||||||
 | 
								Languages='1033'
 | 
				
			||||||
 | 
								SummaryCodepage='1252' />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<Media Id='1' Cabinet='data.cab' EmbedCab='yes' />
 | 
				
			||||||
 | 
							<Icon Id='fiv.ico' SourceFile='fiv.ico' />
 | 
				
			||||||
 | 
							<Property Id='ARPPRODUCTICON' Value='fiv.ico' />
 | 
				
			||||||
 | 
							<Property Id='ARPURLINFOABOUT' Value='@ProjectURL@' />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<UIRef Id='WixUI_Minimal' />
 | 
				
			||||||
 | 
							<!-- This isn't supported by msitools, but is necessary for WiX.
 | 
				
			||||||
 | 
							<WixVariable Id='WixUILicenseRtf' Value='License.rtf' />
 | 
				
			||||||
 | 
							-->
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<Directory Id='TARGETDIR' Name='SourceDir'>
 | 
				
			||||||
 | 
								<Directory Id='$(var.ProgramFilesFolder)'>
 | 
				
			||||||
 | 
									<Directory Id='INSTALLDIR' Name='$(var.FullName)' />
 | 
				
			||||||
 | 
								</Directory>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<Directory Id='ProgramMenuFolder'>
 | 
				
			||||||
 | 
									<Directory Id='ProgramMenuDir' Name='$(var.FullName)' />
 | 
				
			||||||
 | 
								</Directory>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								<Directory Id='DesktopFolder' />
 | 
				
			||||||
 | 
							</Directory>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<DirectoryRef Id='ProgramMenuDir'>
 | 
				
			||||||
 | 
								<Component Id='ProgramMenuDir' Guid='*'>
 | 
				
			||||||
 | 
									<Shortcut Id='ProgramsMenuShortcut'
 | 
				
			||||||
 | 
										Name='@ProjectName@'
 | 
				
			||||||
 | 
										Target='[INSTALLDIR]\fiv.exe'
 | 
				
			||||||
 | 
										WorkingDirectory='INSTALLDIR'
 | 
				
			||||||
 | 
										Arguments='"%USERPROFILE%"'
 | 
				
			||||||
 | 
										Icon='fiv.ico' />
 | 
				
			||||||
 | 
									<RemoveFolder Id='ProgramMenuDir' On='uninstall' />
 | 
				
			||||||
 | 
									<RegistryValue Root='HKCU'
 | 
				
			||||||
 | 
										Key='Software\[Manufacturer]\[ProductName]'
 | 
				
			||||||
 | 
										Type='string'
 | 
				
			||||||
 | 
										Value=''
 | 
				
			||||||
 | 
										KeyPath='yes' />
 | 
				
			||||||
 | 
								</Component>
 | 
				
			||||||
 | 
							</DirectoryRef>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							<Feature Id='Complete' Level='1'>
 | 
				
			||||||
 | 
								<ComponentGroupRef Id='CG.fiv' />
 | 
				
			||||||
 | 
								<ComponentRef Id='ProgramMenuDir' />
 | 
				
			||||||
 | 
							</Feature>
 | 
				
			||||||
 | 
						</Product>
 | 
				
			||||||
 | 
					</Wix>
 | 
				
			||||||
							
								
								
									
										386
									
								
								meson.build
									
									
									
									
									
								
							
							
						
						@@ -1,86 +1,384 @@
 | 
				
			|||||||
project('fastiv', 'c',
 | 
					# vim: noet ts=4 sts=4 sw=4:
 | 
				
			||||||
 | 
					project('fiv', 'c',
 | 
				
			||||||
	default_options : ['c_std=gnu99', 'warning_level=2'],
 | 
						default_options : ['c_std=gnu99', 'warning_level=2'],
 | 
				
			||||||
	version : '0.1.0')
 | 
						version : '1.0.0',
 | 
				
			||||||
 | 
						meson_version : '>=0.57')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					cc = meson.get_compiler('c')
 | 
				
			||||||
add_project_arguments(
 | 
					add_project_arguments(
 | 
				
			||||||
	meson.get_compiler('c').get_supported_arguments('-Wno-cast-function-type'),
 | 
						cc.get_supported_arguments('-Wno-cast-function-type'), language : 'c')
 | 
				
			||||||
	language : 'c',
 | 
					 | 
				
			||||||
)
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
if get_option('buildtype').startswith('debug')
 | 
					# This, annoyingly, enables the leak sanitizer by default,
 | 
				
			||||||
	flags = meson.get_compiler('c').get_supported_arguments(
 | 
					# which requires some tuning to not stand in the way.
 | 
				
			||||||
		'-fsanitize=address,undefined')
 | 
					# Use -Db_sanitize=address,undefined and adjust LSAN_OPTIONS yourself.
 | 
				
			||||||
	add_project_arguments(flags, language : ['c'])
 | 
					#if get_option('buildtype').startswith('debug')
 | 
				
			||||||
	add_project_link_arguments(flags, language : ['c'])
 | 
					#	flags = cc.get_supported_arguments('-fsanitize=address,undefined')
 | 
				
			||||||
endif
 | 
					#	add_project_arguments(flags, language : ['c'])
 | 
				
			||||||
 | 
					#	add_project_link_arguments(flags, language : ['c'])
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# TODO(p): Use libraw_r later, when we start parallelizing/preloading.
 | 
					win32 = host_machine.system() == 'windows'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# The likelihood of this being installed is nearly zero. Enable the wrap.
 | 
				
			||||||
 | 
					libjpegqs = dependency('libjpegqs', required : get_option('libjpegqs'),
 | 
				
			||||||
 | 
						allow_fallback : true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					lcms2 = dependency('lcms2', required : get_option('lcms2'))
 | 
				
			||||||
 | 
					# Note that only libraw_r is thread-safe, but we'll just run it out-of process.
 | 
				
			||||||
libraw = dependency('libraw', required : get_option('libraw'))
 | 
					libraw = dependency('libraw', required : get_option('libraw'))
 | 
				
			||||||
 | 
					# This is a direct runtime dependency, but its usage may be disabled for images.
 | 
				
			||||||
librsvg = dependency('librsvg-2.0', required : get_option('librsvg'))
 | 
					librsvg = dependency('librsvg-2.0', required : get_option('librsvg'))
 | 
				
			||||||
xcursor = dependency('xcursor', required : get_option('xcursor'))
 | 
					xcursor = dependency('xcursor', required : get_option('xcursor'))
 | 
				
			||||||
libheif = dependency('libheif', required : get_option('libheif'))
 | 
					libheif = dependency('libheif', required : get_option('libheif'))
 | 
				
			||||||
libtiff = dependency('libtiff-4', required : get_option('libtiff'))
 | 
					libtiff = dependency('libtiff-4', required : get_option('libtiff'))
 | 
				
			||||||
 | 
					# This is a direct dependency of GTK+, but its usage may be disabled for images.
 | 
				
			||||||
gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
 | 
					gdkpixbuf = dependency('gdk-pixbuf-2.0', required : get_option('gdk-pixbuf'))
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
	dependency('gtk+-3.0'),
 | 
						dependency('gtk+-3.0'),
 | 
				
			||||||
	dependency('libturbojpeg'),
 | 
					 | 
				
			||||||
	dependency('spng', version : '>=0.7.0',
 | 
					 | 
				
			||||||
		default_options: 'default_library=static'),
 | 
					 | 
				
			||||||
	dependency('pixman-1'),
 | 
						dependency('pixman-1'),
 | 
				
			||||||
 | 
						dependency('epoxy'),
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						dependency('libjpeg'),
 | 
				
			||||||
 | 
						dependency('libturbojpeg'),
 | 
				
			||||||
 | 
						dependency('libwebp'),
 | 
				
			||||||
 | 
						dependency('libwebpdemux'),
 | 
				
			||||||
 | 
						dependency('libwebpdecoder', required : false),
 | 
				
			||||||
 | 
						dependency('libwebpmux'),
 | 
				
			||||||
 | 
						# Wuffs is included as a submodule.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						lcms2,
 | 
				
			||||||
 | 
						libjpegqs,
 | 
				
			||||||
	libraw,
 | 
						libraw,
 | 
				
			||||||
	librsvg,
 | 
						librsvg,
 | 
				
			||||||
	xcursor,
 | 
						xcursor,
 | 
				
			||||||
	libheif,
 | 
						libheif,
 | 
				
			||||||
	libtiff,
 | 
						libtiff,
 | 
				
			||||||
	gdkpixbuf,
 | 
					
 | 
				
			||||||
	meson.get_compiler('c').find_library('m', required : false),
 | 
						cc.find_library('m', required : false),
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# As of writing, no pkg-config file is produced, and the plugin is not installed
 | 
				
			||||||
 | 
					# by default. The library can be built statically, but it's a bit of a hassle.
 | 
				
			||||||
 | 
					have_lcms2_fast_float = false
 | 
				
			||||||
 | 
					if not get_option('lcms2fastfloat').disabled()
 | 
				
			||||||
 | 
						lcms2ff = dependency('lcms2_fast_float', required : false)
 | 
				
			||||||
 | 
						if not lcms2ff.found()
 | 
				
			||||||
 | 
							lcms2ff = cc.find_library(
 | 
				
			||||||
 | 
								'lcms2_fast_float', required : get_option('lcms2fastfloat'))
 | 
				
			||||||
 | 
							if lcms2ff.found() and not cc.has_header('lcms2_fast_float.h')
 | 
				
			||||||
 | 
								error('lcms2_fast_float.h not found')
 | 
				
			||||||
 | 
							endif
 | 
				
			||||||
 | 
						endif
 | 
				
			||||||
 | 
						if lcms2ff.found()
 | 
				
			||||||
 | 
							dependencies += lcms2ff
 | 
				
			||||||
 | 
							have_lcms2_fast_float = true
 | 
				
			||||||
 | 
						endif
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# As of writing, the API is unstable, and no pkg-config file is produced.
 | 
				
			||||||
 | 
					# Trying to wrap Cargo in Meson is a recipe for pain, so no version pinning.
 | 
				
			||||||
 | 
					have_resvg = false
 | 
				
			||||||
 | 
					if not get_option('resvg').disabled()
 | 
				
			||||||
 | 
						resvg = dependency('resvg', required : false)
 | 
				
			||||||
 | 
						if not resvg.found()
 | 
				
			||||||
 | 
							resvg = cc.find_library('resvg', required : get_option('resvg'))
 | 
				
			||||||
 | 
							if resvg.found() and not cc.has_header('resvg.h')
 | 
				
			||||||
 | 
								error('resvg.h not found')
 | 
				
			||||||
 | 
							endif
 | 
				
			||||||
 | 
						endif
 | 
				
			||||||
 | 
						if resvg.found()
 | 
				
			||||||
 | 
							dependencies += resvg
 | 
				
			||||||
 | 
							have_resvg = true
 | 
				
			||||||
 | 
						endif
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# XXX: https://github.com/mesonbuild/meson/issues/825
 | 
				
			||||||
 | 
					docdir = get_option('datadir') / 'doc' / meson.project_name()
 | 
				
			||||||
 | 
					application_ns = 'name.janouch.'
 | 
				
			||||||
 | 
					application_url = 'https://janouch.name/p/' + meson.project_name()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
conf = configuration_data()
 | 
					conf = configuration_data()
 | 
				
			||||||
conf.set_quoted('PROJECT_NAME', meson.project_name())
 | 
					conf.set_quoted('PROJECT_NAME', meson.project_name())
 | 
				
			||||||
conf.set_quoted('PROJECT_VERSION', meson.project_version())
 | 
					conf.set_quoted('PROJECT_VERSION', '@VCS_TAG@')
 | 
				
			||||||
 | 
					conf.set_quoted('PROJECT_NS', application_ns)
 | 
				
			||||||
 | 
					conf.set_quoted('PROJECT_URL', application_url)
 | 
				
			||||||
 | 
					conf.set_quoted('PROJECT_DOCDIR', get_option('prefix') / docdir)
 | 
				
			||||||
 | 
					if win32
 | 
				
			||||||
 | 
						conf.set_quoted('PROJECT_DOCDIR', docdir)
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					conf.set('HAVE_JPEG_QS', libjpegqs.found())
 | 
				
			||||||
 | 
					conf.set('HAVE_LCMS2', lcms2.found())
 | 
				
			||||||
 | 
					conf.set('HAVE_LCMS2_FAST_FLOAT', have_lcms2_fast_float)
 | 
				
			||||||
conf.set('HAVE_LIBRAW', libraw.found())
 | 
					conf.set('HAVE_LIBRAW', libraw.found())
 | 
				
			||||||
 | 
					conf.set('HAVE_RESVG', have_resvg)
 | 
				
			||||||
conf.set('HAVE_LIBRSVG', librsvg.found())
 | 
					conf.set('HAVE_LIBRSVG', librsvg.found())
 | 
				
			||||||
conf.set('HAVE_XCURSOR', xcursor.found())
 | 
					conf.set('HAVE_XCURSOR', xcursor.found())
 | 
				
			||||||
conf.set('HAVE_LIBHEIF', libheif.found())
 | 
					conf.set('HAVE_LIBHEIF', libheif.found())
 | 
				
			||||||
conf.set('HAVE_LIBTIFF', libtiff.found())
 | 
					conf.set('HAVE_LIBTIFF', libtiff.found())
 | 
				
			||||||
conf.set('HAVE_GDKPIXBUF', gdkpixbuf.found())
 | 
					conf.set('HAVE_GDKPIXBUF', gdkpixbuf.found())
 | 
				
			||||||
configure_file(
 | 
					
 | 
				
			||||||
 | 
					config = vcs_tag(
 | 
				
			||||||
 | 
						command : ['git', 'describe', '--always', '--dirty=+'],
 | 
				
			||||||
 | 
						input : configure_file(output : 'config.h.in', configuration : conf),
 | 
				
			||||||
	output : 'config.h',
 | 
						output : 'config.h',
 | 
				
			||||||
	configuration : conf,
 | 
					 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					rc = []
 | 
				
			||||||
 | 
					if win32
 | 
				
			||||||
 | 
						windows = import('windows')
 | 
				
			||||||
 | 
						rsvg_convert = find_program('rsvg-convert')
 | 
				
			||||||
 | 
						icotool = find_program('icotool')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# Meson is brain-dead and retarded, so these PNGs cannot be installed,
 | 
				
			||||||
 | 
						# only because they must all have the same name when installed.
 | 
				
			||||||
 | 
						# The largest size is mainly for an appropriately sized Windows icon.
 | 
				
			||||||
 | 
						icon_png_list = []
 | 
				
			||||||
 | 
						foreach size : ['16', '32', '48', '256']
 | 
				
			||||||
 | 
							icon_dimensions = size + 'x' + size
 | 
				
			||||||
 | 
							icon_png_list += custom_target(icon_dimensions + ' icon',
 | 
				
			||||||
 | 
								input : 'fiv.svg',
 | 
				
			||||||
 | 
								output : 'fiv.' + icon_dimensions + '.png',
 | 
				
			||||||
 | 
								command : [rsvg_convert, '--output', '@OUTPUT@',
 | 
				
			||||||
 | 
									'--width', size, '--height', size, '@INPUT@'])
 | 
				
			||||||
 | 
						endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						icon_ico = custom_target('fiv.ico',
 | 
				
			||||||
 | 
							output : 'fiv.ico', input : icon_png_list,
 | 
				
			||||||
 | 
							command : [icotool, '-c', '-o', '@OUTPUT@', '@INPUT@'])
 | 
				
			||||||
 | 
						rc += windows.compile_resources('fiv.rc', depends : icon_ico)
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gnome = import('gnome')
 | 
					gnome = import('gnome')
 | 
				
			||||||
resources = gnome.compile_resources('resources',
 | 
					gresources = gnome.compile_resources('resources',
 | 
				
			||||||
	'resources/resources.gresource.xml',
 | 
						'resources/resources.gresource.xml',
 | 
				
			||||||
	source_dir : 'resources',
 | 
						source_dir : 'resources',
 | 
				
			||||||
	c_name : 'resources',
 | 
						c_name : 'resources',
 | 
				
			||||||
)
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
exe = executable('fastiv', 'fastiv.c', 'fastiv-view.c', 'fastiv-io.c',
 | 
					tiff_tables = custom_target('tiff-tables.h',
 | 
				
			||||||
	'fastiv-browser.c', 'fastiv-sidebar.c', 'xdg.c', resources,
 | 
						output : 'tiff-tables.h',
 | 
				
			||||||
 | 
						input : 'tiff-tables.db',
 | 
				
			||||||
 | 
						# Meson 0.56 chokes on files() as well as on a relative path.
 | 
				
			||||||
 | 
						command : [meson.current_source_dir() / 'tiff-tables.awk', '@INPUT@'],
 | 
				
			||||||
 | 
						capture : true,
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					desktops = ['fiv.desktop', 'fiv-browse.desktop']
 | 
				
			||||||
 | 
					iolib = static_library('fiv-io', 'fiv-io.c', 'fiv-io-cmm.c', 'xdg.c',
 | 
				
			||||||
 | 
						tiff_tables, config,
 | 
				
			||||||
 | 
						dependencies : dependencies).extract_all_objects(recursive : true)
 | 
				
			||||||
 | 
					exe = executable('fiv', 'fiv.c', 'fiv-view.c', 'fiv-context-menu.c',
 | 
				
			||||||
 | 
						'fiv-browser.c', 'fiv-sidebar.c', 'fiv-thumbnail.c', 'fiv-collection.c',
 | 
				
			||||||
 | 
						'fiv-io-model.c', gresources, rc, config,
 | 
				
			||||||
 | 
						objects : iolib,
 | 
				
			||||||
 | 
						dependencies : dependencies,
 | 
				
			||||||
	install : true,
 | 
						install : true,
 | 
				
			||||||
	dependencies : [dependencies])
 | 
						win_subsystem : 'windows',
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
if gdkpixbuf.found()
 | 
					desktops += 'fiv-jpegcrop.desktop'
 | 
				
			||||||
	executable('io-benchmark', 'fastiv-io-benchmark.c', 'fastiv-io.c', 'xdg.c',
 | 
					jpegcrop = executable('fiv-jpegcrop', 'fiv-jpegcrop.c', rc, config,
 | 
				
			||||||
 | 
						install : true,
 | 
				
			||||||
 | 
						dependencies : [
 | 
				
			||||||
 | 
							dependency('gtk+-3.0'),
 | 
				
			||||||
 | 
							dependency('libturbojpeg'),
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
						win_subsystem : 'windows',
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if get_option('tools').enabled()
 | 
				
			||||||
 | 
						# libjq has only received a pkg-config file in version 1.7.
 | 
				
			||||||
 | 
						# libjq >= 1.6 is required.
 | 
				
			||||||
 | 
						tools_dependencies = [
 | 
				
			||||||
 | 
							cc.find_library('jq'), dependency('libpng'), dependency('libraw')]
 | 
				
			||||||
 | 
						tools_c_args = cc.get_supported_arguments(
 | 
				
			||||||
 | 
							'-Wno-unused-function', '-Wno-unused-parameter')
 | 
				
			||||||
 | 
						foreach tool : ['info', 'pnginfo', 'rawinfo', 'hotpixels']
 | 
				
			||||||
 | 
							executable(tool, 'tools/' + tool + '.c', tiff_tables,
 | 
				
			||||||
 | 
								dependencies : tools_dependencies,
 | 
				
			||||||
 | 
								c_args: tools_c_args)
 | 
				
			||||||
 | 
						endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if gdkpixbuf.found()
 | 
				
			||||||
 | 
							executable('benchmark-io', 'tools/benchmark-io.c',
 | 
				
			||||||
 | 
								objects : iolib,
 | 
				
			||||||
 | 
								dependencies : [dependencies, gdkpixbuf])
 | 
				
			||||||
 | 
						endif
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copying the files to the build directory makes GSettings find them in devenv.
 | 
				
			||||||
 | 
					gsettings_schemas = ['fiv.gschema.xml']
 | 
				
			||||||
 | 
					foreach schema : gsettings_schemas
 | 
				
			||||||
 | 
						configure_file(
 | 
				
			||||||
 | 
							input : schema,
 | 
				
			||||||
 | 
							output : application_ns + schema,
 | 
				
			||||||
 | 
							copy : true,
 | 
				
			||||||
 | 
							install: true,
 | 
				
			||||||
 | 
							install_dir : get_option('datadir') / 'glib-2.0' / 'schemas')
 | 
				
			||||||
 | 
					endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# For the purposes of development: make the program find its GSettings schemas.
 | 
				
			||||||
 | 
					gnome.compile_schemas(depend_files : files(gsettings_schemas))
 | 
				
			||||||
 | 
					gnome.post_install(glib_compile_schemas : true, gtk_update_icon_cache : true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Meson is broken on Windows and removes the backslashes, so this ends up empty.
 | 
				
			||||||
 | 
					symbolics = run_command(find_program('sed', required : false, disabler : true),
 | 
				
			||||||
 | 
						'-n', 's@.*>\\([^<>]*[.]svg\\)<.*@resources/\\1@p',
 | 
				
			||||||
 | 
						configure_file(
 | 
				
			||||||
 | 
							input : 'resources/resources.gresource.xml',
 | 
				
			||||||
 | 
							output : 'resources.gresource.xml.stamp',
 | 
				
			||||||
 | 
							copy : true,
 | 
				
			||||||
 | 
						), capture : true, check : true).stdout().strip()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Validate various files, if there are tools around to do it.
 | 
				
			||||||
 | 
					xmls = ['fiv.svg', 'fiv.manifest', 'resources/resources.gresource.xml'] + \
 | 
				
			||||||
 | 
						gsettings_schemas
 | 
				
			||||||
 | 
					if symbolics != ''
 | 
				
			||||||
 | 
						xmls += symbolics.split('\n')
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					xmlwf = find_program('xmlwf', required : false, disabler : true)
 | 
				
			||||||
 | 
					xmllint = find_program('xmllint', required : false, disabler : true)
 | 
				
			||||||
 | 
					foreach xml : xmls
 | 
				
			||||||
 | 
						test('xmlwf ' + xml, xmlwf, args : files(xml))
 | 
				
			||||||
 | 
						test('xmllint ' + xml, xmllint, args : ['--noout', files(xml)])
 | 
				
			||||||
 | 
					endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dfv = find_program('desktop-file-validate', required : false, disabler : true)
 | 
				
			||||||
 | 
					foreach desktop : desktops
 | 
				
			||||||
 | 
						test(desktop, dfv, args : files(desktop))
 | 
				
			||||||
 | 
					endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Finish the installation.
 | 
				
			||||||
 | 
					install_data('fiv.svg',
 | 
				
			||||||
 | 
						install_dir : get_option('datadir') / 'icons/hicolor/scalable/apps')
 | 
				
			||||||
 | 
					install_subdir('docs',
 | 
				
			||||||
 | 
						install_dir : docdir, strip_directory : true)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if not win32
 | 
				
			||||||
 | 
						asciidoctor = find_program('asciidoctor', required : false)
 | 
				
			||||||
 | 
						a2x = find_program('a2x', required : false)
 | 
				
			||||||
 | 
						if not asciidoctor.found() and not a2x.found()
 | 
				
			||||||
 | 
							warning('Neither asciidoctor nor a2x were found, ' +
 | 
				
			||||||
 | 
								'falling back to a substandard manual page generator')
 | 
				
			||||||
 | 
						endif
 | 
				
			||||||
 | 
						foreach page : [meson.project_name()]
 | 
				
			||||||
 | 
							man_capture = false
 | 
				
			||||||
 | 
							if asciidoctor.found()
 | 
				
			||||||
 | 
								command = [asciidoctor, '-b', 'manpage',
 | 
				
			||||||
 | 
									'-a', 'release-version=' + meson.project_version(),
 | 
				
			||||||
 | 
									'-o', '@OUTPUT@', '@INPUT@']
 | 
				
			||||||
 | 
							elif a2x.found()
 | 
				
			||||||
 | 
								command = [a2x, '--doctype', 'manpage', '--format', 'manpage',
 | 
				
			||||||
 | 
									'-a', 'release-version=' + meson.project_version(),
 | 
				
			||||||
 | 
									'-D', '@OUTDIR@', '@INPUT@']
 | 
				
			||||||
 | 
							else
 | 
				
			||||||
 | 
								command = ['env', 'LC_ALL=C',
 | 
				
			||||||
 | 
									'asciidoc-release-version=' + meson.project_version(),
 | 
				
			||||||
 | 
									'awk', '-f', files('submodules/liberty/tools/asciiman.awk'),
 | 
				
			||||||
 | 
									'@INPUT@']
 | 
				
			||||||
 | 
								man_capture = true
 | 
				
			||||||
 | 
							endif
 | 
				
			||||||
 | 
							custom_target('manpage for ' + page,
 | 
				
			||||||
 | 
								input : 'docs' / page + '.adoc',
 | 
				
			||||||
 | 
								output : page + '.1',
 | 
				
			||||||
 | 
								capture : man_capture,
 | 
				
			||||||
 | 
								command : command,
 | 
				
			||||||
 | 
								install : true,
 | 
				
			||||||
 | 
								install_dir : get_option('mandir') / 'man1')
 | 
				
			||||||
 | 
						endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						foreach desktop : desktops
 | 
				
			||||||
 | 
							install_data(desktop,
 | 
				
			||||||
 | 
								rename : application_ns + desktop,
 | 
				
			||||||
 | 
								install_dir : get_option('datadir') / 'applications')
 | 
				
			||||||
 | 
						endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# TODO(p): Consider moving this to /usr/share or /usr/lib.
 | 
				
			||||||
 | 
						install_data('fiv-reverse-search',
 | 
				
			||||||
 | 
							install_dir : get_option('bindir'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# As usual, handling generated files in Meson is a fucking pain.
 | 
				
			||||||
 | 
						updatable_desktops = [application_ns + 'fiv.desktop']
 | 
				
			||||||
 | 
						foreach name, uri : {
 | 
				
			||||||
 | 
							'Google' : 'https://lens.google.com/uploadbyurl?url=',
 | 
				
			||||||
 | 
							'Bing' : 'https://www.bing.com/images/searchbyimage?cbir=sbi&imgurl=',
 | 
				
			||||||
 | 
							'Yandex' : 'https://yandex.com/images/search?rpt=imageview&url=',
 | 
				
			||||||
 | 
							'TinEye' : 'https://tineye.com/search?url=',
 | 
				
			||||||
 | 
							'SauceNAO' : 'https://saucenao.com/search.php?url=',
 | 
				
			||||||
 | 
							'IQDB' : 'https://iqdb.org/?url=',
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
							desktop = 'fiv-reverse-search-' + name.to_lower() + '.desktop'
 | 
				
			||||||
 | 
							updatable_desktops += application_ns + desktop
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							test(desktop, dfv, args : configure_file(
 | 
				
			||||||
 | 
								input : 'fiv-reverse-search.desktop.in',
 | 
				
			||||||
 | 
								output : application_ns + desktop,
 | 
				
			||||||
 | 
								configuration : {'NAME' : name, 'URL' : uri},
 | 
				
			||||||
 | 
								install : true,
 | 
				
			||||||
 | 
								install_dir : get_option('datadir') / 'applications',
 | 
				
			||||||
 | 
							))
 | 
				
			||||||
 | 
						endforeach
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# With gdk-pixbuf, fiv.desktop depends on currently installed modules;
 | 
				
			||||||
 | 
						# the package manager needs to be told to run this script as necessary.
 | 
				
			||||||
 | 
						dynamic_desktops = gdkpixbuf.found()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						updater = configure_file(
 | 
				
			||||||
 | 
							input : 'fiv-update-desktop-files.in',
 | 
				
			||||||
 | 
							output : 'fiv-update-desktop-files',
 | 
				
			||||||
 | 
							configuration : {
 | 
				
			||||||
 | 
								'FIV' : get_option('prefix') / get_option('bindir') / exe.name(),
 | 
				
			||||||
 | 
								'DESKTOPDIR' : get_option('prefix') /
 | 
				
			||||||
 | 
									get_option('datadir') / 'applications',
 | 
				
			||||||
 | 
								'DESKTOPS' : ' \\\n\t'.join(updatable_desktops),
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							install : dynamic_desktops,
 | 
				
			||||||
 | 
							install_dir : get_option('bindir'))
 | 
				
			||||||
 | 
						if not meson.is_cross_build()
 | 
				
			||||||
 | 
							meson.add_install_script(updater, skip_if_destdir : dynamic_desktops)
 | 
				
			||||||
 | 
						endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# Quick and dirty package generation, lacking dependencies.
 | 
				
			||||||
 | 
						packaging = configuration_data({
 | 
				
			||||||
 | 
							'name' : meson.project_name(),
 | 
				
			||||||
 | 
							'version' : meson.project_version(),
 | 
				
			||||||
 | 
							'summary' : 'Image viewer',
 | 
				
			||||||
 | 
							'author' : 'Přemysl Eric Janouch',
 | 
				
			||||||
 | 
						})
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						subdir('submodules/liberty/meson/packaging')
 | 
				
			||||||
 | 
					elif meson.is_cross_build()
 | 
				
			||||||
 | 
						# Note that even compiling /from within MSYS2/ can still be a cross-build.
 | 
				
			||||||
 | 
						msys2_root = meson.get_external_property('msys2_root')
 | 
				
			||||||
 | 
						meson.add_install_script('msys2-install.sh', msys2_root)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						wxs = configure_file(
 | 
				
			||||||
 | 
							input : 'fiv.wxs.in',
 | 
				
			||||||
 | 
							output : 'fiv.wxs',
 | 
				
			||||||
 | 
							configuration : configuration_data({
 | 
				
			||||||
 | 
								'ProjectName' : meson.project_name(),
 | 
				
			||||||
 | 
								'ProjectVersion' : meson.project_version(),
 | 
				
			||||||
 | 
								'ProjectURL' : application_url,
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
						)
 | 
				
			||||||
 | 
						msi = meson.project_name() + '-' + meson.project_version() + \
 | 
				
			||||||
 | 
							'-' + host_machine.cpu() + '.msi'
 | 
				
			||||||
 | 
						custom_target('package',
 | 
				
			||||||
 | 
							output : msi,
 | 
				
			||||||
 | 
							command : [meson.current_source_dir() / 'msys2-package.sh',
 | 
				
			||||||
 | 
								host_machine.cpu(), msi, wxs],
 | 
				
			||||||
 | 
							env : ['MESON_BUILD_ROOT=' + meson.current_build_dir(),
 | 
				
			||||||
 | 
								'MESON_SOURCE_ROOT=' + meson.current_source_dir()],
 | 
				
			||||||
 | 
							console : true,
 | 
				
			||||||
 | 
							build_always_stale : true,
 | 
				
			||||||
		build_by_default : false,
 | 
							build_by_default : false,
 | 
				
			||||||
		dependencies : [dependencies, gdkpixbuf])
 | 
						)
 | 
				
			||||||
endif
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
install_data('fastiv.desktop',
 | 
						# This is the minimum to run targets from msys2-configure.sh builds.
 | 
				
			||||||
	install_dir : get_option('datadir') + '/applications')
 | 
						meson.add_devenv({
 | 
				
			||||||
install_data('fastiv-browse.desktop',
 | 
							'WINEPATH' : msys2_root / 'bin',
 | 
				
			||||||
	install_dir : get_option('datadir') + '/applications')
 | 
							'XDG_DATA_DIRS' : msys2_root / 'share',
 | 
				
			||||||
install_data('fastiv.svg',
 | 
						})
 | 
				
			||||||
	install_dir : get_option('datadir') + '/icons/hicolor/scalable/apps')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
# TODO(p): Replace this with custom_target().
 | 
					 | 
				
			||||||
if not meson.is_cross_build()
 | 
					 | 
				
			||||||
	meson.add_install_script(
 | 
					 | 
				
			||||||
		'sh', '-c', '''sed -i "/^MimeType=/{c \\
 | 
					 | 
				
			||||||
MimeType=$($1 --list-supported-media-types | tr "\\012" ";")
 | 
					 | 
				
			||||||
}" "$MESON_INSTALL_DESTDIR_PREFIX/$2"''',
 | 
					 | 
				
			||||||
		'sh',
 | 
					 | 
				
			||||||
		exe.full_path(),
 | 
					 | 
				
			||||||
		get_option('datadir') + '/applications/fastiv.desktop')
 | 
					 | 
				
			||||||
endif
 | 
					endif
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,16 @@
 | 
				
			|||||||
 | 
					option('tools', type : 'feature', value : 'disabled',
 | 
				
			||||||
 | 
						description : 'Build a few extra file inspection tools')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					option('lcms2', type : 'feature', value : 'auto',
 | 
				
			||||||
 | 
						description : 'Build with Little CMS colour management')
 | 
				
			||||||
 | 
					option('lcms2fastfloat', type : 'feature', value : 'auto',
 | 
				
			||||||
 | 
						description : 'Build with Little CMS fast float plugin support')
 | 
				
			||||||
 | 
					option('libjpegqs', type : 'feature', value : 'auto',
 | 
				
			||||||
 | 
						description : 'Build with JPEG Quant Smooth integration')
 | 
				
			||||||
option('libraw', type : 'feature', value : 'auto',
 | 
					option('libraw', type : 'feature', value : 'auto',
 | 
				
			||||||
    description : 'Build with raw photo support, requires LibRaw')
 | 
						description : 'Build with raw photo support, requires LibRaw')
 | 
				
			||||||
 | 
					option('resvg', type : 'feature', value : 'disabled',
 | 
				
			||||||
 | 
						description : 'Build with SVG support via resvg (pre-1.0 unstable API)')
 | 
				
			||||||
option('librsvg', type : 'feature', value : 'auto',
 | 
					option('librsvg', type : 'feature', value : 'auto',
 | 
				
			||||||
	description : 'Build with SVG support, requires librsvg')
 | 
						description : 'Build with SVG support, requires librsvg')
 | 
				
			||||||
option('xcursor', type : 'feature', value : 'auto',
 | 
					option('xcursor', type : 'feature', value : 'auto',
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										149
									
								
								msys2-configure.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,149 @@
 | 
				
			|||||||
 | 
					#!/bin/sh -e
 | 
				
			||||||
 | 
					# msys2-configure.sh: set up an MSYS2-based Meson build (x86-64 by default)
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Dependencies: AWK, sed, coreutils, cURL, bsdtar (libarchive),
 | 
				
			||||||
 | 
					# wine64, Meson, mingw-w64-binutils, mingw-w64-gcc, pkg-config
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# We support running directly from within MSYS2 on Windows,
 | 
				
			||||||
 | 
					# albeit while still downloading a complete copy of runtime depencies.
 | 
				
			||||||
 | 
					pkg=${MINGW_PACKAGE_PREFIX:-mingw-w64-x86_64}
 | 
				
			||||||
 | 
					prefix=${MSYSTEM_PREFIX:-/mingw64}
 | 
				
			||||||
 | 
					repo=https://repo.msys2.org/mingw$prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					chost=${MSYSTEM_CHOST:-x86_64-w64-mingw32}
 | 
				
			||||||
 | 
					carch=${MSYSTEM_CARCH:-x86_64}
 | 
				
			||||||
 | 
					[ "$carch" = "i686" ] && carch=x86
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if [ -n "$MSYSTEM" ]
 | 
				
			||||||
 | 
					then
 | 
				
			||||||
 | 
						wine64() { "$@"; }
 | 
				
			||||||
 | 
						awk() { command awk -v RS='\r?\n' "$@"; }
 | 
				
			||||||
 | 
						pacman -S --needed libarchive $pkg-ca-certificates $pkg-gcc $pkg-icoutils \
 | 
				
			||||||
 | 
							$pkg-librsvg $pkg-meson $pkg-msitools $pkg-pkgconf
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					status() {
 | 
				
			||||||
 | 
						echo "$(tput bold)-- $*$(tput sgr0)"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					dbsync() {
 | 
				
			||||||
 | 
						status Fetching repository DB
 | 
				
			||||||
 | 
						[ -f db.tsv ] || curl -# "$repo$prefix.db" | bsdtar -xOf- | awk '
 | 
				
			||||||
 | 
							function flush() { print f["%NAME%"] f["%FILENAME%"] f["%DEPENDS%"] }
 | 
				
			||||||
 | 
							NR > 1 && $0 == "%FILENAME%" { flush(); for (i in f) delete f[i] }
 | 
				
			||||||
 | 
							!/^[^%]/ { field = $0; next } { f[field] = f[field] $0 "\t" }
 | 
				
			||||||
 | 
							field == "%SHA256SUM%" { path = "*packages/" f["%FILENAME%"]
 | 
				
			||||||
 | 
								sub(/\t$/, "", path); print $0, path > "db.sums" } END { flush() }
 | 
				
			||||||
 | 
						' > db.tsv
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					fetch() {
 | 
				
			||||||
 | 
						status Resolving "$@"
 | 
				
			||||||
 | 
						mkdir -p packages
 | 
				
			||||||
 | 
						awk -F'\t' 'function get(name,    i, a) {
 | 
				
			||||||
 | 
							if (visited[name]++ || !(name in filenames)) return
 | 
				
			||||||
 | 
							print filenames[name]; split(deps[name], a); for (i in a) get(a[i])
 | 
				
			||||||
 | 
						} BEGIN { while ((getline < "db.tsv") > 0) {
 | 
				
			||||||
 | 
							filenames[$1] = $2; deps[$1] = ""; for (i = 3; i <= NF; i++) {
 | 
				
			||||||
 | 
								gsub(/[<=>].*/, "", $i); deps[$1] = deps[$1] $i FS }
 | 
				
			||||||
 | 
						} for (i = 0; i < ARGC; i++) get(ARGV[i]) }' "$@" | tee db.want | \
 | 
				
			||||||
 | 
						while IFS= read -r name
 | 
				
			||||||
 | 
						do
 | 
				
			||||||
 | 
							status Fetching "$name"
 | 
				
			||||||
 | 
							[ -f "packages/$name" ] || curl -#o "packages/$name" "$repo/$name"
 | 
				
			||||||
 | 
						done
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						version=$(curl -# https://exiftool.org/ver.txt)
 | 
				
			||||||
 | 
						name=exiftool-$version.tar.gz remotename=Image-ExifTool-$version.tar.gz
 | 
				
			||||||
 | 
						status Fetching "$remotename"
 | 
				
			||||||
 | 
						[ -f "$name" ] || curl -#o "$name" "https://exiftool.org/$remotename"
 | 
				
			||||||
 | 
						ln -sf "$name" exiftool.tar.gz
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					verify() {
 | 
				
			||||||
 | 
						status Verifying checksums
 | 
				
			||||||
 | 
						sha256sum --ignore-missing --quiet -c db.sums
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extract() {
 | 
				
			||||||
 | 
						status Extracting packages
 | 
				
			||||||
 | 
						for subdir in *
 | 
				
			||||||
 | 
						do [ -d "$subdir" -a "$subdir" != packages ] && rm -rf -- "$subdir"
 | 
				
			||||||
 | 
						done
 | 
				
			||||||
 | 
						while IFS= read -r name
 | 
				
			||||||
 | 
						do bsdtar -xf "packages/$name" --strip-components 1 \
 | 
				
			||||||
 | 
							--exclude '*/share/man' --exclude '*/share/doc'
 | 
				
			||||||
 | 
						done < db.want
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# Don't require Perl, which may not exist anymore on i686:
 | 
				
			||||||
 | 
						# https://github.com/msys2/MINGW-packages/pull/20085
 | 
				
			||||||
 | 
						if [ -d lib/perl5 ]
 | 
				
			||||||
 | 
						then
 | 
				
			||||||
 | 
							bsdtar -xf exiftool.tar.gz
 | 
				
			||||||
 | 
							mv Image-ExifTool-*/exiftool bin
 | 
				
			||||||
 | 
							mv Image-ExifTool-*/lib/* lib/perl5/site_perl
 | 
				
			||||||
 | 
							rm -rf Image-ExifTool-*
 | 
				
			||||||
 | 
						fi
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					configure() {
 | 
				
			||||||
 | 
						# Don't let GLib programs inherit wrong paths from the environment.
 | 
				
			||||||
 | 
						export XDG_DATA_DIRS=$msys2_root/share
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						status Configuring packages
 | 
				
			||||||
 | 
						wine64 bin/update-mime-database.exe share/mime
 | 
				
			||||||
 | 
						wine64 bin/glib-compile-schemas.exe share/glib-2.0/schemas
 | 
				
			||||||
 | 
						wine64 bin/gdk-pixbuf-query-loaders.exe \
 | 
				
			||||||
 | 
							> lib/gdk-pixbuf-2.0/2.10.0/loaders.cache
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					setup() {
 | 
				
			||||||
 | 
						status Setting up Meson
 | 
				
			||||||
 | 
						wrap=true pclibdir=$msys2_root/share/pkgconfig:$msys2_root/lib/pkgconfig
 | 
				
			||||||
 | 
						[ -n "$MSYSTEM" ] && \
 | 
				
			||||||
 | 
							wrap=false pclibdir="$(pwd -W)/share/pkgconfig;$(pwd -W)/lib/pkgconfig"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						cat >"$toolchain" <<-EOF
 | 
				
			||||||
 | 
						[binaries]
 | 
				
			||||||
 | 
						c = '$chost-gcc'
 | 
				
			||||||
 | 
						cpp = '$chost-g++'
 | 
				
			||||||
 | 
						ar = '$chost-gcc-ar'
 | 
				
			||||||
 | 
						ranlib = '$chost-gcc-ranlib'
 | 
				
			||||||
 | 
						strip = '$chost-strip'
 | 
				
			||||||
 | 
						windres = '$chost-windres'
 | 
				
			||||||
 | 
						pkgconfig = 'pkg-config'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[properties]
 | 
				
			||||||
 | 
						sys_root = '$builddir'
 | 
				
			||||||
 | 
						msys2_root = '$msys2_root'
 | 
				
			||||||
 | 
						pkg_config_libdir = '$pclibdir'
 | 
				
			||||||
 | 
						needs_exe_wrapper = $wrap
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						[host_machine]
 | 
				
			||||||
 | 
						system = 'windows'
 | 
				
			||||||
 | 
						cpu_family = '$carch'
 | 
				
			||||||
 | 
						cpu = '$carch'
 | 
				
			||||||
 | 
						endian = 'little'
 | 
				
			||||||
 | 
						EOF
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						meson setup --buildtype=debugoptimized --prefix=/ \
 | 
				
			||||||
 | 
							--bindir . --libdir . --cross-file="$toolchain" "$builddir" "$sourcedir"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					sourcedir=$(realpath "${2:-$(dirname "$0")}")
 | 
				
			||||||
 | 
					builddir=$(realpath "${1:-builddir}")
 | 
				
			||||||
 | 
					toolchain=$builddir/msys2-cross-toolchain.meson
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This directory name matches the prefix in .pc files, so we don't need to
 | 
				
			||||||
 | 
					# modify them (pkgconf has --prefix-variable, but Meson can't pass that option).
 | 
				
			||||||
 | 
					msys2_root=$builddir$prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mkdir -p "$msys2_root"
 | 
				
			||||||
 | 
					cd "$msys2_root"
 | 
				
			||||||
 | 
					dbsync
 | 
				
			||||||
 | 
					fetch $pkg-gtk3 $pkg-lcms2 $pkg-libraw $pkg-libheif $pkg-libjxl $pkg-perl \
 | 
				
			||||||
 | 
						$pkg-perl-win32-api $pkg-libwinpthread-git # Because we don't do "provides"?
 | 
				
			||||||
 | 
					verify
 | 
				
			||||||
 | 
					extract
 | 
				
			||||||
 | 
					configure
 | 
				
			||||||
 | 
					setup
 | 
				
			||||||
							
								
								
									
										77
									
								
								msys2-install.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,77 @@
 | 
				
			|||||||
 | 
					#!/bin/sh -e
 | 
				
			||||||
 | 
					export LC_ALL=C
 | 
				
			||||||
 | 
					cd "$MESON_INSTALL_DESTDIR_PREFIX"
 | 
				
			||||||
 | 
					msys2_root=$1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Support running directly from within MSYS2 on Windows.
 | 
				
			||||||
 | 
					if [ -n "$MSYSTEM" ]
 | 
				
			||||||
 | 
					then
 | 
				
			||||||
 | 
						wine64() { "$@"; }
 | 
				
			||||||
 | 
						awk() { command awk -v RS='\r?\n' "$@"; }
 | 
				
			||||||
 | 
					fi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Copy binaries we directly or indirectly depend on.
 | 
				
			||||||
 | 
					cp -p "$msys2_root"/bin/*.dll .
 | 
				
			||||||
 | 
					cp -p "$msys2_root"/bin/wperl.exe . || :
 | 
				
			||||||
 | 
					cp -p "$msys2_root"/bin/exiftool . || :
 | 
				
			||||||
 | 
					# The console helper is only useful for debug builds.
 | 
				
			||||||
 | 
					cp -p "$msys2_root"/bin/gspawn-*-helper*.exe .
 | 
				
			||||||
 | 
					cp -pR "$msys2_root"/etc/ .
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					mkdir -p lib
 | 
				
			||||||
 | 
					cp -pR "$msys2_root"/lib/gdk-pixbuf-2.0/ lib
 | 
				
			||||||
 | 
					cp -pR "$msys2_root"/lib/perl5/ lib || :
 | 
				
			||||||
 | 
					mkdir -p share/glib-2.0/schemas
 | 
				
			||||||
 | 
					cp -pR "$msys2_root"/share/glib-2.0/schemas/*.Settings.* share/glib-2.0/schemas
 | 
				
			||||||
 | 
					mkdir -p share/icons
 | 
				
			||||||
 | 
					cp -pR "$msys2_root"/share/icons/Adwaita/ share/icons
 | 
				
			||||||
 | 
					mkdir -p share/icons/hicolor
 | 
				
			||||||
 | 
					cp -p "$msys2_root"/share/icons/hicolor/index.theme share/icons/hicolor
 | 
				
			||||||
 | 
					mkdir -p share/mime
 | 
				
			||||||
 | 
					# GIO doesn't use the database on Windows, this subset is for us.
 | 
				
			||||||
 | 
					find "$msys2_root"/share/mime/ -maxdepth 1 -type f -exec cp -p {} share/mime \;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Remove unreferenced libraries.
 | 
				
			||||||
 | 
					find lib -name '*.a' -exec rm -- {} +
 | 
				
			||||||
 | 
					awk 'function whitelist(binary) {
 | 
				
			||||||
 | 
						if (seen[binary]++)
 | 
				
			||||||
 | 
							return
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						delete orphans[binary]
 | 
				
			||||||
 | 
						while (("strings -a \"" binary "\" 2>/dev/null" | getline string) > 0)
 | 
				
			||||||
 | 
							if (match(string, /[-.+_a-zA-Z0-9]+[.][Dd][Ll][Ll]$/))
 | 
				
			||||||
 | 
								whitelist("./" substr(string, RSTART, RLENGTH))
 | 
				
			||||||
 | 
					} BEGIN {
 | 
				
			||||||
 | 
						while (("find . -type f -path \"./*.[Dd][Ll][Ll]\"" | getline) > 0)
 | 
				
			||||||
 | 
							orphans[$0]++
 | 
				
			||||||
 | 
						while (("find . -type f -path \"./*.[Ee][Xx][Ee]\"" | getline) > 0)
 | 
				
			||||||
 | 
							whitelist($0)
 | 
				
			||||||
 | 
						while (("find ./lib -type f -path \"./*.[Dd][Ll][Ll]\"" | getline) > 0)
 | 
				
			||||||
 | 
							whitelist($0)
 | 
				
			||||||
 | 
						for (library in orphans)
 | 
				
			||||||
 | 
							print library
 | 
				
			||||||
 | 
					}' | xargs rm --
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Removes unused icons from the Adwaita theme. It could be even more aggressive,
 | 
				
			||||||
 | 
					# since it keeps around lots of sizes and all the GTK+ stock icons.
 | 
				
			||||||
 | 
					find share/icons/Adwaita -type f | awk 'BEGIN {
 | 
				
			||||||
 | 
						while (("grep -aho \"[a-z][a-z-]*\" *.dll *.exe" | getline) > 0)
 | 
				
			||||||
 | 
							good[$0] = 1
 | 
				
			||||||
 | 
					} /[.](png|svg|cur|ani)$/ {
 | 
				
			||||||
 | 
						# Cut out the basename without extensions.
 | 
				
			||||||
 | 
						match($0, /[^\/]+$/)
 | 
				
			||||||
 | 
						base = substr($0, RSTART)
 | 
				
			||||||
 | 
						sub(/[.].+$/, "", base)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# Try matching while cutting off suffixes.
 | 
				
			||||||
 | 
						# Disregarding the not-much-used GTK_ICON_LOOKUP_GENERIC_FALLBACK.
 | 
				
			||||||
 | 
						while (!(keep = good[base]) &&
 | 
				
			||||||
 | 
							sub(/-(ltr|rtl|symbolic)$/, "", base)) {}
 | 
				
			||||||
 | 
						if (!keep)
 | 
				
			||||||
 | 
							print
 | 
				
			||||||
 | 
					}' | xargs rm --
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wine64 "$msys2_root"/bin/glib-compile-schemas.exe share/glib-2.0/schemas
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# This may speed up program start-up a little bit.
 | 
				
			||||||
 | 
					wine64 "$msys2_root"/bin/gtk-update-icon-cache-3.0.exe share/icons/Adwaita
 | 
				
			||||||
							
								
								
									
										35
									
								
								msys2-package.sh
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,35 @@
 | 
				
			|||||||
 | 
					#!/bin/sh -e
 | 
				
			||||||
 | 
					export LC_ALL=C
 | 
				
			||||||
 | 
					cd "$MESON_BUILD_ROOT"
 | 
				
			||||||
 | 
					arch=$1 msi=$2 files=package-files.wxs
 | 
				
			||||||
 | 
					destdir=$(pwd)/package/${msi%.*}
 | 
				
			||||||
 | 
					shift 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# We're being passed host_machine.cpu(), which will be either x86 or x86_64.
 | 
				
			||||||
 | 
					[ "$arch" = "x86" ] || arch=x64
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					rm -rf "$destdir"
 | 
				
			||||||
 | 
					meson install --destdir "$destdir"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					txt2rtf() {
 | 
				
			||||||
 | 
						LC_ALL=C.UTF-8 iconv -f utf-8 -t ascii//translit "$@" | awk 'BEGIN {
 | 
				
			||||||
 | 
							print "{\\rtf1\\ansi\\ansicpg1252\\deff0{\\fonttbl{\\f0 Tahoma;}}"
 | 
				
			||||||
 | 
							print "\\f0\\fs24{\\pard\\sa240"
 | 
				
			||||||
 | 
						} {
 | 
				
			||||||
 | 
							gsub(/\\/, "\\\\"); gsub(/[{]/, "\\{"); gsub(/[}]/, "\\}")
 | 
				
			||||||
 | 
							if (!$0) { print "\\par}{\\pard\\sa240"; prefix = "" }
 | 
				
			||||||
 | 
							else { print prefix $0; prefix = " " }
 | 
				
			||||||
 | 
						} END {
 | 
				
			||||||
 | 
							print "\\par}}"
 | 
				
			||||||
 | 
						}'
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# msitools have this filename hardcoded in UI files, and it's required.
 | 
				
			||||||
 | 
					txt2rtf "$MESON_SOURCE_ROOT/LICENSE" > License.rtf
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					find "$destdir" -type f \
 | 
				
			||||||
 | 
						| wixl-heat --prefix "$destdir/" --directory-ref INSTALLDIR \
 | 
				
			||||||
 | 
							--component-group CG.fiv --var var.SourceDir > "$files"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					wixl --verbose --arch "$arch" -D SourceDir="$destdir" --ext ui \
 | 
				
			||||||
 | 
						--output "$msi" "$@" "$files"
 | 
				
			||||||
							
								
								
									
										154
									
								
								resources/blend-tool-symbolic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,154 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
				
			||||||
 | 
					    <filter id="a" height="100%" width="100%" x="0%" y="0%">
 | 
				
			||||||
 | 
					        <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
 | 
				
			||||||
 | 
					    </filter>
 | 
				
			||||||
 | 
					    <mask id="b">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="c">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="d">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="e">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="f">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="g">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="h">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="i">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="j">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="k">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="l">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="m">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="n">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="o">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="p">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="q">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="r">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="s">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="t">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="u">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="v">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="w">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="x">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="y">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="z">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="A">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g fill="#2e3436">
 | 
				
			||||||
 | 
					        <path d="m 1 1 v 14 h 14 v -14 z m 1 1 h 12 v 12 h -12 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 6 11 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -1 -3 h 1 v 1 h -1 z m 1 1 h 1 v 1 h -1 z m -4 -1 h 3 v 10 h -3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 8 3 h 1 v 10 h -1 z m 2 9 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m -1 7 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 -2 h 1 v 1 h -1 z m 0 0" fill-opacity="0.524444"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -96 -620)">
 | 
				
			||||||
 | 
					        <path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 6.6 KiB  | 
							
								
								
									
										115
									
								
								resources/checkerboard-symbolic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,115 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
				
			||||||
 | 
					    <filter id="a" height="100%" width="100%" x="0%" y="0%">
 | 
				
			||||||
 | 
					        <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
 | 
				
			||||||
 | 
					    </filter>
 | 
				
			||||||
 | 
					    <mask id="b">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="c">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="d">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="e">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="f">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="g">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="h">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="i">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="j">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="k">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="l">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="m">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="n">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="o">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="p">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="q">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="r">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="s">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 16 748 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 17 747 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 18 750 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 16 750 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 17 751 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 19 751 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 136 776 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -256 -756)">
 | 
				
			||||||
 | 
					        <path d="m 219 758 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g fill="#2e3436">
 | 
				
			||||||
 | 
					        <path d="m 14 5 v -3 h -3 v 3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 11 8 v -3 h -3 v 3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 14 11 v -3 h -3 v 3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 11 14 v -3 h -3 v 3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 8 11 v -3 h -3 v 3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 5 14 v -3 h -3 v 3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 5 8 v -3 h -3 v 3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 8 5 v -3 h -3 v 3 z m 0 0"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 4.3 KiB  | 
							
								
								
									
										4
									
								
								resources/cross-large-symbolic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg">
 | 
				
			||||||
 | 
					    <path d="m 1.980469 2 h 1 h 0.03125 c 0.253906 0.011719 0.511719 0.128906 0.6875 0.3125 l 4.28125 4.28125 l 4.3125 -4.28125 c 0.265625 -0.230469 0.445312 -0.304688 0.6875 -0.3125 h 1 v 1 c 0 0.285156 -0.035157 0.550781 -0.25 0.75 l -4.28125 4.28125 l 4.25 4.25 c 0.1875 0.1875 0.28125 0.453125 0.28125 0.71875 v 1 h -1 c -0.265625 0 -0.53125 -0.09375 -0.71875 -0.28125 l -4.28125 -4.28125 l -4.28125 4.28125 c -0.1875 0.1875 -0.453125 0.28125 -0.71875 0.28125 h -1 v -1 c 0 -0.265625 0.09375 -0.53125 0.28125 -0.71875 l 4.28125 -4.25 l -4.28125 -4.28125 c -0.210938 -0.195312 -0.304688 -0.46875 -0.28125 -0.75 z m 0 0" fill="#222222"/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 774 B  | 
							
								
								
									
										107
									
								
								resources/heal-symbolic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,107 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
				
			||||||
 | 
					    <filter id="a" height="100%" width="100%" x="0%" y="0%">
 | 
				
			||||||
 | 
					        <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
 | 
				
			||||||
 | 
					    </filter>
 | 
				
			||||||
 | 
					    <mask id="b">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="c">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="d">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="e">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="f">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="g">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="h">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="i">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="j">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="k">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="l">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="m">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="n">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="o">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="p">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="q">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="r">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="s">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1600 v 1200 h -1600 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 16 748 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 17 747 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 18 750 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 16 750 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 17 751 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 19 751 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <path d="m 8.4375 1.480469 c -0.21875 0.179687 -0.410156 0.410156 -0.546875 0.675781 l -4.65625 9.125 c -0.542969 1.070312 -0.109375 2.386719 0.96875 2.960938 l 0.945313 0.496093 c 1.074218 0.570313 2.371093 0.175781 2.914062 -0.894531 l 4.660156 -9.121094 c 0.542969 -1.070312 0.109375 -2.390625 -0.96875 -2.960937 l -0.945312 -0.496094 c -0.804688 -0.429687 -1.726563 -0.320313 -2.371094 0.214844 z m -1.472656 4.464843 c 0.332031 -0.246093 0.8125 -0.179687 1.0625 0.140626 c 0.253906 0.320312 0.1875 0.789062 -0.140625 1.035156 c -0.332031 0.246094 -0.8125 0.183594 -1.0625 -0.140625 c -0.253907 -0.320313 -0.1875 -0.789063 0.140625 -1.035157 z m -0.992188 1.980469 c 0.328125 -0.246093 0.808594 -0.183593 1.0625 0.136719 c 0.25 0.324219 0.1875 0.792969 -0.144531 1.039062 c -0.328125 0.246094 -0.808594 0.179688 -1.0625 -0.140624 c -0.25 -0.320313 -0.1875 -0.789063 0.144531 -1.035157 z m 3.023438 -1.011719 c 0.328125 -0.246093 0.808594 -0.183593 1.0625 0.140626 c 0.25 0.320312 0.1875 0.789062 -0.144532 1.035156 c -0.328124 0.246094 -0.808593 0.183594 -1.0625 -0.140625 c -0.25 -0.320313 -0.1875 -0.789063 0.144532 -1.035157 z m -0.992188 1.980469 c 0.328125 -0.246093 0.808594 -0.183593 1.0625 0.136719 c 0.25 0.324219 0.183594 0.792969 -0.144531 1.039062 c -0.328125 0.242188 -0.8125 0.179688 -1.0625 -0.140624 c -0.25 -0.320313 -0.1875 -0.789063 0.144531 -1.035157 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    <path d="m 3.59375 3 c -0.839844 -0.035156 -1.609375 0.4375 -1.96875 1.28125 l -0.4375 1 c -0.480469 1.128906 0 2.46875 1.09375 3 l 0.875 0.40625 l 2.4375 -4.90625 l -1.15625 -0.5625 c -0.273438 -0.132812 -0.5625 -0.207031 -0.84375 -0.21875 z m 9.28125 4.28125 l -2.46875 4.90625 l 1.21875 0.59375 c 1.09375 0.53125 2.332031 0.066406 2.8125 -1.0625 l 0.4375 -1 c 0.480469 -1.128906 0 -2.46875 -1.09375 -3 z m 0 0" fill="#2e3436" fill-opacity="0.501961"/>
 | 
				
			||||||
 | 
					    <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 136 776 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -96 -756)">
 | 
				
			||||||
 | 
					        <path d="m 219 758 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 5.7 KiB  | 
							
								
								
									
										150
									
								
								resources/info-symbolic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,150 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
				
			||||||
 | 
					    <filter id="a" height="100%" width="100%" x="0%" y="0%">
 | 
				
			||||||
 | 
					        <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
 | 
				
			||||||
 | 
					    </filter>
 | 
				
			||||||
 | 
					    <mask id="b">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="c">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="d">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="e">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="f">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="g">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="h">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="i">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="j">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="k">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="l">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="m">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="n">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="o">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="p">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="q">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="r">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="s">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="t">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="u">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="v">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="w">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="x">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="y">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="z">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="A">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <path d="m 7.90625 1 c -3.828125 0.050781 -6.90625 3.171875 -6.90625 7 c 0 3.867188 3.132812 7 7 7 s 7 -3.132812 7 -7 s -3.132812 -7 -7 -7 c -0.03125 0 -0.0625 0 -0.09375 0 z m -0.40625 3 h 1 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 h -1 c -0.277344 0 -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m -0.5 3 h 2 v 5 h -2 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -660 -222)">
 | 
				
			||||||
 | 
					        <path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 6.4 KiB  | 
							
								
								
									
										152
									
								
								resources/pin2-symbolic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,152 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
				
			||||||
 | 
					    <filter id="a" height="100%" width="100%" x="0%" y="0%">
 | 
				
			||||||
 | 
					        <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
 | 
				
			||||||
 | 
					    </filter>
 | 
				
			||||||
 | 
					    <mask id="b">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="c">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="d">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="e">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="f">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="g">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="h">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="i">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="j">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="k">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="l">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="m">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="n">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="o">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="p">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="q">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="r">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="s">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="t">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="u">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="v">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="w">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="x">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="y">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="z">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="A">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <path d="m 14 28 c 0 3.3125 -2.6875 6 -6 6 s -6 -2.6875 -6 -6 s 2.6875 -6 6 -6 s 6 2.6875 6 6 z m 0 0" fill="none" stroke="#2e3436" stroke-linecap="round" stroke-width="2"/>
 | 
				
			||||||
 | 
					    <path d="m 6.992188 2 l -5.070313 4.992188 l 3.261719 2.539062 c -0.519532 2.046875 0.078125 4.214844 1.566406 5.710938 l 3.742188 -3.742188 l 4.5 4.5 h 1 v -1 l -4.5 -4.5 l 3.746093 -3.742188 c -1.5 -1.496093 -3.679687 -2.089843 -5.730469 -1.5625 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    <path d="m 1.992188 7 l 5 -5" fill="none" stroke="#2e3436" stroke-linecap="square" stroke-width="2"/>
 | 
				
			||||||
 | 
					    <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -620 -624)">
 | 
				
			||||||
 | 
					        <path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 6.6 KiB  | 
@@ -1,7 +1,18 @@
 | 
				
			|||||||
<?xml version="1.0" encoding="UTF-8"?>
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
<gresources>
 | 
					<gresources>
 | 
				
			||||||
	<gresource prefix="/org/gnome/design/IconLibrary/">
 | 
						<gresource prefix="/">
 | 
				
			||||||
 | 
							<file alias="LICENSE">../LICENSE</file>
 | 
				
			||||||
 | 
						</gresource>
 | 
				
			||||||
 | 
						<gresource prefix="/org/gnome/design/IconLibrary/scalable/actions/">
 | 
				
			||||||
 | 
							<file preprocess="xml-stripblanks">text-symbolic.svg</file>
 | 
				
			||||||
		<file preprocess="xml-stripblanks">circle-filled-symbolic.svg</file>
 | 
							<file preprocess="xml-stripblanks">circle-filled-symbolic.svg</file>
 | 
				
			||||||
 | 
							<file preprocess="xml-stripblanks">cross-large-symbolic.svg</file>
 | 
				
			||||||
		<file preprocess="xml-stripblanks">funnel-symbolic.svg</file>
 | 
							<file preprocess="xml-stripblanks">funnel-symbolic.svg</file>
 | 
				
			||||||
 | 
							<file preprocess="xml-stripblanks">blend-tool-symbolic.svg</file>
 | 
				
			||||||
 | 
							<file preprocess="xml-stripblanks">checkerboard-symbolic.svg</file>
 | 
				
			||||||
 | 
							<file preprocess="xml-stripblanks">heal-symbolic.svg</file>
 | 
				
			||||||
 | 
							<file preprocess="xml-stripblanks">info-symbolic.svg</file>
 | 
				
			||||||
 | 
							<file preprocess="xml-stripblanks">pin2-symbolic.svg</file>
 | 
				
			||||||
 | 
							<file preprocess="xml-stripblanks">shapes-symbolic.svg</file>
 | 
				
			||||||
	</gresource>
 | 
						</gresource>
 | 
				
			||||||
</gresources>
 | 
					</gresources>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										154
									
								
								resources/shapes-symbolic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,154 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
				
			||||||
 | 
					    <filter id="a" height="100%" width="100%" x="0%" y="0%">
 | 
				
			||||||
 | 
					        <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
 | 
				
			||||||
 | 
					    </filter>
 | 
				
			||||||
 | 
					    <mask id="b">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="c">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="d">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="e">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="f">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="g">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="h">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="i">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="j">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="k">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="l">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="m">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="n">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="o">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="p">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="q">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="r">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="s">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="t">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="u">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="v">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="w">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="x">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="y">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="z">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="A">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <g fill="#2e3436">
 | 
				
			||||||
 | 
					        <path d="m 5.191406 1.296875 c -0.390625 -0.390625 -1.023437 -0.390625 -1.414062 0 l -2.5 2.5 c -0.390625 0.390625 -0.390625 1.023437 0 1.414063 l 2.5 2.5 c 0.390625 0.390624 1.023437 0.390624 1.414062 0 l 2.496094 -2.5 c 0.390625 -0.390626 0.390625 -1.023438 0 -1.414063 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 9.984375 12.003906 c 0 1.65625 -1.34375 3 -3 3 c -1.660156 0 -3 -1.34375 -3 -3 s 1.339844 -3 3 -3 c 1.65625 0 3 1.34375 3 3 z m 0 0"/>
 | 
				
			||||||
 | 
					        <path d="m 11.929688 2.007812 c -0.339844 0.015626 -0.644532 0.203126 -0.8125 0.496094 l -2.320313 4 c -0.386719 0.664063 0.09375 1.5 0.863281 1.5 h 4.644532 c 0.769531 0 1.25 -0.835937 0.863281 -1.5 l -2.320313 -4 c -0.1875 -0.328125 -0.542968 -0.519531 -0.917968 -0.496094 z m 0 0"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -620 -420)">
 | 
				
			||||||
 | 
					        <path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 6.7 KiB  | 
							
								
								
									
										150
									
								
								resources/text-symbolic.svg
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,150 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<svg height="16px" viewBox="0 0 16 16" width="16px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
 | 
				
			||||||
 | 
					    <filter id="a" height="100%" width="100%" x="0%" y="0%">
 | 
				
			||||||
 | 
					        <feColorMatrix in="SourceGraphic" type="matrix" values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 1 0"/>
 | 
				
			||||||
 | 
					    </filter>
 | 
				
			||||||
 | 
					    <mask id="b">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="c">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="d">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="e">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="f">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="g">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="h">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="i">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="j">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="k">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="l">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="m">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="n">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.05"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="o">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="p">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.3"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="q">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="r">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="s">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="t">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="u">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="v">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.4"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="w">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="x">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="y">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <mask id="z">
 | 
				
			||||||
 | 
					        <g filter="url(#a)">
 | 
				
			||||||
 | 
					            <path d="m 0 0 h 16 v 16 h -16 z" fill-opacity="0.5"/>
 | 
				
			||||||
 | 
					        </g>
 | 
				
			||||||
 | 
					    </mask>
 | 
				
			||||||
 | 
					    <clipPath id="A">
 | 
				
			||||||
 | 
					        <path d="m 0 0 h 1024 v 800 h -1024 z"/>
 | 
				
			||||||
 | 
					    </clipPath>
 | 
				
			||||||
 | 
					    <g clip-path="url(#c)" mask="url(#b)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 562.460938 212.058594 h 10.449218 c -1.183594 0.492187 -1.296875 2.460937 0 3 h -10.449218 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#e)" mask="url(#d)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 16 632 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#g)" mask="url(#f)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 17 631 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#i)" mask="url(#h)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 18 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#k)" mask="url(#j)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 16 634 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#m)" mask="url(#l)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 17 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#o)" mask="url(#n)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 19 635 h 1 v 1 h -1 z m 0 0" fill="#2e3436" fill-rule="evenodd"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <path d="m 6 1 l -5 14 h 3 c 1.484375 -4 0.023438 0 1.507812 -4 h 4.984376 l 1.507812 4 h 3 l -5 -14 z m 2 3 l 2.023438 5 h -4 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    <g clip-path="url(#q)" mask="url(#p)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 136 660 v 7 h 7 v -7 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#s)" mask="url(#r)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 199 642 h 3 v 12 h -3 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#u)" mask="url(#t)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 209.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#w)" mask="url(#v)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 206.5 144.160156 c 0.277344 0 0.5 0.222656 0.5 0.5 v 1 c 0 0.277344 -0.222656 0.5 -0.5 0.5 s -0.5 -0.222656 -0.5 -0.5 v -1 c 0 -0.277344 0.222656 -0.5 0.5 -0.5 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#y)" mask="url(#x)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 229.5 143.160156 c -0.546875 0 -1 0.457032 -1 1 c 0 0.546875 0.453125 1 1 1 s 1 -0.453125 1 -1 c 0 -0.542968 -0.453125 -1 -1 -1 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					    <g clip-path="url(#A)" mask="url(#z)" transform="matrix(1 0 0 1 -56 -640)">
 | 
				
			||||||
 | 
					        <path d="m 226.453125 143.160156 c -0.519531 0 -0.953125 0.433594 -0.953125 0.953125 v 0.09375 c 0 0.519531 0.433594 0.953125 0.953125 0.953125 h 0.09375 c 0.519531 0 0.953125 -0.433594 0.953125 -0.953125 v -0.09375 c 0 -0.519531 -0.433594 -0.953125 -0.953125 -0.953125 z m 0 0" fill="#2e3436"/>
 | 
				
			||||||
 | 
					    </g>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 6.1 KiB  | 
							
								
								
									
										1
									
								
								submodules/liberty
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										1
									
								
								submodules/wuffs-mirror-release-c
									
									
									
									
									
										Submodule
									
								
							
							
								
								
								
								
								
							
						
						
							
								
								
									
										9
									
								
								subprojects/libjpegqs.wrap
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
				
			|||||||
 | 
					[wrap-file]
 | 
				
			||||||
 | 
					directory = jpeg-quantsmooth-1.20230818
 | 
				
			||||||
 | 
					source_url = https://github.com/ilyakurdyukov/jpeg-quantsmooth/archive/refs/tags/1.20230818.tar.gz
 | 
				
			||||||
 | 
					source_filename = jpeg-quantsmooth-1.20230818.tar.gz
 | 
				
			||||||
 | 
					source_hash = ff9a62e8560851648c60d84b3d97ebd9769f01ce6b995779e071d19a759eca06
 | 
				
			||||||
 | 
					patch_directory = libjpegqs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[provide]
 | 
				
			||||||
 | 
					libjpegqs = jpegqs_dep
 | 
				
			||||||
							
								
								
									
										4
									
								
								subprojects/packagefiles/libjpegqs/include/libjpegqs.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					// This separate directory is necessary for Debian's multiarch with jpeg-turbo,
 | 
				
			||||||
 | 
					// because its jpeglib.h cannot perform local inclusion of jconfig.h,
 | 
				
			||||||
 | 
					// resulting in it being found within jpeg-quantsmooth and breaking the build.
 | 
				
			||||||
 | 
					#include "../libjpegqs.h"
 | 
				
			||||||
							
								
								
									
										45
									
								
								subprojects/packagefiles/libjpegqs/meson.build
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,45 @@
 | 
				
			|||||||
 | 
					# vim: noet ts=4 sts=4 sw=4:
 | 
				
			||||||
 | 
					project('jpeg-qs', 'c')
 | 
				
			||||||
 | 
					add_project_arguments('-DWITH_LOG', language : 'c')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					deps = [
 | 
				
			||||||
 | 
						dependency('libjpeg'),
 | 
				
			||||||
 | 
						meson.get_compiler('c').find_library('m', required : false),
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if host_machine.cpu_family() == 'x86_64'
 | 
				
			||||||
 | 
						jpegqs_avx512 = static_library('jpegqs-avx512', 'libjpegqs.c',
 | 
				
			||||||
 | 
							c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=avx512',
 | 
				
			||||||
 | 
								'-mavx512f', '-mfma', '-DSIMD_AVX512'],
 | 
				
			||||||
 | 
							dependencies : deps,
 | 
				
			||||||
 | 
							implicit_include_directories : false)
 | 
				
			||||||
 | 
						jpegqs_avx2 = static_library('jpegqs-avx2', 'libjpegqs.c',
 | 
				
			||||||
 | 
							c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=avx2',
 | 
				
			||||||
 | 
								'-mavx2', '-mfma', '-DSIMD_AVX2'],
 | 
				
			||||||
 | 
							dependencies : deps,
 | 
				
			||||||
 | 
							implicit_include_directories : false)
 | 
				
			||||||
 | 
						jpegqs_sse2 = static_library('jpegqs-sse2', 'libjpegqs.c',
 | 
				
			||||||
 | 
							c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=sse2', '-msse2', '-DSIMD_SSE2'],
 | 
				
			||||||
 | 
							dependencies : deps,
 | 
				
			||||||
 | 
							implicit_include_directories : false)
 | 
				
			||||||
 | 
						jpegqs_base = static_library('jpegqs-base', 'libjpegqs.c',
 | 
				
			||||||
 | 
							c_args : ['-DSIMD_SELECT', '-DSIMD_NAME=base', '-DSIMD_BASE'],
 | 
				
			||||||
 | 
							dependencies : deps,
 | 
				
			||||||
 | 
							implicit_include_directories : false)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jpegqs_lib = static_library('jpegqs', 'libjpegqs.c',
 | 
				
			||||||
 | 
							c_args : ['-DSIMD_SELECT'],
 | 
				
			||||||
 | 
							dependencies : deps,
 | 
				
			||||||
 | 
							link_with : [jpegqs_base, jpegqs_sse2, jpegqs_avx2, jpegqs_avx512],
 | 
				
			||||||
 | 
							implicit_include_directories : false)
 | 
				
			||||||
 | 
					else
 | 
				
			||||||
 | 
						jpegqs_lib = static_library('jpegqs', 'libjpegqs.c',
 | 
				
			||||||
 | 
							c_args : ['-DNO_SIMD'],
 | 
				
			||||||
 | 
							dependencies : deps,
 | 
				
			||||||
 | 
							implicit_include_directories : false)
 | 
				
			||||||
 | 
					endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					jpegqs_dep = declare_dependency(
 | 
				
			||||||
 | 
						link_with : jpegqs_lib,
 | 
				
			||||||
 | 
						include_directories : include_directories('include'),
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
@@ -1,8 +0,0 @@
 | 
				
			|||||||
[wrap-file]
 | 
					 | 
				
			||||||
directory = libspng-0.7.1
 | 
					 | 
				
			||||||
source_url = https://github.com/randy408/libspng/archive/refs/tags/v0.7.1.tar.gz
 | 
					 | 
				
			||||||
source_filename = libspng-0.7.1.tar.gz
 | 
					 | 
				
			||||||
source_hash = 0726a4914ad7155028f3baa94027244d439cd2a2fbe8daf780c2150c4c951d8e
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
[provide]
 | 
					 | 
				
			||||||
spng = spng_dep
 | 
					 | 
				
			||||||
							
								
								
									
										112
									
								
								tiff-tables.awk
									
									
									
									
									
										Executable file
									
								
							
							
						
						@@ -0,0 +1,112 @@
 | 
				
			|||||||
 | 
					#!/usr/bin/awk -f
 | 
				
			||||||
 | 
					BEGIN {
 | 
				
			||||||
 | 
						FS = ", *"
 | 
				
			||||||
 | 
						print "// Generated by tiff-tables.awk. DO NOT MODIFY."
 | 
				
			||||||
 | 
						print ""
 | 
				
			||||||
 | 
						print "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
 | 
				
			||||||
 | 
						print "#include <stddef.h>"
 | 
				
			||||||
 | 
						print "#include <stdint.h>"
 | 
				
			||||||
 | 
						print ""
 | 
				
			||||||
 | 
						print "struct tiff_value {"
 | 
				
			||||||
 | 
						print "\tconst char *name;"
 | 
				
			||||||
 | 
						print "\tuint16_t value;"
 | 
				
			||||||
 | 
						print "};"
 | 
				
			||||||
 | 
						print ""
 | 
				
			||||||
 | 
						print "struct tiff_entry {"
 | 
				
			||||||
 | 
						print "\tconst char *name;"
 | 
				
			||||||
 | 
						print "\tuint16_t tag;"
 | 
				
			||||||
 | 
						print "\tstruct tiff_value *values;"
 | 
				
			||||||
 | 
						print "};"
 | 
				
			||||||
 | 
						print "#endif"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						# Remember and strip consecutive comments.
 | 
				
			||||||
 | 
						if (match($0, /#/))
 | 
				
			||||||
 | 
							comment[++comments] = substr($0, RSTART + 1)
 | 
				
			||||||
 | 
						else if (!/[[:space:]]/)
 | 
				
			||||||
 | 
							comments = 0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						sub(/#.*$/, "")
 | 
				
			||||||
 | 
						sub(/[[:space:]]*$/, "")
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Converts arbitrary strings to C identifiers (when running in the C locale).
 | 
				
			||||||
 | 
					function identifize(s) {
 | 
				
			||||||
 | 
						# Regard parenthesised text as comments.
 | 
				
			||||||
 | 
						while (match(s, /[[:space:]]\([^)]+\)/)) {
 | 
				
			||||||
 | 
							comment[++comments] = substr(s, RSTART, RLENGTH)
 | 
				
			||||||
 | 
							s = substr(s, 1, RSTART - 1) substr(s, RSTART + RLENGTH)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# Capitalize words (toupper is POSIX), removing spaces and dashes between.
 | 
				
			||||||
 | 
						while (match(s, /[-[:space:]]./)) {
 | 
				
			||||||
 | 
							s = substr(s, 1, RSTART - 1) \
 | 
				
			||||||
 | 
								toupper(substr(s, RSTART + 1, 1)) \
 | 
				
			||||||
 | 
								substr(s, RSTART + RLENGTH)
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						# Replace any remaining non-identifier characters with underscores.
 | 
				
			||||||
 | 
						gsub(/[^[:alnum:]]/, "_", s)
 | 
				
			||||||
 | 
						return s
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function flushcomments(prefix,    i, acc) {
 | 
				
			||||||
 | 
						for (i = 1; i <= comments; i++)
 | 
				
			||||||
 | 
							acc = acc prefix comment[i] "\n"
 | 
				
			||||||
 | 
						comments = 0
 | 
				
			||||||
 | 
						return acc
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function flushvalues() {
 | 
				
			||||||
 | 
						if (values) {
 | 
				
			||||||
 | 
							allvalues = allvalues "enum " fieldname " {\n" values "};\n\n"
 | 
				
			||||||
 | 
							values = ""
 | 
				
			||||||
 | 
							fields = fields "\n\t\t{}\n\t}},"
 | 
				
			||||||
 | 
						} else if (fields) {
 | 
				
			||||||
 | 
							fields = fields " NULL},"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					function flushsection() {
 | 
				
			||||||
 | 
						if (section) {
 | 
				
			||||||
 | 
							flushvalues()
 | 
				
			||||||
 | 
							print "};\n\n" allvalues "#ifndef TIFF_TABLES_CONSTANTS_ONLY"
 | 
				
			||||||
 | 
							print "static struct tiff_entry " \
 | 
				
			||||||
 | 
								  sectionsnakecase "_entries[] = {" fields "\n\t{}\n};"
 | 
				
			||||||
 | 
							print "#endif"
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Section marker
 | 
				
			||||||
 | 
					/^= / {
 | 
				
			||||||
 | 
						flushsection()
 | 
				
			||||||
 | 
						section = identifize(substr($0, 3))
 | 
				
			||||||
 | 
						sectionsnakecase = tolower(substr($0, 3))
 | 
				
			||||||
 | 
						gsub(/[^[:alnum:]]/, "_", sectionsnakecase)
 | 
				
			||||||
 | 
						fields = ""
 | 
				
			||||||
 | 
						allvalues = ""
 | 
				
			||||||
 | 
						print "\n" flushcomments("//") "enum {"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Field
 | 
				
			||||||
 | 
					section && /^[^\t=]/ {
 | 
				
			||||||
 | 
						flushvalues()
 | 
				
			||||||
 | 
						fieldname = section "_" identifize($2)
 | 
				
			||||||
 | 
						fields = fields "\n\t{\"" $2 "\", " fieldname ","
 | 
				
			||||||
 | 
						print flushcomments("\t//") "\t" fieldname " = " $1 ","
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Value
 | 
				
			||||||
 | 
					section && /^\t/ {
 | 
				
			||||||
 | 
						sub(/^\t*/, "")
 | 
				
			||||||
 | 
						valuename = fieldname "_" identifize($2)
 | 
				
			||||||
 | 
						if (!values)
 | 
				
			||||||
 | 
							fields = fields " (struct tiff_value[]) {"
 | 
				
			||||||
 | 
						values = values flushcomments("\t//") "\t" valuename " = " $1 ",\n"
 | 
				
			||||||
 | 
						fields = fields "\n\t\t{\"" $2 "\", " valuename "},"
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					END {
 | 
				
			||||||
 | 
						flushsection()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										471
									
								
								tiff-tables.db
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,471 @@
 | 
				
			|||||||
 | 
					# Use tiff-tables.awk to produce a C source file from this database.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Use the Internet Archive should any of these links go down.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# TIFF Revision 6.0 (1992)
 | 
				
			||||||
 | 
					# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFF6.pdf
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# TIFF Technical Note 1: TIFF Trees (1993)
 | 
				
			||||||
 | 
					# https://download.osgeo.org/libtiff/old/TTN1.ps
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# DRAFT TIFF Technical Note 2 (1995)
 | 
				
			||||||
 | 
					# https://www.awaresystems.be/imaging/tiff/specification/TIFFTechNote2.txt
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Adobe PageMaker 6.0 TIFF Technical Notes (1995) [includes TTN1]
 | 
				
			||||||
 | 
					# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFPM6.pdf
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Adobe Photoshop TIFF Technical Notes (2002)
 | 
				
			||||||
 | 
					# https://www.adobe.io/content/dam/udp/en/open/standards/tiff/TIFFphotoshop.pdf
 | 
				
			||||||
 | 
					# https://www.alternatiff.com/resources/TIFFphotoshop.pdf
 | 
				
			||||||
 | 
					#  - Note that ImageSourceData 8BIM frames are specified differently
 | 
				
			||||||
 | 
					#    from how Adobe XMP Specification Part 3 defines them.
 | 
				
			||||||
 | 
					#  - The document places a condition on SubIFDs, without further explanation.
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Adobe Photoshop TIFF Technical Note 3 (2005)
 | 
				
			||||||
 | 
					# http://chriscox.org/TIFFTN3d1.pdf
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Exif Version 2.3 (2012)
 | 
				
			||||||
 | 
					# https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Exif Version 2.32 (2019)
 | 
				
			||||||
 | 
					# https://www.cipa.jp/e/std/std-sec.html
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# ISO/DIS 12234-2 (TIFF/EP) (2000-06-21)
 | 
				
			||||||
 | 
					# http://www.barrypearson.co.uk/top2009/downloads/TAG2000-22_DIS12234-2.pdf
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# Digital Negative (DNG) Specification 1.5.0.0 (2019)
 | 
				
			||||||
 | 
					# https://www.adobe.com/content/dam/acom/en/products/photoshop/pdfs/dng_spec_1.5.0.0.pdf
 | 
				
			||||||
 | 
					#
 | 
				
			||||||
 | 
					# CIPA DC-007-2021 (Multi-Picture Format)
 | 
				
			||||||
 | 
					# https://www.cipa.jp/e/std/std-sec.html
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# TIFF 6.0
 | 
				
			||||||
 | 
					= TIFF
 | 
				
			||||||
 | 
					254, NewSubfileType
 | 
				
			||||||
 | 
					255, SubfileType
 | 
				
			||||||
 | 
						1, Full-resolution image data
 | 
				
			||||||
 | 
						2, Reduced-resolution image data
 | 
				
			||||||
 | 
						3, Page of a multi-page image
 | 
				
			||||||
 | 
					256, ImageWidth
 | 
				
			||||||
 | 
					257, ImageLength
 | 
				
			||||||
 | 
					258, BitsPerSample
 | 
				
			||||||
 | 
					259, Compression
 | 
				
			||||||
 | 
						1, Uncompressed
 | 
				
			||||||
 | 
						2, CCITT 1D
 | 
				
			||||||
 | 
						3, Group 3 Fax
 | 
				
			||||||
 | 
						4, Group 4 Fax
 | 
				
			||||||
 | 
						5, LZW
 | 
				
			||||||
 | 
						6, JPEG
 | 
				
			||||||
 | 
						7, JPEG datastream  # DRAFT TIFF Technical Note 2 + TIFFphotoshop.pdf
 | 
				
			||||||
 | 
						8, Deflate/zlib  # Adobe Photoshop TIFF Technical Notes
 | 
				
			||||||
 | 
						32773, PackBits
 | 
				
			||||||
 | 
						32946, Deflate  # Adobe Photoshop TIFF Technical Notes
 | 
				
			||||||
 | 
					262, PhotometricInterpretation
 | 
				
			||||||
 | 
						0, WhiteIsZero
 | 
				
			||||||
 | 
						1, BlackIsZero
 | 
				
			||||||
 | 
						2, RGB
 | 
				
			||||||
 | 
						3, RGB Palette
 | 
				
			||||||
 | 
						4, Transparency mask
 | 
				
			||||||
 | 
						5, CMYK
 | 
				
			||||||
 | 
						6, YCbCr
 | 
				
			||||||
 | 
						8, CIELab
 | 
				
			||||||
 | 
						9, ICCLab  # Adobe PageMaker 6.0 TIFF Technical Notes
 | 
				
			||||||
 | 
						32803, Color filter array  # DIS/ISO 12234-2 + DNG 1.5.0.0
 | 
				
			||||||
 | 
						34892, LinearRaw  # DNG 1.5.0.0
 | 
				
			||||||
 | 
					263, Threshholding
 | 
				
			||||||
 | 
						1, No dithering or halftoning
 | 
				
			||||||
 | 
						2, Ordered dither or halftoning
 | 
				
			||||||
 | 
						3, Randomized process
 | 
				
			||||||
 | 
					264, CellWidth
 | 
				
			||||||
 | 
					265, CellLength
 | 
				
			||||||
 | 
					266, FillOrder
 | 
				
			||||||
 | 
						1, MSB-first
 | 
				
			||||||
 | 
						2, LSB-first
 | 
				
			||||||
 | 
					269, DocumentName
 | 
				
			||||||
 | 
					270, ImageDescription
 | 
				
			||||||
 | 
					271, Make
 | 
				
			||||||
 | 
					272, Model
 | 
				
			||||||
 | 
					273, StripOffsets
 | 
				
			||||||
 | 
					274, Orientation
 | 
				
			||||||
 | 
						1, TopLeft
 | 
				
			||||||
 | 
						2, TopRight
 | 
				
			||||||
 | 
						3, BottomRight
 | 
				
			||||||
 | 
						4, BottomLeft
 | 
				
			||||||
 | 
						5, LeftTop
 | 
				
			||||||
 | 
						6, RightTop
 | 
				
			||||||
 | 
						7, RightBottom
 | 
				
			||||||
 | 
						8, LeftBottom
 | 
				
			||||||
 | 
					277, SamplesPerPixel
 | 
				
			||||||
 | 
					278, RowsPerStrip
 | 
				
			||||||
 | 
					279, StripByteCounts
 | 
				
			||||||
 | 
					280, MinSampleValue
 | 
				
			||||||
 | 
					281, MaxSampleValue
 | 
				
			||||||
 | 
					282, XResolution
 | 
				
			||||||
 | 
					283, YResolution
 | 
				
			||||||
 | 
					284, PlanarConfiguration
 | 
				
			||||||
 | 
						1, Chunky
 | 
				
			||||||
 | 
						2, Planar
 | 
				
			||||||
 | 
					285, PageName
 | 
				
			||||||
 | 
					286, XPosition
 | 
				
			||||||
 | 
					287, YPosition
 | 
				
			||||||
 | 
					288, FreeOffsets
 | 
				
			||||||
 | 
					289, FreeByteCounts
 | 
				
			||||||
 | 
					290, GrayResponseUnit
 | 
				
			||||||
 | 
						1, 1/10
 | 
				
			||||||
 | 
						2, 1/100
 | 
				
			||||||
 | 
						3, 1/1000
 | 
				
			||||||
 | 
						4, 1/10000
 | 
				
			||||||
 | 
						5, 1/100000
 | 
				
			||||||
 | 
					291, GrayResponseCurve
 | 
				
			||||||
 | 
					292, T4Options
 | 
				
			||||||
 | 
					293, T6Options
 | 
				
			||||||
 | 
					296, ResolutionUnit
 | 
				
			||||||
 | 
						1, None
 | 
				
			||||||
 | 
						2, Inch
 | 
				
			||||||
 | 
						3, Centimeter
 | 
				
			||||||
 | 
					297, PageNumber
 | 
				
			||||||
 | 
					301, TransferFunction
 | 
				
			||||||
 | 
					305, Software
 | 
				
			||||||
 | 
					306, DateTime
 | 
				
			||||||
 | 
					315, Artist
 | 
				
			||||||
 | 
					316, HostComputer
 | 
				
			||||||
 | 
					317, Predictor
 | 
				
			||||||
 | 
						1, None
 | 
				
			||||||
 | 
						2, Horizontal
 | 
				
			||||||
 | 
						3, Floating point  # Adobe Photoshop TIFF Technical Note 3
 | 
				
			||||||
 | 
					318, WhitePoint
 | 
				
			||||||
 | 
					319, PrimaryChromaticities
 | 
				
			||||||
 | 
					320, ColorMap
 | 
				
			||||||
 | 
					321, HalftoneHints
 | 
				
			||||||
 | 
					322, TileWidth
 | 
				
			||||||
 | 
					323, TileLength
 | 
				
			||||||
 | 
					324, TileOffsets
 | 
				
			||||||
 | 
					325, TileByteCounts
 | 
				
			||||||
 | 
					330, SubIFDs  # TIFF Technical Note 1: TIFF Trees
 | 
				
			||||||
 | 
					332, InkSet
 | 
				
			||||||
 | 
						1, CMYK
 | 
				
			||||||
 | 
						2, Non-CMYK
 | 
				
			||||||
 | 
					333, InkNames
 | 
				
			||||||
 | 
					334, NumberOfInks
 | 
				
			||||||
 | 
					336, DotRange
 | 
				
			||||||
 | 
					337, TargetPrinter
 | 
				
			||||||
 | 
					338, ExtraSamples
 | 
				
			||||||
 | 
						0, Unspecified
 | 
				
			||||||
 | 
						1, Associated alpha
 | 
				
			||||||
 | 
						2, Unassociated alpha
 | 
				
			||||||
 | 
					339, SampleFormat
 | 
				
			||||||
 | 
						1, Unsigned integer
 | 
				
			||||||
 | 
						2, Two's complement signed integer
 | 
				
			||||||
 | 
						3, IEEE floating-point
 | 
				
			||||||
 | 
						4, Undefined
 | 
				
			||||||
 | 
					340, SMinSampleValue
 | 
				
			||||||
 | 
					341, SMaxSampleValue
 | 
				
			||||||
 | 
					342, TransferRange
 | 
				
			||||||
 | 
					343, ClipPath  # TIFF Technical Note 2: Clipping Path
 | 
				
			||||||
 | 
					344, XClipPathUnits  # TIFF Technical Note 2: Clipping Path
 | 
				
			||||||
 | 
					345, YClipPathUnits  # TIFF Technical Note 2: Clipping Path
 | 
				
			||||||
 | 
					346, Indexed  # TIFF Technical Note 3: Indexed Images
 | 
				
			||||||
 | 
					347, JPEGTables  # DRAFT TIFF Technical Note 2 + TIFFphotoshop.pdf
 | 
				
			||||||
 | 
					351, OPIProxy  # Adobe PageMaker 6.0 TIFF Technical Notes
 | 
				
			||||||
 | 
					512, JPEGProc
 | 
				
			||||||
 | 
						1, Baseline sequential
 | 
				
			||||||
 | 
						14, Lossless Huffman
 | 
				
			||||||
 | 
					513, JPEGInterchangeFormat
 | 
				
			||||||
 | 
					514, JPEGInterchangeFormatLength
 | 
				
			||||||
 | 
					515, JPEGRestartInterval
 | 
				
			||||||
 | 
					517, JPEGLosslessPredictors
 | 
				
			||||||
 | 
						1, A
 | 
				
			||||||
 | 
						2, B
 | 
				
			||||||
 | 
						3, C
 | 
				
			||||||
 | 
						4, A+B+C
 | 
				
			||||||
 | 
						5, A+((B-C)/2)
 | 
				
			||||||
 | 
						6, B+((A-C)/2)
 | 
				
			||||||
 | 
						7, (A+B)/2
 | 
				
			||||||
 | 
					518, JPEGPointTransforms
 | 
				
			||||||
 | 
					519, JPEGQTables
 | 
				
			||||||
 | 
					520, JPEGDCTables
 | 
				
			||||||
 | 
					521, JPEGACTables
 | 
				
			||||||
 | 
					529, YCbCrCoefficients
 | 
				
			||||||
 | 
					530, YCbCrSubSampling
 | 
				
			||||||
 | 
					531, YCbCrPositioning
 | 
				
			||||||
 | 
						1, Centered
 | 
				
			||||||
 | 
						2, Co-sited
 | 
				
			||||||
 | 
					532, ReferenceBlackWhite
 | 
				
			||||||
 | 
					700, XMP  # Adobe XMP Specification Part 3 Table 12/13/39
 | 
				
			||||||
 | 
					32781, ImageID  # Adobe PageMaker 6.0 TIFF Technical Notes
 | 
				
			||||||
 | 
					33421, CFARepeatPatternDim  # DIS/ISO 12234-2
 | 
				
			||||||
 | 
					33422, CFAPattern  # DIS/ISO 12234-2
 | 
				
			||||||
 | 
					33423, BatteryLevel  # DIS/ISO 12234-2
 | 
				
			||||||
 | 
					33432, Copyright
 | 
				
			||||||
 | 
					# TODO(p): Extract IPTC DataSets, like we do directly with PSIRs.
 | 
				
			||||||
 | 
					33723, IPTC  # Adobe XMP Specification Part 3 Table 12/39
 | 
				
			||||||
 | 
					# TODO(p): Extract PSIRs, like we do directly with the JPEG segment.
 | 
				
			||||||
 | 
					34377, Photoshop  # Adobe XMP Specification Part 3 Table 12/39
 | 
				
			||||||
 | 
					34665, Exif IFD Pointer  # Exif 2.3
 | 
				
			||||||
 | 
					34853, GPS Info IFD Pointer  # Exif 2.3
 | 
				
			||||||
 | 
					37398, TIFF/EP StandardID  # DIS/ISO 12234-2
 | 
				
			||||||
 | 
					37399, SensingMethod  # DIS/ISO 12234-2, similar to Exif 41495
 | 
				
			||||||
 | 
						0, Undefined
 | 
				
			||||||
 | 
						1, Monochrome area sensor
 | 
				
			||||||
 | 
						2, One-chip color area sensor
 | 
				
			||||||
 | 
						3, Two-chip color area sensor
 | 
				
			||||||
 | 
						4, Three-chip color area sensor
 | 
				
			||||||
 | 
						5, Color sequential area sensor
 | 
				
			||||||
 | 
						6, Monochrome linear sensor
 | 
				
			||||||
 | 
						7, Trilinear sensor
 | 
				
			||||||
 | 
						8, Color sequential linear sensor
 | 
				
			||||||
 | 
					# TODO(p): Add more TIFF/EP tags that can be only in IFD0.
 | 
				
			||||||
 | 
					37724, ImageSourceData  # Adobe Photoshop TIFF Technical Notes
 | 
				
			||||||
 | 
					50706, DNGVersion  # DNG 1.5.0.0
 | 
				
			||||||
 | 
					50707, DNGBackwardVersion  # DNG 1.5.0.0
 | 
				
			||||||
 | 
					50708, UniqueCameraModel  # DNG 1.5.0.0
 | 
				
			||||||
 | 
					50709, LocalizedCameraModel  # DNG 1.5.0.0
 | 
				
			||||||
 | 
					# TODO(p): Add more DNG tags that can be only in IFD0.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Exif 2.3 4.6.5
 | 
				
			||||||
 | 
					= Exif
 | 
				
			||||||
 | 
					33434, ExposureTime
 | 
				
			||||||
 | 
					33437, FNumber
 | 
				
			||||||
 | 
					34850, ExposureProgram
 | 
				
			||||||
 | 
						0, Not defined
 | 
				
			||||||
 | 
						1, Manual
 | 
				
			||||||
 | 
						2, Normal program
 | 
				
			||||||
 | 
						3, Aperture priority
 | 
				
			||||||
 | 
						4, Shutter priority
 | 
				
			||||||
 | 
						5, Creative program
 | 
				
			||||||
 | 
						6, Action program
 | 
				
			||||||
 | 
						7, Portrait mode
 | 
				
			||||||
 | 
						8, Landscape mode
 | 
				
			||||||
 | 
					34852, SpectralSensitivity
 | 
				
			||||||
 | 
					34855, PhotographicSensitivity
 | 
				
			||||||
 | 
					34856, OECF
 | 
				
			||||||
 | 
					34864, SensitivityType
 | 
				
			||||||
 | 
						0, Unknown
 | 
				
			||||||
 | 
						1, Standard output sensitivity
 | 
				
			||||||
 | 
						2, Recommended exposure index
 | 
				
			||||||
 | 
						3, ISO speed
 | 
				
			||||||
 | 
						4, SOS and REI
 | 
				
			||||||
 | 
						5, SOS and ISO speed
 | 
				
			||||||
 | 
						6, REI and ISO speed
 | 
				
			||||||
 | 
						7, SOS and REI and ISO speed
 | 
				
			||||||
 | 
					34865, StandardOutputSensitivity
 | 
				
			||||||
 | 
					34866, RecommendedExposureIndex
 | 
				
			||||||
 | 
					34867, ISOSpeed
 | 
				
			||||||
 | 
					34868, ISOSpeedLatitudeyyy
 | 
				
			||||||
 | 
					34869, ISOSpeedLatitudezzz
 | 
				
			||||||
 | 
					36864, ExifVersion
 | 
				
			||||||
 | 
					36867, DateTimeOriginal
 | 
				
			||||||
 | 
					36868, DateTimeDigitized
 | 
				
			||||||
 | 
					36880, OffsetTime  # 2.31
 | 
				
			||||||
 | 
					36881, OffsetTimeOriginal  # 2.31
 | 
				
			||||||
 | 
					36882, OffsetTimeDigitized  # 2.31
 | 
				
			||||||
 | 
					37121, ComponentsConfiguration
 | 
				
			||||||
 | 
						0, Does not exist
 | 
				
			||||||
 | 
						1, Y
 | 
				
			||||||
 | 
						2, Cb
 | 
				
			||||||
 | 
						3, Cr
 | 
				
			||||||
 | 
						4, R
 | 
				
			||||||
 | 
						5, G
 | 
				
			||||||
 | 
						6, B
 | 
				
			||||||
 | 
					37122, CompressedBitsPerPixel
 | 
				
			||||||
 | 
					37377, ShutterSpeedValue
 | 
				
			||||||
 | 
					37378, ApertureValue
 | 
				
			||||||
 | 
					37379, BrightnessValue
 | 
				
			||||||
 | 
					37380, ExposureBiasValue
 | 
				
			||||||
 | 
					37381, MaxApertureValue
 | 
				
			||||||
 | 
					37382, SubjectDistance
 | 
				
			||||||
 | 
					37383, MeteringMode
 | 
				
			||||||
 | 
						0, Unknown
 | 
				
			||||||
 | 
						1, Average
 | 
				
			||||||
 | 
						2, CenterWeightedAverage
 | 
				
			||||||
 | 
						3, Spot
 | 
				
			||||||
 | 
						4, MultiSpot
 | 
				
			||||||
 | 
						5, Pattern
 | 
				
			||||||
 | 
						6, Partial
 | 
				
			||||||
 | 
						255, Other
 | 
				
			||||||
 | 
					37384, LightSource
 | 
				
			||||||
 | 
						0, Unknown
 | 
				
			||||||
 | 
						1, Daylight
 | 
				
			||||||
 | 
						2, Fluorescent
 | 
				
			||||||
 | 
						3, Tungsten (incandescent light)
 | 
				
			||||||
 | 
						4, Flash
 | 
				
			||||||
 | 
						9, Fine weather
 | 
				
			||||||
 | 
						10, Cloudy weather
 | 
				
			||||||
 | 
						11, Shade
 | 
				
			||||||
 | 
						12, Daylight fluorescent (D 5700 - 7100K)
 | 
				
			||||||
 | 
						13, Day white fluorescent (N 4600 - 5500K)
 | 
				
			||||||
 | 
						14, Cool white fluorescent (W 3800 - 4500K)
 | 
				
			||||||
 | 
						15, White fluorescent (WW 3250 - 3800K)
 | 
				
			||||||
 | 
						16, Warm white fluorescent (L 2600 - 3250K)
 | 
				
			||||||
 | 
						17, Standard light A
 | 
				
			||||||
 | 
						18, Standard light B
 | 
				
			||||||
 | 
						19, Standard light C
 | 
				
			||||||
 | 
						20, D55
 | 
				
			||||||
 | 
						21, D65
 | 
				
			||||||
 | 
						22, D75
 | 
				
			||||||
 | 
						23, D50
 | 
				
			||||||
 | 
						24, ISO studio tungsten
 | 
				
			||||||
 | 
						255, Other light source
 | 
				
			||||||
 | 
					37385, Flash
 | 
				
			||||||
 | 
					37386, FocalLength
 | 
				
			||||||
 | 
					37396, SubjectArea
 | 
				
			||||||
 | 
					37500, MakerNote
 | 
				
			||||||
 | 
					# TODO(p): Decode.
 | 
				
			||||||
 | 
					37510, UserComment
 | 
				
			||||||
 | 
					37520, SubSecTime
 | 
				
			||||||
 | 
					37521, SubSecTimeOriginal
 | 
				
			||||||
 | 
					37522, SubSecTimeDigitized
 | 
				
			||||||
 | 
					37888, Temperature  # 2.31
 | 
				
			||||||
 | 
					37889, Humidity  # 2.31
 | 
				
			||||||
 | 
					37890, Pressure  # 2.31
 | 
				
			||||||
 | 
					37891, WaterDepth  # 2.31
 | 
				
			||||||
 | 
					37892, Acceleration  # 2.31
 | 
				
			||||||
 | 
					37893, CameraElevationAngle  # 2.31
 | 
				
			||||||
 | 
					40960, FlashpixVersion
 | 
				
			||||||
 | 
					40961, ColorSpace
 | 
				
			||||||
 | 
						1, sRGB
 | 
				
			||||||
 | 
						65535, Uncalibrated
 | 
				
			||||||
 | 
					40962, PixelXDimension
 | 
				
			||||||
 | 
					40963, PixelYDimension
 | 
				
			||||||
 | 
					40964, RelatedSoundFile
 | 
				
			||||||
 | 
					40965, Interoperability IFD Pointer
 | 
				
			||||||
 | 
					41483, FlashEnergy
 | 
				
			||||||
 | 
					41484, SpatialFrequencyResponse
 | 
				
			||||||
 | 
					41486, FocalPlaneXResolution
 | 
				
			||||||
 | 
					41487, FocalPlaneYResolution
 | 
				
			||||||
 | 
					41488, FocalPlaneResolutionUnit
 | 
				
			||||||
 | 
					41492, SubjectLocation
 | 
				
			||||||
 | 
					41493, ExposureIndex
 | 
				
			||||||
 | 
					41495, SensingMethod
 | 
				
			||||||
 | 
						1, Not defined
 | 
				
			||||||
 | 
						2, One-chip color area sensor
 | 
				
			||||||
 | 
						3, Two-chip color area sensor
 | 
				
			||||||
 | 
						4, Three-chip color area sensor
 | 
				
			||||||
 | 
						5, Color sequential area sensor
 | 
				
			||||||
 | 
						7, Trilinear sensor
 | 
				
			||||||
 | 
						8, Color sequential linear sensor
 | 
				
			||||||
 | 
					41728, FileSource
 | 
				
			||||||
 | 
						0, Others
 | 
				
			||||||
 | 
						1, Scanner of transparent type
 | 
				
			||||||
 | 
						2, Scanner of reflex type
 | 
				
			||||||
 | 
						3, DSC
 | 
				
			||||||
 | 
					41729, SceneType
 | 
				
			||||||
 | 
						1, Directly-photographed image
 | 
				
			||||||
 | 
					41730, CFAPattern
 | 
				
			||||||
 | 
					41985, CustomRendered
 | 
				
			||||||
 | 
						0, Normal process
 | 
				
			||||||
 | 
						1, Custom process
 | 
				
			||||||
 | 
					41986, ExposureMode
 | 
				
			||||||
 | 
						0, Auto exposure
 | 
				
			||||||
 | 
						1, Manual exposure
 | 
				
			||||||
 | 
						2, Auto bracket
 | 
				
			||||||
 | 
					41987, WhiteBalance
 | 
				
			||||||
 | 
						0, Auto white balance
 | 
				
			||||||
 | 
						1, Manual white balance
 | 
				
			||||||
 | 
					41988, DigitalZoomRatio
 | 
				
			||||||
 | 
					41989, FocalLengthIn35mmFilm
 | 
				
			||||||
 | 
					41990, SceneCaptureType
 | 
				
			||||||
 | 
						0, Standard
 | 
				
			||||||
 | 
						1, Landscape
 | 
				
			||||||
 | 
						2, Portrait
 | 
				
			||||||
 | 
						3, Night scene
 | 
				
			||||||
 | 
					41991, GainControl
 | 
				
			||||||
 | 
						0, None
 | 
				
			||||||
 | 
						1, Low gain up
 | 
				
			||||||
 | 
						2, High gain up
 | 
				
			||||||
 | 
						3, Low gain down
 | 
				
			||||||
 | 
						4, High gain down
 | 
				
			||||||
 | 
					41992, Contrast
 | 
				
			||||||
 | 
						0, Normal
 | 
				
			||||||
 | 
						1, Soft
 | 
				
			||||||
 | 
						2, Hard
 | 
				
			||||||
 | 
					41993, Saturation
 | 
				
			||||||
 | 
						0, Normal
 | 
				
			||||||
 | 
						1, Low
 | 
				
			||||||
 | 
						2, High
 | 
				
			||||||
 | 
					41994, Sharpness
 | 
				
			||||||
 | 
						0, Normal
 | 
				
			||||||
 | 
						1, Soft
 | 
				
			||||||
 | 
						2, Hard
 | 
				
			||||||
 | 
					41995, DeviceSettingDescription
 | 
				
			||||||
 | 
					41996, SubjectDistanceRange
 | 
				
			||||||
 | 
						0, Unknown
 | 
				
			||||||
 | 
						1, Macro
 | 
				
			||||||
 | 
						2, Close view
 | 
				
			||||||
 | 
						3, Distant view
 | 
				
			||||||
 | 
					42016, ImageUniqueID
 | 
				
			||||||
 | 
					42032, CameraOwnerName
 | 
				
			||||||
 | 
					42033, BodySerialNumber
 | 
				
			||||||
 | 
					42034, LensSpecification
 | 
				
			||||||
 | 
					42035, LensMake
 | 
				
			||||||
 | 
					42036, LensModel
 | 
				
			||||||
 | 
					42037, LensSerialNumber
 | 
				
			||||||
 | 
					42080, CompositeImage  # 2.32
 | 
				
			||||||
 | 
					42081, SourceImageNumberOfCompositeImage  # 2.32
 | 
				
			||||||
 | 
					42082, SourceExposureTimesOfCompositeImage  # 2.32
 | 
				
			||||||
 | 
					42240, Gamma
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Exif 2.3 4.6.6 (Notice it starts at 0.)
 | 
				
			||||||
 | 
					= Exif GPS
 | 
				
			||||||
 | 
					0, GPSVersionID
 | 
				
			||||||
 | 
					1, GPSLatitudeRef
 | 
				
			||||||
 | 
					2, GPSLatitude
 | 
				
			||||||
 | 
					3, GPSLongitudeRef
 | 
				
			||||||
 | 
					4, GPSLongitude
 | 
				
			||||||
 | 
					5, GPSAltitudeRef
 | 
				
			||||||
 | 
						0, Sea level
 | 
				
			||||||
 | 
						1, Sea level reference (negative value)
 | 
				
			||||||
 | 
					6, GPSAltitude
 | 
				
			||||||
 | 
					7, GPSTimeStamp
 | 
				
			||||||
 | 
					8, GPSSatellites
 | 
				
			||||||
 | 
					9, GPSStatus
 | 
				
			||||||
 | 
					10, GPSMeasureMode
 | 
				
			||||||
 | 
					11, GPSDOP
 | 
				
			||||||
 | 
					12, GPSSpeedRef
 | 
				
			||||||
 | 
					13, GPSSpeed
 | 
				
			||||||
 | 
					14, GPSTrackRef
 | 
				
			||||||
 | 
					15, GPSTrack
 | 
				
			||||||
 | 
					16, GPSImgDirectionRef
 | 
				
			||||||
 | 
					17, GPSImgDirection
 | 
				
			||||||
 | 
					18, GPSMapDatum
 | 
				
			||||||
 | 
					19, GPSDestLatitudeRef
 | 
				
			||||||
 | 
					20, GPSDestLatitude
 | 
				
			||||||
 | 
					21, GPSDestLongitudeRef
 | 
				
			||||||
 | 
					22, GPSDestLongitude
 | 
				
			||||||
 | 
					23, GPSDestBearingRef
 | 
				
			||||||
 | 
					24, GPSDestBearing
 | 
				
			||||||
 | 
					25, GPSDestDistanceRef
 | 
				
			||||||
 | 
					26, GPSDestDistance
 | 
				
			||||||
 | 
					27, GPSProcessingMethod
 | 
				
			||||||
 | 
					28, GPSAreaInformation
 | 
				
			||||||
 | 
					29, GPSDateStamp
 | 
				
			||||||
 | 
					30, GPSDifferential
 | 
				
			||||||
 | 
						0, Measurement without differential correction
 | 
				
			||||||
 | 
						1, Differential correction applied
 | 
				
			||||||
 | 
					31, GPSHPositioningError
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# Exif 2.3 4.6.7 (Notice it starts at 1, and collides with GPS.)
 | 
				
			||||||
 | 
					= Exif Interoperability
 | 
				
			||||||
 | 
					1, InteroperabilityIndex
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					# CIPA DC-007-2021 5.2.3., 5.2.4. (But derive "field names" from "tag names".)
 | 
				
			||||||
 | 
					= MPF
 | 
				
			||||||
 | 
					45056, MP Format Version Number  # MPFVersion
 | 
				
			||||||
 | 
					45057, Number of Images  # NumberOfImages
 | 
				
			||||||
 | 
					45058, MP Entry  # MPEntry
 | 
				
			||||||
 | 
					45059, Individual Image Unique ID List  # ImageUIDList
 | 
				
			||||||
 | 
					45060, Total Number of Captured Frames  # TotalFrames
 | 
				
			||||||
 | 
					45313, MP Individual Image Number  # MPIndividualNum
 | 
				
			||||||
 | 
					45569, Panorama Scanning Orientation  # PanOrientation
 | 
				
			||||||
 | 
					45570, Panorama Horizontal Overlap  # PanOverlap_H
 | 
				
			||||||
 | 
					45571, Panorama Vertical Overlap  # PanOverlap_V
 | 
				
			||||||
 | 
					45572, Base Viewpoint Number  # BaseViewpointNum
 | 
				
			||||||
 | 
					45573, Convergence Angle  # ConvergenceAngle
 | 
				
			||||||
 | 
					45574, Baseline Length  # BaselineLength
 | 
				
			||||||
 | 
					45575, Divergence Angle  # VerticalDivergence
 | 
				
			||||||
 | 
					45576, Horizontal Axis Distance  # AxisDistance_X
 | 
				
			||||||
 | 
					45577, Vertical Axis Distance  # AxisDistance_Y
 | 
				
			||||||
 | 
					45578, Collimation Axis Distance  # AxisDistance_Z
 | 
				
			||||||
 | 
					45579, Yaw Angle  # YawAngle
 | 
				
			||||||
 | 
					45580, Pitch Angle  # PitchAngle
 | 
				
			||||||
 | 
					45581, Roll Angle  # RollAngle
 | 
				
			||||||
							
								
								
									
										356
									
								
								tiffer.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,356 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// tiffer.h: TIFF reading utilities
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <stdbool.h>
 | 
				
			||||||
 | 
					#include <stddef.h>
 | 
				
			||||||
 | 
					#include <stdint.h>
 | 
				
			||||||
 | 
					#include <string.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Utilities ---------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static uint64_t
 | 
				
			||||||
 | 
					tiffer_u64be(const uint8_t *p)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return (uint64_t) p[0] << 56 | (uint64_t) p[1] << 48 |
 | 
				
			||||||
 | 
							(uint64_t) p[2] << 40 | (uint64_t) p[3] << 32 |
 | 
				
			||||||
 | 
							(uint64_t) p[4] << 24 | p[5] << 16 | p[6] << 8 | p[7];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static uint32_t
 | 
				
			||||||
 | 
					tiffer_u32be(const uint8_t *p)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return (uint32_t) p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static uint16_t
 | 
				
			||||||
 | 
					tiffer_u16be(const uint8_t *p)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return (uint16_t) p[0] << 8 | p[1];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static uint64_t
 | 
				
			||||||
 | 
					tiffer_u64le(const uint8_t *p)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return (uint64_t) p[7] << 56 | (uint64_t) p[6] << 48 |
 | 
				
			||||||
 | 
							(uint64_t) p[5] << 40 | (uint64_t) p[4] << 32 |
 | 
				
			||||||
 | 
							(uint64_t) p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static uint32_t
 | 
				
			||||||
 | 
					tiffer_u32le(const uint8_t *p)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return (uint32_t) p[3] << 24 | p[2] << 16 | p[1] << 8 | p[0];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static uint16_t
 | 
				
			||||||
 | 
					tiffer_u16le(const uint8_t *p)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return (uint16_t) p[1] << 8 | p[0];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- TIFF --------------------------------------------------------------------
 | 
				
			||||||
 | 
					// libtiff is a mess, and the format is not particularly complicated.
 | 
				
			||||||
 | 
					// Exiv2 is senselessly copylefted, and cannot do much.
 | 
				
			||||||
 | 
					// libexif is only marginally better.
 | 
				
			||||||
 | 
					// ExifTool is too user-oriented.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct un {
 | 
				
			||||||
 | 
						uint64_t (*u64) (const uint8_t *);
 | 
				
			||||||
 | 
						uint32_t (*u32) (const uint8_t *);
 | 
				
			||||||
 | 
						uint16_t (*u16) (const uint8_t *);
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static struct un tiffer_unbe = {tiffer_u64be, tiffer_u32be, tiffer_u16be};
 | 
				
			||||||
 | 
					static struct un tiffer_unle = {tiffer_u64le, tiffer_u32le, tiffer_u16le};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct tiffer {
 | 
				
			||||||
 | 
						struct un *un;
 | 
				
			||||||
 | 
						const uint8_t *begin, *p, *end;
 | 
				
			||||||
 | 
						uint16_t remaining_fields;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_u32(struct tiffer *self, uint32_t *u)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (self->end - self->p < 4)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*u = self->un->u32(self->p);
 | 
				
			||||||
 | 
						self->p += 4;
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_u16(struct tiffer *self, uint16_t *u)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (self->end - self->p < 2)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*u = self->un->u16(self->p);
 | 
				
			||||||
 | 
						self->p += 2;
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_init(struct tiffer *self, const uint8_t *tiff, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						self->un = NULL;
 | 
				
			||||||
 | 
						self->begin = self->p = tiff;
 | 
				
			||||||
 | 
						self->end = tiff + len;
 | 
				
			||||||
 | 
						self->remaining_fields = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const uint8_t
 | 
				
			||||||
 | 
							le[4] = {'I', 'I', 42, 0},
 | 
				
			||||||
 | 
							be[4] = {'M', 'M', 0, 42};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if (tiff + 8 > self->end)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						else if (!memcmp(tiff, le, sizeof le))
 | 
				
			||||||
 | 
							self->un = &tiffer_unle;
 | 
				
			||||||
 | 
						else if (!memcmp(tiff, be, sizeof be))
 | 
				
			||||||
 | 
							self->un = &tiffer_unbe;
 | 
				
			||||||
 | 
						else
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						self->p = tiff + 4;
 | 
				
			||||||
 | 
						// The first IFD needs to be read by caller explicitly,
 | 
				
			||||||
 | 
						// even though it's required to be present by TIFF 6.0.
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Read the next IFD in a sequence.
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_next_ifd(struct tiffer *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// All fields from any previous IFD need to be read first.
 | 
				
			||||||
 | 
						if (self->remaining_fields)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uint32_t ifd_offset = 0;
 | 
				
			||||||
 | 
						if (!tiffer_u32(self, &ifd_offset))
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// There is nothing more to read, this chain has terminated.
 | 
				
			||||||
 | 
						if (!ifd_offset)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Note that TIFF 6.0 requires there to be at least one entry,
 | 
				
			||||||
 | 
						// but there is no need for us to check it.
 | 
				
			||||||
 | 
						self->p = self->begin + ifd_offset;
 | 
				
			||||||
 | 
						return tiffer_u16(self, &self->remaining_fields);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static size_t
 | 
				
			||||||
 | 
					tiffer_length(const struct tiffer *self)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return self->begin > self->end ? 0 : self->end - self->begin;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Initialize a derived TIFF reader for a subIFD at the given location.
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_subifd(
 | 
				
			||||||
 | 
						const struct tiffer *self, uint32_t offset, struct tiffer *subreader)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (tiffer_length(self) < offset)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						*subreader = *self;
 | 
				
			||||||
 | 
						subreader->p = subreader->begin + offset;
 | 
				
			||||||
 | 
						return tiffer_u16(subreader, &subreader->remaining_fields);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum tiffer_type {
 | 
				
			||||||
 | 
						TIFFER_BYTE = 1, TIFFER_ASCII, TIFFER_SHORT, TIFFER_LONG,
 | 
				
			||||||
 | 
						TIFFER_RATIONAL,
 | 
				
			||||||
 | 
						TIFFER_SBYTE, TIFFER_UNDEFINED, TIFFER_SSHORT, TIFFER_SLONG,
 | 
				
			||||||
 | 
						TIFFER_SRATIONAL,
 | 
				
			||||||
 | 
						TIFFER_FLOAT,
 | 
				
			||||||
 | 
						TIFFER_DOUBLE,
 | 
				
			||||||
 | 
						// This last type from TIFF Technical Note 1 isn't really used much.
 | 
				
			||||||
 | 
						TIFFER_IFD,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static size_t
 | 
				
			||||||
 | 
					tiffer_value_size(enum tiffer_type type)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						switch (type) {
 | 
				
			||||||
 | 
						case TIFFER_BYTE:
 | 
				
			||||||
 | 
						case TIFFER_SBYTE:
 | 
				
			||||||
 | 
						case TIFFER_ASCII:
 | 
				
			||||||
 | 
						case TIFFER_UNDEFINED:
 | 
				
			||||||
 | 
							return 1;
 | 
				
			||||||
 | 
						case TIFFER_SHORT:
 | 
				
			||||||
 | 
						case TIFFER_SSHORT:
 | 
				
			||||||
 | 
							return 2;
 | 
				
			||||||
 | 
						case TIFFER_LONG:
 | 
				
			||||||
 | 
						case TIFFER_SLONG:
 | 
				
			||||||
 | 
						case TIFFER_FLOAT:
 | 
				
			||||||
 | 
						case TIFFER_IFD:
 | 
				
			||||||
 | 
							return 4;
 | 
				
			||||||
 | 
						case TIFFER_RATIONAL:
 | 
				
			||||||
 | 
						case TIFFER_SRATIONAL:
 | 
				
			||||||
 | 
						case TIFFER_DOUBLE:
 | 
				
			||||||
 | 
							return 8;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return 0;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A lean iterator for values within entries.
 | 
				
			||||||
 | 
					struct tiffer_entry {
 | 
				
			||||||
 | 
						uint16_t tag;
 | 
				
			||||||
 | 
						enum tiffer_type type;
 | 
				
			||||||
 | 
						// For {S,}BYTE, ASCII, UNDEFINED, use these fields directly.
 | 
				
			||||||
 | 
						const uint8_t *p;
 | 
				
			||||||
 | 
						uint32_t remaining_count;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_next_value(struct tiffer_entry *entry)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!entry->remaining_count)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						entry->p += tiffer_value_size(entry->type);
 | 
				
			||||||
 | 
						entry->remaining_count--;
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_integer(
 | 
				
			||||||
 | 
						const struct tiffer *self, const struct tiffer_entry *entry, int64_t *out)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!entry->remaining_count)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Somewhat excessively lenient, intended for display.
 | 
				
			||||||
 | 
						// TIFF 6.0 only directly suggests that a reader is should accept
 | 
				
			||||||
 | 
						// any of BYTE/SHORT/LONG for unsigned integers.
 | 
				
			||||||
 | 
						switch (entry->type) {
 | 
				
			||||||
 | 
						case TIFFER_BYTE:
 | 
				
			||||||
 | 
						case TIFFER_ASCII:
 | 
				
			||||||
 | 
						case TIFFER_UNDEFINED:
 | 
				
			||||||
 | 
							*out = *entry->p;
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						case TIFFER_SBYTE:
 | 
				
			||||||
 | 
							*out = (int8_t) *entry->p;
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						case TIFFER_SHORT:
 | 
				
			||||||
 | 
							*out = self->un->u16(entry->p);
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						case TIFFER_SSHORT:
 | 
				
			||||||
 | 
							*out = (int16_t) self->un->u16(entry->p);
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						case TIFFER_LONG:
 | 
				
			||||||
 | 
						case TIFFER_IFD:
 | 
				
			||||||
 | 
							*out = self->un->u32(entry->p);
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						case TIFFER_SLONG:
 | 
				
			||||||
 | 
							*out = (int32_t) self->un->u32(entry->p);
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_rational(const struct tiffer *self, const struct tiffer_entry *entry,
 | 
				
			||||||
 | 
						int64_t *numerator, int64_t *denominator)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!entry->remaining_count)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Somewhat excessively lenient, intended for display.
 | 
				
			||||||
 | 
						switch (entry->type) {
 | 
				
			||||||
 | 
						case TIFFER_RATIONAL:
 | 
				
			||||||
 | 
							*numerator = self->un->u32(entry->p);
 | 
				
			||||||
 | 
							*denominator = self->un->u32(entry->p + 4);
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						case TIFFER_SRATIONAL:
 | 
				
			||||||
 | 
							*numerator = (int32_t) self->un->u32(entry->p);
 | 
				
			||||||
 | 
							*denominator = (int32_t) self->un->u32(entry->p + 4);
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							if (tiffer_integer(self, entry, numerator)) {
 | 
				
			||||||
 | 
								*denominator = 1;
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_real(
 | 
				
			||||||
 | 
						const struct tiffer *self, const struct tiffer_entry *entry, double *out)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!entry->remaining_count)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Somewhat excessively lenient, intended for display.
 | 
				
			||||||
 | 
						// Assuming the host architecture uses IEEE 754.
 | 
				
			||||||
 | 
						switch (entry->type) {
 | 
				
			||||||
 | 
							int64_t numerator, denominator;
 | 
				
			||||||
 | 
						case TIFFER_FLOAT:
 | 
				
			||||||
 | 
							*out = *(float *) entry->p;
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						case TIFFER_DOUBLE:
 | 
				
			||||||
 | 
							*out = *(double *) entry->p;
 | 
				
			||||||
 | 
							return true;
 | 
				
			||||||
 | 
						default:
 | 
				
			||||||
 | 
							if (tiffer_rational(self, entry, &numerator, &denominator)) {
 | 
				
			||||||
 | 
								*out = (double) numerator / denominator;
 | 
				
			||||||
 | 
								return true;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					tiffer_next_entry(struct tiffer *self, struct tiffer_entry *entry)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!self->remaining_fields)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uint16_t type = entry->type = 0xFFFF;
 | 
				
			||||||
 | 
						if (!tiffer_u16(self, &entry->tag) || !tiffer_u16(self, &type) ||
 | 
				
			||||||
 | 
							!tiffer_u32(self, &entry->remaining_count))
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Short values may and will be inlined, rather than pointed to.
 | 
				
			||||||
 | 
						size_t values_size = tiffer_value_size(type) * entry->remaining_count;
 | 
				
			||||||
 | 
						uint32_t offset = 0;
 | 
				
			||||||
 | 
						if (values_size <= sizeof offset) {
 | 
				
			||||||
 | 
							entry->p = self->p;
 | 
				
			||||||
 | 
							self->p += sizeof offset;
 | 
				
			||||||
 | 
						} else if (tiffer_u32(self, &offset) && tiffer_length(self) >= offset) {
 | 
				
			||||||
 | 
							entry->p = self->begin + offset;
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// All entries are pre-checked not to overflow.
 | 
				
			||||||
 | 
						if (values_size > PTRDIFF_MAX ||
 | 
				
			||||||
 | 
							self->end - entry->p < (ptrdiff_t) values_size)
 | 
				
			||||||
 | 
							return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Setting it at the end may provide an indication while debugging.
 | 
				
			||||||
 | 
						entry->type = type;
 | 
				
			||||||
 | 
						self->remaining_fields--;
 | 
				
			||||||
 | 
						return true;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										3
									
								
								tools/.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -1,3 +0,0 @@
 | 
				
			|||||||
/pnginfo
 | 
					 | 
				
			||||||
/jpeginfo
 | 
					 | 
				
			||||||
/tiffinfo
 | 
					 | 
				
			||||||
@@ -1,15 +0,0 @@
 | 
				
			|||||||
SHELL = /bin/sh
 | 
					 | 
				
			||||||
# libjq 1.6 lacks a pkg-config file, and there is no release in sight.
 | 
					 | 
				
			||||||
# libjq 1.6 is required.
 | 
					 | 
				
			||||||
CFLAGS = -g -O2 -Wall -Wextra `pkg-config --cflags $(deps)`
 | 
					 | 
				
			||||||
LDLIBS = -ljq `pkg-config --libs $(deps)`
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
deps = libpng
 | 
					 | 
				
			||||||
targets = pnginfo jpeginfo tiffinfo
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
all: $(targets)
 | 
					 | 
				
			||||||
$(targets): info.h
 | 
					 | 
				
			||||||
clean:
 | 
					 | 
				
			||||||
	rm -f $(targets)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.PHONY: all clean
 | 
					 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
//
 | 
					//
 | 
				
			||||||
// fastiv-io-benchmark.c: see if we're worth the name
 | 
					// benchmark-io.c: measure and compare image loading times
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
@@ -15,11 +15,11 @@
 | 
				
			|||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
//
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include <gdk/gdk.h>
 | 
					 | 
				
			||||||
#include <gdk-pixbuf/gdk-pixbuf.h>
 | 
					#include <gdk-pixbuf/gdk-pixbuf.h>
 | 
				
			||||||
 | 
					#include <gdk/gdk.h>
 | 
				
			||||||
#include <time.h>
 | 
					#include <time.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#include "fastiv-io.h"
 | 
					#include "fiv-io.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static double
 | 
					static double
 | 
				
			||||||
timestamp(void)
 | 
					timestamp(void)
 | 
				
			||||||
@@ -32,26 +32,36 @@ timestamp(void)
 | 
				
			|||||||
static void
 | 
					static void
 | 
				
			||||||
one_file(const char *filename)
 | 
					one_file(const char *filename)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	double since_us = timestamp();
 | 
						GFile *file = g_file_new_for_commandline_arg(filename);
 | 
				
			||||||
	cairo_surface_t *loaded_by_us = fastiv_io_open(filename, NULL);
 | 
						double since_us = timestamp(), us = 0;
 | 
				
			||||||
 | 
						FivIoOpenContext ctx = {
 | 
				
			||||||
 | 
							.uri = g_file_get_uri(file),
 | 
				
			||||||
 | 
							.screen_dpi = 96,
 | 
				
			||||||
 | 
							// Only using this array as a redirect.
 | 
				
			||||||
 | 
							.warnings = g_ptr_array_new_with_free_func(g_free),
 | 
				
			||||||
 | 
						};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						FivIoImage *loaded_by_us = fiv_io_open(&ctx, NULL);
 | 
				
			||||||
 | 
						g_clear_object(&file);
 | 
				
			||||||
 | 
						g_free((char *) ctx.uri);
 | 
				
			||||||
 | 
						g_ptr_array_free(ctx.warnings, TRUE);
 | 
				
			||||||
	if (!loaded_by_us)
 | 
						if (!loaded_by_us)
 | 
				
			||||||
		return;
 | 
							return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cairo_surface_destroy(loaded_by_us);
 | 
						fiv_io_image_unref(loaded_by_us);
 | 
				
			||||||
	double us = timestamp() - since_us;
 | 
						us = timestamp() - since_us;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	double since_pixbuf = timestamp();
 | 
						double since_pixbuf = timestamp(), pixbuf = 0;
 | 
				
			||||||
	GdkPixbuf *gdk_pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
 | 
						GdkPixbuf *gdk_pixbuf = gdk_pixbuf_new_from_file(filename, NULL);
 | 
				
			||||||
	if (!gdk_pixbuf)
 | 
						if (gdk_pixbuf) {
 | 
				
			||||||
		return;
 | 
							cairo_surface_t *loaded_by_pixbuf =
 | 
				
			||||||
 | 
								gdk_cairo_surface_create_from_pixbuf(gdk_pixbuf, 1, NULL);
 | 
				
			||||||
 | 
							g_object_unref(gdk_pixbuf);
 | 
				
			||||||
 | 
							cairo_surface_destroy(loaded_by_pixbuf);
 | 
				
			||||||
 | 
							pixbuf = timestamp() - since_pixbuf;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	cairo_surface_t *loaded_by_pixbuf =
 | 
						printf("%.3f\t%.3f\t%.0f%%\t%s\n", us, pixbuf, us / pixbuf * 100, filename);
 | 
				
			||||||
		gdk_cairo_surface_create_from_pixbuf(gdk_pixbuf, 1, NULL);
 | 
					 | 
				
			||||||
	g_object_unref(gdk_pixbuf);
 | 
					 | 
				
			||||||
	cairo_surface_destroy(loaded_by_pixbuf);
 | 
					 | 
				
			||||||
	double pixbuf = timestamp() - since_pixbuf;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	printf("%f\t%f\t%.0f%%\t%s\n", us, pixbuf, us / pixbuf * 100, filename);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
int
 | 
					int
 | 
				
			||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
#!/bin/sh -e
 | 
					#!/bin/sh -e
 | 
				
			||||||
# Remove thumbnails with URIs pointing to at this moment non-existing files.
 | 
					# Remove thumbnails with URIs pointing to at this moment non-existing files.
 | 
				
			||||||
make pnginfo
 | 
					ninja pnginfo
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pnginfo=$(pwd)/pnginfo cache_home=${XDG_CACHE_HOME:-$HOME/.cache}
 | 
					pnginfo=$(pwd)/pnginfo cache_home=${XDG_CACHE_HOME:-$HOME/.cache}
 | 
				
			||||||
for size in normal large x-large xx-large; do
 | 
					for size in normal large x-large xx-large; do
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										210
									
								
								tools/hotpixels.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,210 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// hotpixels.c: look for hot pixels in raw image files
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Usage: pass a bunch of raw photo images taken with the lens cap on at,
 | 
				
			||||||
 | 
					// e.g., ISO 8000-12800 @ 1/20-1/60, and store the resulting file as,
 | 
				
			||||||
 | 
					// e.g., Nikon D7500.badpixels, which can then be directly used by Rawtherapee.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <libraw.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
 | 
				
			||||||
 | 
					#error LibRaw 0.21.0 or newer is required.
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <errno.h>
 | 
				
			||||||
 | 
					#include <stdbool.h>
 | 
				
			||||||
 | 
					#include <stdio.h>
 | 
				
			||||||
 | 
					#include <stdlib.h>
 | 
				
			||||||
 | 
					#include <string.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void *
 | 
				
			||||||
 | 
					xreallocarray(void *o, size_t n, size_t m)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (m && n > SIZE_MAX / m) {
 | 
				
			||||||
 | 
							fprintf(stderr, "xreallocarray: %s\n", strerror(ENOMEM));
 | 
				
			||||||
 | 
							exit(EXIT_FAILURE);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						void *p = realloc(o, n * m);
 | 
				
			||||||
 | 
						if (!p && n && m) {
 | 
				
			||||||
 | 
							fprintf(stderr, "xreallocarray: %s\n", strerror(errno));
 | 
				
			||||||
 | 
							exit(EXIT_FAILURE);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return p;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct coord { ushort x, y; };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					coord_equals(struct coord a, struct coord b)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return a.x == b.x && a.y == b.y;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static int
 | 
				
			||||||
 | 
					coord_cmp(const void *a, const void *b)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						const struct coord *ca = (const struct coord *) a;
 | 
				
			||||||
 | 
						const struct coord *cb = (const struct coord *) b;
 | 
				
			||||||
 | 
						return ca->y != cb->y
 | 
				
			||||||
 | 
							? (int) ca->y - (int) cb->y
 | 
				
			||||||
 | 
							: (int) ca->x - (int) cb->x;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct candidates {
 | 
				
			||||||
 | 
						struct coord *xy;
 | 
				
			||||||
 | 
						size_t len;
 | 
				
			||||||
 | 
						size_t alloc;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static void
 | 
				
			||||||
 | 
					candidates_add(struct candidates *c, ushort x, ushort y)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (c->len == c->alloc) {
 | 
				
			||||||
 | 
							c->alloc += 64;
 | 
				
			||||||
 | 
							c->xy = xreallocarray(c->xy, sizeof *c->xy, c->alloc);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						c->xy[c->len++] = (struct coord) {x, y};
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// A stretch of zeroes that is assumed to mean start of outliers.
 | 
				
			||||||
 | 
					#define SPAN 10
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *
 | 
				
			||||||
 | 
					process_raw(struct candidates *c, const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
 | 
				
			||||||
 | 
						if (!iprc)
 | 
				
			||||||
 | 
							return "failed to obtain a LibRaw handle";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						int err = 0;
 | 
				
			||||||
 | 
						if ((err = libraw_open_buffer(iprc, p, len)) ||
 | 
				
			||||||
 | 
							(err = libraw_unpack(iprc))) {
 | 
				
			||||||
 | 
							libraw_close(iprc);
 | 
				
			||||||
 | 
							return libraw_strerror(err);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (!iprc->rawdata.raw_image) {
 | 
				
			||||||
 | 
							libraw_close(iprc);
 | 
				
			||||||
 | 
							return "only Bayer raws are supported, not Foveon";
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Make a histogram.
 | 
				
			||||||
 | 
						uint64_t bins[USHRT_MAX] = {};
 | 
				
			||||||
 | 
						for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
 | 
				
			||||||
 | 
							for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
 | 
				
			||||||
 | 
								ushort y = iprc->sizes.top_margin + yy;
 | 
				
			||||||
 | 
								ushort x = iprc->sizes.left_margin + xx;
 | 
				
			||||||
 | 
								bins[iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x]]++;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Detecting outliers is not completely straight-forward,
 | 
				
			||||||
 | 
						// it may help to see the histogram.
 | 
				
			||||||
 | 
						if (getenv("HOTPIXELS_HISTOGRAM")) {
 | 
				
			||||||
 | 
							for (ushort i = 0; i < USHRT_MAX; i++)
 | 
				
			||||||
 | 
								fprintf(stderr, "%u ", (unsigned) bins[i]);
 | 
				
			||||||
 | 
							fputc('\n', stderr);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Go to the first non-zero pixel value.
 | 
				
			||||||
 | 
						size_t last = 0;
 | 
				
			||||||
 | 
						for (; last < USHRT_MAX; last++)
 | 
				
			||||||
 | 
							if (bins[last])
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Find the last pixel value we assume to not be hot.
 | 
				
			||||||
 | 
						for (; last < USHRT_MAX - SPAN - 1; last++) {
 | 
				
			||||||
 | 
							uint64_t nonzero = 0;
 | 
				
			||||||
 | 
							for (int i = 1; i <= SPAN; i++)
 | 
				
			||||||
 | 
								nonzero += bins[last + i];
 | 
				
			||||||
 | 
							if (!nonzero)
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Store coordinates for all pixels above that value.
 | 
				
			||||||
 | 
						for (ushort yy = 0; yy < iprc->sizes.height; yy++) {
 | 
				
			||||||
 | 
							for (ushort xx = 0; xx < iprc->sizes.width; xx++) {
 | 
				
			||||||
 | 
								ushort y = iprc->sizes.top_margin + yy;
 | 
				
			||||||
 | 
								ushort x = iprc->sizes.left_margin + xx;
 | 
				
			||||||
 | 
								if (iprc->rawdata.raw_image[y * iprc->sizes.raw_width + x] > last)
 | 
				
			||||||
 | 
									candidates_add(c, xx, yy);
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						libraw_close(iprc);
 | 
				
			||||||
 | 
						return NULL;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static const char *
 | 
				
			||||||
 | 
					do_file(struct candidates *c, const char *filename)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						FILE *fp = fopen(filename, "rb");
 | 
				
			||||||
 | 
						if (!fp)
 | 
				
			||||||
 | 
							return strerror(errno);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uint8_t *data = NULL, buf[256 << 10];
 | 
				
			||||||
 | 
						size_t n, len = 0;
 | 
				
			||||||
 | 
						while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
 | 
				
			||||||
 | 
							data = xreallocarray(data, len + n, 1);
 | 
				
			||||||
 | 
							memcpy(data + len, buf, n);
 | 
				
			||||||
 | 
							len += n;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const char *err = ferror(fp)
 | 
				
			||||||
 | 
							? strerror(errno)
 | 
				
			||||||
 | 
							: process_raw(c, data, len);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						fclose(fp);
 | 
				
			||||||
 | 
						free(data);
 | 
				
			||||||
 | 
						return err;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int
 | 
				
			||||||
 | 
					main(int argc, char *argv[])
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						struct candidates c = {};
 | 
				
			||||||
 | 
						for (int i = 1; i < argc; i++) {
 | 
				
			||||||
 | 
							const char *filename = argv[i], *err = do_file(&c, filename);
 | 
				
			||||||
 | 
							if (err) {
 | 
				
			||||||
 | 
								fprintf(stderr, "%s: %s\n", filename, err);
 | 
				
			||||||
 | 
								return EXIT_FAILURE;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						qsort(c.xy, c.len, sizeof *c.xy, coord_cmp);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// If it is detected in all passed photos, it is probably indeed bad.
 | 
				
			||||||
 | 
						int count = 1;
 | 
				
			||||||
 | 
						for (size_t i = 1; i <= c.len; i++) {
 | 
				
			||||||
 | 
							if (i != c.len && coord_equals(c.xy[i - 1], c.xy[i])) {
 | 
				
			||||||
 | 
								count++;
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (count == argc - 1)
 | 
				
			||||||
 | 
								printf("%u %u\n", c.xy[i - 1].x, c.xy[i - 1].y);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							count = 1;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										286
									
								
								tools/info.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,286 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// info.c: acquire information about JPEG/TIFF/BMFF/WebP files in JSON format
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2021 - 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "info.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <jv.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <errno.h>
 | 
				
			||||||
 | 
					#include <stdio.h>
 | 
				
			||||||
 | 
					#include <stdlib.h>
 | 
				
			||||||
 | 
					#include <string.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- ISO/IEC base media file format ------------------------------------------
 | 
				
			||||||
 | 
					// ISO/IEC 14496-12:2015(E), used to be publicly available, now there's only:
 | 
				
			||||||
 | 
					// https://mpeg.chiariglione.org/standards/mpeg-4/iso-base-media-file-format/text-isoiec-14496-12-5th-edition
 | 
				
			||||||
 | 
					// but people have managed to archive the final version as well:
 | 
				
			||||||
 | 
					// https://b.goeswhere.com/ISO_IEC_14496-12_2015.pdf
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// ISO/IEC 23008-12:2017 Information technology -
 | 
				
			||||||
 | 
					// High efficiency coding and media delivery in heterogeneous environments -
 | 
				
			||||||
 | 
					// Part 12: Image File Format + Cor 1:2020 Technical Corrigendum 1
 | 
				
			||||||
 | 
					// https://standards.iso.org/ittf/PubliclyAvailableStandards/
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					parse_bmff_box(jv o, const char *type, const uint8_t *data, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// TODO(p): Parse out "uuid"'s uint8_t[16] initial field, present as hex.
 | 
				
			||||||
 | 
						// TODO(p): Parse out "ftyp" contents: 14496-12:2015 4.3
 | 
				
			||||||
 | 
						// TODO(p): Parse out other important boxes: 14496-12:2015 8+
 | 
				
			||||||
 | 
						return add_to_subarray(o, "boxes", jv_string(type));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					detect_bmff(const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// 4.2 Object Structure--this box need not be present, nor at the beginning
 | 
				
			||||||
 | 
						// TODO(p): What does `aligned(8)` mean? It's probably in bits.
 | 
				
			||||||
 | 
						return len >= 8 && !memcmp(p + 4, "ftyp", 4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					parse_bmff(jv o, const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!detect_bmff(p, len))
 | 
				
			||||||
 | 
							return add_error(o, "not BMFF at all or unsupported");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const uint8_t *end = p + len;
 | 
				
			||||||
 | 
						while (p < end) {
 | 
				
			||||||
 | 
							if (end - p < 8) {
 | 
				
			||||||
 | 
								o = add_warning(o, "box framing mismatch");
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							char type[5] = "";
 | 
				
			||||||
 | 
							memcpy(type, p + 4, 4);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							uint64_t box_size = u32be(p);
 | 
				
			||||||
 | 
							const uint8_t *data = p + 8;
 | 
				
			||||||
 | 
							if (box_size == 1) {
 | 
				
			||||||
 | 
								if (end - p < 16) {
 | 
				
			||||||
 | 
									o = add_warning(o, "unexpected EOF");
 | 
				
			||||||
 | 
									break;
 | 
				
			||||||
 | 
								}
 | 
				
			||||||
 | 
								box_size = u64be(data);
 | 
				
			||||||
 | 
								data += 8;
 | 
				
			||||||
 | 
							} else if (!box_size)
 | 
				
			||||||
 | 
								box_size = end - p;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (box_size > (uint64_t) (end - p)) {
 | 
				
			||||||
 | 
								o = add_warning(o, "unexpected EOF");
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							size_t data_len = box_size - (data - p);
 | 
				
			||||||
 | 
							o = parse_bmff_box(o, type, data, data_len);
 | 
				
			||||||
 | 
							p += box_size;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return o;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- WebP --------------------------------------------------------------------
 | 
				
			||||||
 | 
					// libwebp won't let us simply iterate over all chunks, so handroll it.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// https://github.com/webmproject/libwebp/blob/master/doc/webp-container-spec.txt
 | 
				
			||||||
 | 
					// https://github.com/webmproject/libwebp/blob/master/doc/webp-lossless-bitstream-spec.txt
 | 
				
			||||||
 | 
					// https://datatracker.ietf.org/doc/html/rfc6386
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Pretty versions, hopefully not outdated:
 | 
				
			||||||
 | 
					// https://developers.google.com/speed/webp/docs/riff_container
 | 
				
			||||||
 | 
					// https://developers.google.com/speed/webp/docs/webp_lossless_bitstream_specification
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static bool
 | 
				
			||||||
 | 
					detect_webp(const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						return len >= 12 && !memcmp(p, "RIFF", 4) && !memcmp(p + 8, "WEBP", 4);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					parse_webp_vp8(jv o, const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (len < 10 || (p[0] & 1) != 0 /* key frame */ ||
 | 
				
			||||||
 | 
							p[3] != 0x9d || p[4] != 0x01 || p[5] != 0x2a) {
 | 
				
			||||||
 | 
							return add_warning(o, "invalid VP8 chunk");
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("width"), jv_number(u16le(p + 6) & 0x3fff));
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("height"), jv_number(u16le(p + 8) & 0x3fff));
 | 
				
			||||||
 | 
						return o;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					parse_webp_vp8l(jv o, const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (len < 5 || p[0] != 0x2f)
 | 
				
			||||||
 | 
							return add_warning(o, "invalid VP8L chunk");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Reading LSB-first from a little endian value means reading in order.
 | 
				
			||||||
 | 
						uint32_t header = u32le(p + 1);
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("width"), jv_number((header & 0x3fff) + 1));
 | 
				
			||||||
 | 
						header >>= 14;
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("height"), jv_number((header & 0x3fff) + 1));
 | 
				
			||||||
 | 
						header >>= 14;
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("alpha_is_used"), jv_bool(header & 1));
 | 
				
			||||||
 | 
						return o;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					parse_webp_vp8x(jv o, const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (len < 10)
 | 
				
			||||||
 | 
							return add_warning(o, "invalid VP8X chunk");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// Most of the fields in this chunk are duplicate or inferrable.
 | 
				
			||||||
 | 
						// Probably not worth decoding or verifying.
 | 
				
			||||||
 | 
						// TODO(p): For animations, we need to use the width and height from here.
 | 
				
			||||||
 | 
						uint8_t flags = p[0];
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("animation"), jv_bool((flags >> 1) & 1));
 | 
				
			||||||
 | 
						return o;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					parse_webp(jv o, const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						if (!detect_webp(p, len))
 | 
				
			||||||
 | 
							return add_error(o, "not a WEBP file");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// TODO(p): This can still be parseable.
 | 
				
			||||||
 | 
						// TODO(p): Warn on trailing data.
 | 
				
			||||||
 | 
						uint32_t size = u32le(p + 4);
 | 
				
			||||||
 | 
						if (8 + size < len)
 | 
				
			||||||
 | 
							return add_error(o, "truncated file");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						const uint8_t *end = p + 8 + size;
 | 
				
			||||||
 | 
						p += 12;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jv chunks = jv_array();
 | 
				
			||||||
 | 
						while (p < end) {
 | 
				
			||||||
 | 
							if (end - p < 8) {
 | 
				
			||||||
 | 
								o = add_warning(o, "framing mismatch");
 | 
				
			||||||
 | 
								printf("%ld", end - p);
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							uint32_t chunk_size = u32le(p + 4);
 | 
				
			||||||
 | 
							uint32_t chunk_advance = (chunk_size + 1) & ~1;
 | 
				
			||||||
 | 
							if (p + 8 + chunk_advance > end) {
 | 
				
			||||||
 | 
								o = add_warning(o, "runaway chunk payload");
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							char fourcc[5] = "";
 | 
				
			||||||
 | 
							memcpy(fourcc, p, 4);
 | 
				
			||||||
 | 
							chunks = jv_array_append(chunks, jv_string(fourcc));
 | 
				
			||||||
 | 
							p += 8;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							// TODO(p): Decode more chunks.
 | 
				
			||||||
 | 
							if (!strcmp(fourcc, "VP8 "))
 | 
				
			||||||
 | 
								o = parse_webp_vp8(o, p, chunk_size);
 | 
				
			||||||
 | 
							if (!strcmp(fourcc, "VP8L"))
 | 
				
			||||||
 | 
								o = parse_webp_vp8l(o, p, chunk_size);
 | 
				
			||||||
 | 
							if (!strcmp(fourcc, "VP8X"))
 | 
				
			||||||
 | 
								o = parse_webp_vp8x(o, p, chunk_size);
 | 
				
			||||||
 | 
							if (!strcmp(fourcc, "EXIF"))
 | 
				
			||||||
 | 
								o = parse_exif(o, p, chunk_size);
 | 
				
			||||||
 | 
							if (!strcmp(fourcc, "ICCP"))
 | 
				
			||||||
 | 
								o = parse_icc(o, p, chunk_size);
 | 
				
			||||||
 | 
							p += chunk_advance;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return jv_set(o, jv_string("chunks"), chunks);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- I/O ---------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static struct {
 | 
				
			||||||
 | 
						const char *name;
 | 
				
			||||||
 | 
						bool (*detect) (const uint8_t *, size_t);
 | 
				
			||||||
 | 
						jv (*parse) (jv, const uint8_t *, size_t);
 | 
				
			||||||
 | 
					} formats[] = {
 | 
				
			||||||
 | 
						{"JPEG", detect_jpeg, parse_jpeg},
 | 
				
			||||||
 | 
						{"TIFF", detect_tiff, parse_tiff},
 | 
				
			||||||
 | 
						{"BMFF", detect_bmff, parse_bmff},
 | 
				
			||||||
 | 
						{"WebP", detect_webp, parse_webp},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					parse_any(jv o, const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// TODO(p): Also see if the file extension is appropriate.
 | 
				
			||||||
 | 
						for (size_t i = 0; i < sizeof formats / sizeof *formats; i++) {
 | 
				
			||||||
 | 
							if (!formats[i].detect(p, len))
 | 
				
			||||||
 | 
								continue;
 | 
				
			||||||
 | 
							if (getenv("INFO_IDENTIFY"))
 | 
				
			||||||
 | 
								o = jv_set(o, jv_string("format"), jv_string(formats[i].name));
 | 
				
			||||||
 | 
							return formats[i].parse(o, p, len);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return add_error(o, "unsupported file format");
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					do_file(const char *filename, jv o)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						const char *err = NULL;
 | 
				
			||||||
 | 
						FILE *fp = fopen(filename, "rb");
 | 
				
			||||||
 | 
						if (!fp) {
 | 
				
			||||||
 | 
							err = strerror(errno);
 | 
				
			||||||
 | 
							goto error;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uint8_t *data = NULL, buf[256 << 10];
 | 
				
			||||||
 | 
						size_t n, len = 0;
 | 
				
			||||||
 | 
						while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
 | 
				
			||||||
 | 
							data = realloc(data, len + n);
 | 
				
			||||||
 | 
							memcpy(data + len, buf, n);
 | 
				
			||||||
 | 
							len += n;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (ferror(fp)) {
 | 
				
			||||||
 | 
							err = strerror(errno);
 | 
				
			||||||
 | 
							goto error_read;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#if 0
 | 
				
			||||||
 | 
						// Not sure if I want to ensure their existence...
 | 
				
			||||||
 | 
						o = jv_object_set(o, jv_string("info"), jv_array());
 | 
				
			||||||
 | 
						o = jv_object_set(o, jv_string("warnings"), jv_array());
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o = parse_any(o, data, len);
 | 
				
			||||||
 | 
					error_read:
 | 
				
			||||||
 | 
						fclose(fp);
 | 
				
			||||||
 | 
						free(data);
 | 
				
			||||||
 | 
					error:
 | 
				
			||||||
 | 
						if (err)
 | 
				
			||||||
 | 
							o = add_error(o, err);
 | 
				
			||||||
 | 
						return o;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int
 | 
				
			||||||
 | 
					main(int argc, char *argv[])
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
 | 
				
			||||||
 | 
						// Usage: find . -print0 | xargs -0 ./info
 | 
				
			||||||
 | 
						for (int i = 1; i < argc; i++) {
 | 
				
			||||||
 | 
							const char *filename = argv[i];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							jv o = jv_object();
 | 
				
			||||||
 | 
							o = jv_object_set(o, jv_string("filename"), jv_string(filename));
 | 
				
			||||||
 | 
							o = do_file(filename, o);
 | 
				
			||||||
 | 
							jv_dumpf(o, stdout, 0 /* JV_PRINT_SORTED would discard information. */);
 | 
				
			||||||
 | 
							fputc('\n', stdout);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										1593
									
								
								tools/info.h
									
									
									
									
									
								
							
							
						
						
							
								
								
									
										701
									
								
								tools/jpeginfo.c
									
									
									
									
									
								
							
							
						
						@@ -1,701 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// jpeginfo.c: acquire information about JPEG files in JSON format
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
					 | 
				
			||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
					 | 
				
			||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
					 | 
				
			||||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
					 | 
				
			||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
					 | 
				
			||||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
					 | 
				
			||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include "info.h"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <jv.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <errno.h>
 | 
					 | 
				
			||||||
#include <stdlib.h>
 | 
					 | 
				
			||||||
#include <string.h>
 | 
					 | 
				
			||||||
#include <stdbool.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- ICC profiles ------------------------------------------------------------
 | 
					 | 
				
			||||||
// v2 https://www.color.org/ICC_Minor_Revision_for_Web.pdf
 | 
					 | 
				
			||||||
// v4 https://www.color.org/specification/ICC1v43_2010-12.pdf
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
parse_icc_mluc(jv o, const uint8_t *tag, uint32_t tag_length)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// v4 10.13
 | 
					 | 
				
			||||||
	if (tag_length < 16)
 | 
					 | 
				
			||||||
		return add_warning(o, "invalid ICC 'mluc' structure length");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	uint32_t count = u32be(tag + 8);
 | 
					 | 
				
			||||||
	if (count == 0)
 | 
					 | 
				
			||||||
		return add_warning(o, "unnamed ICC profile");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// There is no particularly good reason for us to iterate, take the first.
 | 
					 | 
				
			||||||
	const uint8_t *record = tag + 16 /* + i * u32be(tag + 12) */;
 | 
					 | 
				
			||||||
	uint32_t len = u32be(&record[4]);
 | 
					 | 
				
			||||||
	uint32_t off = u32be(&record[8]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (off + len > tag_length)
 | 
					 | 
				
			||||||
		return add_warning(o, "invalid ICC 'mluc' structure record");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Blindly assume simple ASCII, ensure NUL-termination.
 | 
					 | 
				
			||||||
	char name[len], *p = name;
 | 
					 | 
				
			||||||
	for (uint32_t i = 0; i < len / 2; i++)
 | 
					 | 
				
			||||||
		*p++ = tag[off + i * 2 + 1];
 | 
					 | 
				
			||||||
	*p++ = 0;
 | 
					 | 
				
			||||||
	return jv_set(o, jv_string("ICC"),
 | 
					 | 
				
			||||||
		JV_OBJECT(jv_string("name"), jv_string(name),
 | 
					 | 
				
			||||||
			jv_string("version"), jv_number(4)));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
parse_icc_desc(jv o, const uint8_t *profile, size_t profile_len,
 | 
					 | 
				
			||||||
	uint32_t tag_offset, uint32_t tag_length)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	const uint8_t *tag = profile + tag_offset;
 | 
					 | 
				
			||||||
	if (tag_offset + tag_length > profile_len)
 | 
					 | 
				
			||||||
		return add_warning(o, "unexpected end of ICC profile");
 | 
					 | 
				
			||||||
	if (tag_length < 4)
 | 
					 | 
				
			||||||
		return add_warning(o, "invalid ICC tag structure length");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// v2 6.5.17
 | 
					 | 
				
			||||||
	uint32_t sig = u32be(tag);
 | 
					 | 
				
			||||||
	if (sig == 0x6D6C7563 /* mluc */)
 | 
					 | 
				
			||||||
		return parse_icc_mluc(o, profile + tag_offset, tag_length);
 | 
					 | 
				
			||||||
	if (sig != 0x64657363 /* desc */)
 | 
					 | 
				
			||||||
		return add_warning(o, "invalid ICC 'desc' structure signature");
 | 
					 | 
				
			||||||
	if (tag_length < 12)
 | 
					 | 
				
			||||||
		return add_warning(o, "invalid ICC 'desc' structure length");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	uint32_t count = u32be(tag + 8);
 | 
					 | 
				
			||||||
	if (tag_length < 12 + count)
 | 
					 | 
				
			||||||
		return add_warning(o, "invalid ICC 'desc' structure length");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Double-ensure a trailing NUL byte.
 | 
					 | 
				
			||||||
	char name[count + 1];
 | 
					 | 
				
			||||||
	memcpy(name, tag + 12, count);
 | 
					 | 
				
			||||||
	name[count] = 0;
 | 
					 | 
				
			||||||
	return jv_set(o, jv_string("ICC"),
 | 
					 | 
				
			||||||
		JV_OBJECT(jv_string("name"), jv_string(name),
 | 
					 | 
				
			||||||
			jv_string("version"), jv_number(2)));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
parse_icc(jv o, const uint8_t *profile, size_t profile_len)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// v2 6, v4 7
 | 
					 | 
				
			||||||
	if (profile_len < 132)
 | 
					 | 
				
			||||||
		return add_warning(o, "ICC profile too short");
 | 
					 | 
				
			||||||
	if (u32be(profile) != profile_len)
 | 
					 | 
				
			||||||
		return add_warning(o, "ICC profile size mismatch");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// TODO(p): May decode more of the header fields, and validate them.
 | 
					 | 
				
			||||||
	// Need to check both v2 and v4, this is all fairly annoying.
 | 
					 | 
				
			||||||
	uint32_t count = u32be(profile + 128);
 | 
					 | 
				
			||||||
	if (132 + count * 12 > profile_len)
 | 
					 | 
				
			||||||
		return add_warning(o, "unexpected end of ICC profile");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	for (uint32_t i = 0; i < count; i++) {
 | 
					 | 
				
			||||||
		const uint8_t *entry = profile + 132 + i * 12;
 | 
					 | 
				
			||||||
		uint32_t sig = u32be(&entry[0]);
 | 
					 | 
				
			||||||
		uint32_t off = u32be(&entry[4]);
 | 
					 | 
				
			||||||
		uint32_t len = u32be(&entry[8]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// v2 6.4.32, v4 9.2.41
 | 
					 | 
				
			||||||
		if (sig == 0x64657363 /* desc */)
 | 
					 | 
				
			||||||
			return parse_icc_desc(o, profile, profile_len, off, len);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	// The description is required, so this should be unreachable.
 | 
					 | 
				
			||||||
	return jv_set(o, jv_string("ICC"), jv_bool(true));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- Multi-Picture Format ----------------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum {
 | 
					 | 
				
			||||||
	MPF_MPFVersion = 45056,
 | 
					 | 
				
			||||||
	MPF_NumberOfImages = 45057,
 | 
					 | 
				
			||||||
	MPF_MPEntry = 45058,
 | 
					 | 
				
			||||||
	MPF_ImageUIDList = 45059,
 | 
					 | 
				
			||||||
	MPF_TotalFrames = 45060,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	MPF_MPIndividualNum = 45313,
 | 
					 | 
				
			||||||
	MPF_PanOrientation = 45569,
 | 
					 | 
				
			||||||
	MPF_PanOverlap_H = 45570,
 | 
					 | 
				
			||||||
	MPF_PanOverlap_V = 45571,
 | 
					 | 
				
			||||||
	MPF_BaseViewpointNum = 45572,
 | 
					 | 
				
			||||||
	MPF_ConvergenceAngle = 45573,
 | 
					 | 
				
			||||||
	MPF_BaselineLength = 45574,
 | 
					 | 
				
			||||||
	MPF_VerticalDivergence = 45575,
 | 
					 | 
				
			||||||
	MPF_AxisDistance_X = 45576,
 | 
					 | 
				
			||||||
	MPF_AxisDistance_Y = 45577,
 | 
					 | 
				
			||||||
	MPF_AxisDistance_Z = 45578,
 | 
					 | 
				
			||||||
	MPF_YawAngle = 45579,
 | 
					 | 
				
			||||||
	MPF_PitchAngle = 45580,
 | 
					 | 
				
			||||||
	MPF_RollAngle = 45581
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static struct tiff_entry mpf_entries[] = {
 | 
					 | 
				
			||||||
	{"MP Format Version Number", MPF_MPFVersion, NULL},
 | 
					 | 
				
			||||||
	{"Number of Images", MPF_NumberOfImages, NULL},
 | 
					 | 
				
			||||||
	{"MP Entry", MPF_MPEntry, NULL},
 | 
					 | 
				
			||||||
	{"Individual Image Unique ID List", MPF_ImageUIDList, NULL},
 | 
					 | 
				
			||||||
	{"Total Number of Captured Frames", MPF_TotalFrames, NULL},
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	{"MP Individual Image Number", MPF_MPIndividualNum, NULL},
 | 
					 | 
				
			||||||
	{"Panorama Scanning Orientation", MPF_PanOrientation, NULL},
 | 
					 | 
				
			||||||
	{"Panorama Horizontal Overlap", MPF_PanOverlap_H, NULL},
 | 
					 | 
				
			||||||
	{"Panorama Vertical Overlap", MPF_PanOverlap_V, NULL},
 | 
					 | 
				
			||||||
	{"Base Viewpoint Number", MPF_BaseViewpointNum, NULL},
 | 
					 | 
				
			||||||
	{"Convergence Angle", MPF_ConvergenceAngle, NULL},
 | 
					 | 
				
			||||||
	{"Baseline Length", MPF_BaselineLength, NULL},
 | 
					 | 
				
			||||||
	{"Divergence Angle", MPF_VerticalDivergence, NULL},
 | 
					 | 
				
			||||||
	{"Horizontal Axis Distance", MPF_AxisDistance_X, NULL},
 | 
					 | 
				
			||||||
	{"Vertical Axis Distance", MPF_AxisDistance_Y, NULL},
 | 
					 | 
				
			||||||
	{"Collimation Axis Distance", MPF_AxisDistance_Z, NULL},
 | 
					 | 
				
			||||||
	{"Yaw Angle", MPF_YawAngle, NULL},
 | 
					 | 
				
			||||||
	{"Pitch Angle", MPF_PitchAngle, NULL},
 | 
					 | 
				
			||||||
	{"Roll Angle", MPF_RollAngle, NULL},
 | 
					 | 
				
			||||||
	{}
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static uint32_t
 | 
					 | 
				
			||||||
parse_mpf_mpentry(jv *a, const uint8_t *p, struct tiffer *T)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	uint32_t attrs = T->un->u32(p);
 | 
					 | 
				
			||||||
	uint32_t offset = T->un->u32(p + 8);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	uint32_t type_number = attrs & 0xFFFFFF;
 | 
					 | 
				
			||||||
	jv type = jv_number(type_number);
 | 
					 | 
				
			||||||
	switch (type_number) {
 | 
					 | 
				
			||||||
	break; case 0x030000: type = jv_string("Baseline MP Primary Image");
 | 
					 | 
				
			||||||
	break; case 0x010001: type = jv_string("Large Thumbnail - VGA");
 | 
					 | 
				
			||||||
	break; case 0x010002: type = jv_string("Large Thumbnail - Full HD");
 | 
					 | 
				
			||||||
	break; case 0x020001: type = jv_string("Multi-Frame Image Panorama");
 | 
					 | 
				
			||||||
	break; case 0x020002: type = jv_string("Multi-Frame Image Disparity");
 | 
					 | 
				
			||||||
	break; case 0x020003: type = jv_string("Multi-Frame Image Multi-Angle");
 | 
					 | 
				
			||||||
	break; case 0x000000: type = jv_string("Undefined");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	uint32_t format_number = (attrs >> 24) & 0x7;
 | 
					 | 
				
			||||||
	jv format = jv_number(format_number);
 | 
					 | 
				
			||||||
	if (format_number == 0)
 | 
					 | 
				
			||||||
		format = jv_string("JPEG");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	*a = jv_array_append(*a, JV_OBJECT(
 | 
					 | 
				
			||||||
		jv_string("Individual Image Attribute"), JV_OBJECT(
 | 
					 | 
				
			||||||
			jv_string("Dependent Parent Image"), jv_bool((attrs >> 31) & 1),
 | 
					 | 
				
			||||||
			jv_string("Dependent Child Image"), jv_bool((attrs >> 30) & 1),
 | 
					 | 
				
			||||||
			jv_string("Representative Image"), jv_bool((attrs >> 29) & 1),
 | 
					 | 
				
			||||||
			jv_string("Reserved"), jv_number((attrs >> 27) & 0x3),
 | 
					 | 
				
			||||||
			jv_string("Image Data Format"), format,
 | 
					 | 
				
			||||||
			jv_string("MP Type Code"), type
 | 
					 | 
				
			||||||
		),
 | 
					 | 
				
			||||||
		jv_string("Individual Image Size"),
 | 
					 | 
				
			||||||
		jv_number(T->un->u32(p + 4)),
 | 
					 | 
				
			||||||
		jv_string("Individual Image Data Offset"),
 | 
					 | 
				
			||||||
		jv_number(offset),
 | 
					 | 
				
			||||||
		jv_string("Dependent Image 1 Entry Number"),
 | 
					 | 
				
			||||||
		jv_number(T->un->u16(p + 12)),
 | 
					 | 
				
			||||||
		jv_string("Dependent Image 2 Entry Number"),
 | 
					 | 
				
			||||||
		jv_number(T->un->u16(p + 14))
 | 
					 | 
				
			||||||
	));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Don't report non-JPEGs, even though they're unlikely.
 | 
					 | 
				
			||||||
	return format_number == 0 ? offset : 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
parse_mpf_index_entry(jv o, const uint8_t ***offsets, struct tiffer *T,
 | 
					 | 
				
			||||||
	struct tiffer_entry *entry)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// 5.2.3.3. MP Entry
 | 
					 | 
				
			||||||
	if (entry->tag != MPF_MPEntry || entry->type != UNDEFINED ||
 | 
					 | 
				
			||||||
		entry->remaining_count % 16) {
 | 
					 | 
				
			||||||
		return parse_exif_entry(o, T, entry, mpf_entries);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	uint32_t count = entry->remaining_count / 16;
 | 
					 | 
				
			||||||
	jv a = jv_array_sized(count);
 | 
					 | 
				
			||||||
	const uint8_t **out = *offsets = calloc(sizeof *out, count + 1);
 | 
					 | 
				
			||||||
	for (uint32_t i = 0; i < count; i++) {
 | 
					 | 
				
			||||||
		uint32_t offset = parse_mpf_mpentry(&a, entry->p + i * 16, T);
 | 
					 | 
				
			||||||
		if (offset)
 | 
					 | 
				
			||||||
			*out++ = T->begin + offset;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return jv_set(o, jv_string("MP Entry"), a);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
parse_mpf_index_ifd(const uint8_t ***offsets, struct tiffer *T)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	jv ifd = jv_object();
 | 
					 | 
				
			||||||
	struct tiffer_entry entry = {};
 | 
					 | 
				
			||||||
	while (tiffer_next_entry(T, &entry))
 | 
					 | 
				
			||||||
		ifd = parse_mpf_index_entry(ifd, offsets, T, &entry);
 | 
					 | 
				
			||||||
	return ifd;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
parse_mpf(jv o, const uint8_t ***offsets, const uint8_t *p, size_t len)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	struct tiffer T;
 | 
					 | 
				
			||||||
	if (!tiffer_init(&T, p, len) || !tiffer_next_ifd(&T))
 | 
					 | 
				
			||||||
		return add_warning(o, "invalid MPF segment");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// First image: IFD0 is Index IFD, any IFD1 is Attribute IFD.
 | 
					 | 
				
			||||||
	// Other images: IFD0 is Attribute IFD, there is no Index IFD.
 | 
					 | 
				
			||||||
	if (!*offsets) {
 | 
					 | 
				
			||||||
		o = add_to_subarray(o, "MPF", parse_mpf_index_ifd(offsets, &T));
 | 
					 | 
				
			||||||
		if (!tiffer_next_ifd(&T))
 | 
					 | 
				
			||||||
			return o;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// This isn't optimal, but it will do.
 | 
					 | 
				
			||||||
	return add_to_subarray(o, "MPF", parse_exif_ifd(&T, mpf_entries));
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- JPEG --------------------------------------------------------------------
 | 
					 | 
				
			||||||
// Because the JPEG file format is simple, just do it manually.
 | 
					 | 
				
			||||||
// See: https://www.w3.org/Graphics/JPEG/itu-t81.pdf
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
enum {
 | 
					 | 
				
			||||||
	TEM = 0x01,
 | 
					 | 
				
			||||||
	SOF0 = 0xC0, SOF1, SOF2, SOF3,
 | 
					 | 
				
			||||||
	DHT = 0xC4,
 | 
					 | 
				
			||||||
	SOF5, SOF6, SOF7,
 | 
					 | 
				
			||||||
	JPG = 0xC8,
 | 
					 | 
				
			||||||
	SOF9, SOF10, SOF11,
 | 
					 | 
				
			||||||
	DAC = 0xCC,
 | 
					 | 
				
			||||||
	SOF13, SOF14, SOF15,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	RST0 = 0xD0, RST1, RST2, RST3, RST4, RST5, RST6, RST7,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	SOI = 0xD8,
 | 
					 | 
				
			||||||
	EOI = 0xD9,
 | 
					 | 
				
			||||||
	SOS = 0xDA,
 | 
					 | 
				
			||||||
	DQT = 0xDB,
 | 
					 | 
				
			||||||
	DNL = 0xDC,
 | 
					 | 
				
			||||||
	DRI = 0xDD,
 | 
					 | 
				
			||||||
	DHP = 0xDE,
 | 
					 | 
				
			||||||
	EXP = 0xDF,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	APP0 = 0xE0, APP1, APP2, APP3, APP4, APP5, APP6, APP7,
 | 
					 | 
				
			||||||
	APP8, APP9, APP10, APP11, APP12, APP13, APP14, APP15,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	JPG0 = 0xF0, JPG1, JPG2, JPG3, JPG4, JPG5, JPG6, JPG7,
 | 
					 | 
				
			||||||
	JPG8, JPG9, JPG10, JPG11, JPG12, JPG13,
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	COM = 0xFE
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// The rest is "RES (Reserved)", except for 0xFF (filler) and 0x00 (invalid).
 | 
					 | 
				
			||||||
static const char *marker_ids[0xFF] = {
 | 
					 | 
				
			||||||
	[TEM]   = "TEM",
 | 
					 | 
				
			||||||
	[SOF0]  = "SOF0",  [SOF1]  = "SOF1",  [SOF2]  = "SOF2",  [SOF3]  = "SOF3",
 | 
					 | 
				
			||||||
	[DHT]   = "DHT",   [SOF5]  = "SOF5",  [SOF6]  = "SOF6",  [SOF7]  = "SOF7",
 | 
					 | 
				
			||||||
	[JPG]   = "JPG",   [SOF9]  = "SOF9",  [SOF10] = "SOF10", [SOF11] = "SOF11",
 | 
					 | 
				
			||||||
	[DAC]   = "DAC",   [SOF13] = "SOF13", [SOF14] = "SOF14", [SOF15] = "SOF15",
 | 
					 | 
				
			||||||
	[RST0]  = "RST0",  [RST1]  = "RST1",  [RST2]  = "RST2",  [RST3]  = "RST3",
 | 
					 | 
				
			||||||
	[RST4]  = "RST4",  [RST5]  = "RST5",  [RST6]  = "RST6",  [RST7]  = "RST7",
 | 
					 | 
				
			||||||
	[SOI]   = "SOI",   [EOI]   = "EOI",   [SOS]   = "SOS",   [DQT]   = "DQT",
 | 
					 | 
				
			||||||
	[DNL]   = "DNL",   [DRI]   = "DRI",   [DHP]   = "DHP",   [EXP]   = "EXP",
 | 
					 | 
				
			||||||
	[APP0]  = "APP0",  [APP1]  = "APP1",  [APP2]  = "APP2",  [APP3]  = "APP3",
 | 
					 | 
				
			||||||
	[APP4]  = "APP4",  [APP5]  = "APP5",  [APP6]  = "APP6",  [APP7]  = "APP7",
 | 
					 | 
				
			||||||
	[APP8]  = "APP8",  [APP9]  = "APP9",  [APP10] = "APP10", [APP11] = "APP11",
 | 
					 | 
				
			||||||
	[APP12] = "APP12", [APP13] = "APP13", [APP14] = "APP14", [APP15] = "APP15",
 | 
					 | 
				
			||||||
	[JPG0]  = "JPG0",  [JPG1]  = "JPG1",  [JPG2]  = "JPG2",  [JPG3]  = "JPG3",
 | 
					 | 
				
			||||||
	[JPG4]  = "JPG4",  [JPG5]  = "JPG5",  [JPG6]  = "JPG6",  [JPG7]  = "JPG7",
 | 
					 | 
				
			||||||
	[JPG8]  = "JPG8",  [JPG9]  = "JPG9",  [JPG10] = "JPG10", [JPG11] = "JPG11",
 | 
					 | 
				
			||||||
	[JPG12] = "JPG12", [JPG13] = "JPG13", [COM]   = "COM"
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// The rest is "RES (Reserved)", except for 0xFF (filler) and 0x00 (invalid).
 | 
					 | 
				
			||||||
static const char *marker_descriptions[0xFF] = {
 | 
					 | 
				
			||||||
	[TEM]   = "For temporary private use in arithmetic coding",
 | 
					 | 
				
			||||||
	[SOF0]  = "Baseline DCT",
 | 
					 | 
				
			||||||
	[SOF1]  = "Extended sequential DCT",
 | 
					 | 
				
			||||||
	[SOF2]  = "Progressive DCT",
 | 
					 | 
				
			||||||
	[SOF3]  = "Lossless (sequential)",
 | 
					 | 
				
			||||||
	[DHT]   = "Define Huffman table(s)",
 | 
					 | 
				
			||||||
	[SOF5]  = "Differential sequential DCT",
 | 
					 | 
				
			||||||
	[SOF6]  = "Differential progressive DCT",
 | 
					 | 
				
			||||||
	[SOF7]  = "Differential lossless (sequential)",
 | 
					 | 
				
			||||||
	[JPG]   = "Reserved for JPEG extensions",
 | 
					 | 
				
			||||||
	[SOF9]  = "Extended sequential DCT",
 | 
					 | 
				
			||||||
	[SOF10] = "Progressive DCT",
 | 
					 | 
				
			||||||
	[SOF11] = "Lossless (sequential)",
 | 
					 | 
				
			||||||
	[DAC]   = "Define arithmetic coding conditioning(s)",
 | 
					 | 
				
			||||||
	[SOF13] = "Differential sequential DCT",
 | 
					 | 
				
			||||||
	[SOF14] = "Differential progressive DCT",
 | 
					 | 
				
			||||||
	[SOF15] = "Differential lossless (sequential)",
 | 
					 | 
				
			||||||
	[RST0]  = "Restart with module 8 count 0",
 | 
					 | 
				
			||||||
	[RST1]  = "Restart with module 8 count 1",
 | 
					 | 
				
			||||||
	[RST2]  = "Restart with module 8 count 2",
 | 
					 | 
				
			||||||
	[RST3]  = "Restart with module 8 count 3",
 | 
					 | 
				
			||||||
	[RST4]  = "Restart with module 8 count 4",
 | 
					 | 
				
			||||||
	[RST5]  = "Restart with module 8 count 5",
 | 
					 | 
				
			||||||
	[RST6]  = "Restart with module 8 count 6",
 | 
					 | 
				
			||||||
	[RST7]  = "Restart with module 8 count 7",
 | 
					 | 
				
			||||||
	[SOI]   = "Start of image",
 | 
					 | 
				
			||||||
	[EOI]   = "End of image",
 | 
					 | 
				
			||||||
	[SOS]   = "Start of scan",
 | 
					 | 
				
			||||||
	[DQT]   = "Define quantization table(s)",
 | 
					 | 
				
			||||||
	[DNL]   = "Define number of lines",
 | 
					 | 
				
			||||||
	[DRI]   = "Define restart interval",
 | 
					 | 
				
			||||||
	[DHP]   = "Define hierarchical progression",
 | 
					 | 
				
			||||||
	[EXP]   = "Expand reference component(s)",
 | 
					 | 
				
			||||||
	[APP0]  = "Reserved for application segments, 0",
 | 
					 | 
				
			||||||
	[APP1]  = "Reserved for application segments, 1",
 | 
					 | 
				
			||||||
	[APP2]  = "Reserved for application segments, 2",
 | 
					 | 
				
			||||||
	[APP3]  = "Reserved for application segments, 3",
 | 
					 | 
				
			||||||
	[APP4]  = "Reserved for application segments, 4",
 | 
					 | 
				
			||||||
	[APP5]  = "Reserved for application segments, 5",
 | 
					 | 
				
			||||||
	[APP6]  = "Reserved for application segments, 6",
 | 
					 | 
				
			||||||
	[APP7]  = "Reserved for application segments, 7",
 | 
					 | 
				
			||||||
	[APP8]  = "Reserved for application segments, 8",
 | 
					 | 
				
			||||||
	[APP9]  = "Reserved for application segments, 9",
 | 
					 | 
				
			||||||
	[APP10] = "Reserved for application segments, 10",
 | 
					 | 
				
			||||||
	[APP11] = "Reserved for application segments, 11",
 | 
					 | 
				
			||||||
	[APP12] = "Reserved for application segments, 12",
 | 
					 | 
				
			||||||
	[APP13] = "Reserved for application segments, 13",
 | 
					 | 
				
			||||||
	[APP14] = "Reserved for application segments, 14",
 | 
					 | 
				
			||||||
	[APP15] = "Reserved for application segments, 15",
 | 
					 | 
				
			||||||
	[JPG0]  = "Reserved for JPEG extensions, 0",
 | 
					 | 
				
			||||||
	[JPG1]  = "Reserved for JPEG extensions, 1",
 | 
					 | 
				
			||||||
	[JPG2]  = "Reserved for JPEG extensions, 2",
 | 
					 | 
				
			||||||
	[JPG3]  = "Reserved for JPEG extensions, 3",
 | 
					 | 
				
			||||||
	[JPG4]  = "Reserved for JPEG extensions, 4",
 | 
					 | 
				
			||||||
	[JPG5]  = "Reserved for JPEG extensions, 5",
 | 
					 | 
				
			||||||
	[JPG6]  = "Reserved for JPEG extensions, 6",
 | 
					 | 
				
			||||||
	[JPG7]  = "Reserved for JPEG extensions, 7",
 | 
					 | 
				
			||||||
	[JPG8]  = "Reserved for JPEG extensions, 8",
 | 
					 | 
				
			||||||
	[JPG9]  = "Reserved for JPEG extensions, 9",
 | 
					 | 
				
			||||||
	[JPG10] = "Reserved for JPEG extensions, 10",
 | 
					 | 
				
			||||||
	[JPG11] = "Reserved for JPEG extensions, 11",
 | 
					 | 
				
			||||||
	[JPG12] = "Reserved for JPEG extensions, 12",
 | 
					 | 
				
			||||||
	[JPG13] = "Reserved for JPEG extensions, 13",
 | 
					 | 
				
			||||||
	[COM]   = "Comment",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
struct data {
 | 
					 | 
				
			||||||
	bool ended;
 | 
					 | 
				
			||||||
	uint8_t *exif, *icc, *psir;
 | 
					 | 
				
			||||||
	size_t exif_len, icc_len, psir_len;
 | 
					 | 
				
			||||||
	int icc_sequence, icc_done;
 | 
					 | 
				
			||||||
	const uint8_t **mpf_offsets, **mpf_next;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static void
 | 
					 | 
				
			||||||
parse_append(uint8_t **buffer, size_t *buffer_len, const uint8_t *p, size_t len)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	size_t buffer_longer = *buffer_len + len;
 | 
					 | 
				
			||||||
	*buffer = realloc(*buffer, buffer_longer);
 | 
					 | 
				
			||||||
	memcpy(*buffer + *buffer_len, p, len);
 | 
					 | 
				
			||||||
	*buffer_len = buffer_longer;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static const uint8_t *
 | 
					 | 
				
			||||||
parse_marker(uint8_t marker, const uint8_t *p, const uint8_t *end,
 | 
					 | 
				
			||||||
	struct data *data, jv *o)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// Suspected: MJPEG? Undetected format recursion, e.g., thumbnails?
 | 
					 | 
				
			||||||
	// Found: Random metadata! Multi-Picture Format!
 | 
					 | 
				
			||||||
	if ((data->ended = marker == EOI)) {
 | 
					 | 
				
			||||||
		// TODO(p): Handle Exifs independently--flush the last one.
 | 
					 | 
				
			||||||
		if ((data->mpf_next || (data->mpf_next = data->mpf_offsets)) &&
 | 
					 | 
				
			||||||
			*data->mpf_next)
 | 
					 | 
				
			||||||
			return *data->mpf_next++;
 | 
					 | 
				
			||||||
		if (p != end)
 | 
					 | 
				
			||||||
			*o = add_warning(*o, "trailing data");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// These markers stand alone, not starting a marker segment.
 | 
					 | 
				
			||||||
	switch (marker) {
 | 
					 | 
				
			||||||
	case RST0:
 | 
					 | 
				
			||||||
	case RST1:
 | 
					 | 
				
			||||||
	case RST2:
 | 
					 | 
				
			||||||
	case RST3:
 | 
					 | 
				
			||||||
	case RST4:
 | 
					 | 
				
			||||||
	case RST5:
 | 
					 | 
				
			||||||
	case RST6:
 | 
					 | 
				
			||||||
	case RST7:
 | 
					 | 
				
			||||||
		*o = add_warning(*o, "unexpected restart marker");
 | 
					 | 
				
			||||||
		// Fall-through
 | 
					 | 
				
			||||||
	case SOI:
 | 
					 | 
				
			||||||
	case EOI:
 | 
					 | 
				
			||||||
	case TEM:
 | 
					 | 
				
			||||||
		return p;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	uint16_t length = p[0] << 8 | p[1];
 | 
					 | 
				
			||||||
	const uint8_t *payload = p + 2;
 | 
					 | 
				
			||||||
	if ((p += length) > end) {
 | 
					 | 
				
			||||||
		*o = add_error(*o, "runaway marker segment");
 | 
					 | 
				
			||||||
		return NULL;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	switch (marker) {
 | 
					 | 
				
			||||||
	case SOF0:
 | 
					 | 
				
			||||||
	case SOF1:
 | 
					 | 
				
			||||||
	case SOF2:
 | 
					 | 
				
			||||||
	case SOF3:
 | 
					 | 
				
			||||||
	case SOF5:
 | 
					 | 
				
			||||||
	case SOF6:
 | 
					 | 
				
			||||||
	case SOF7:
 | 
					 | 
				
			||||||
	case SOF9:
 | 
					 | 
				
			||||||
	case SOF10:
 | 
					 | 
				
			||||||
	case SOF11:
 | 
					 | 
				
			||||||
	case SOF13:
 | 
					 | 
				
			||||||
	case SOF14:
 | 
					 | 
				
			||||||
	case SOF15:
 | 
					 | 
				
			||||||
	case DHP:  // B.2.2 and B.3.2.
 | 
					 | 
				
			||||||
		// As per B.2.5, Y can be zero, then there needs to be a DNL segment.
 | 
					 | 
				
			||||||
		*o = add_to_subarray(*o, "info", JV_OBJECT(
 | 
					 | 
				
			||||||
			jv_string("type"), jv_string(marker_descriptions[marker]),
 | 
					 | 
				
			||||||
			jv_string("bits"), jv_number(payload[0]),
 | 
					 | 
				
			||||||
			jv_string("height"), jv_number(payload[1] << 8 | payload[2]),
 | 
					 | 
				
			||||||
			jv_string("width"), jv_number(payload[3] << 8 | payload[4]),
 | 
					 | 
				
			||||||
			jv_string("components"), jv_number(payload[5])
 | 
					 | 
				
			||||||
		));
 | 
					 | 
				
			||||||
		return p;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// See B.1.1.5, we can brute-force our way through the entropy-coded data.
 | 
					 | 
				
			||||||
	if (marker == SOS) {
 | 
					 | 
				
			||||||
		while (p + 2 <= end && (p[0] != 0xFF || p[1] < 0xC0 || p[1] > 0xFE ||
 | 
					 | 
				
			||||||
				(p[1] >= RST0 && p[1] <= RST7)))
 | 
					 | 
				
			||||||
			p++;
 | 
					 | 
				
			||||||
		return p;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// "The interpretation is left to the application."
 | 
					 | 
				
			||||||
	if (marker == COM) {
 | 
					 | 
				
			||||||
		int superascii = 0;
 | 
					 | 
				
			||||||
		char *buf = calloc(3, p - payload), *bufp = buf;
 | 
					 | 
				
			||||||
		for (const uint8_t *q = payload; q < p; q++) {
 | 
					 | 
				
			||||||
			if (*q < 128) {
 | 
					 | 
				
			||||||
				*bufp++ = *q;
 | 
					 | 
				
			||||||
			} else {
 | 
					 | 
				
			||||||
				superascii++;
 | 
					 | 
				
			||||||
				*bufp++ = 0xC0 | (*q >> 6);
 | 
					 | 
				
			||||||
				*bufp++ = 0x80 | (*q & 0x3F);
 | 
					 | 
				
			||||||
			}
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		*bufp++ = 0;
 | 
					 | 
				
			||||||
		*o = add_to_subarray(*o, "comments", jv_string(buf));
 | 
					 | 
				
			||||||
		free(buf);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		if (superascii)
 | 
					 | 
				
			||||||
			*o = add_warning(*o, "super-ASCII comments");
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// These mostly contain an ASCII string header, following JPEG FIF:
 | 
					 | 
				
			||||||
	//
 | 
					 | 
				
			||||||
	// "Application-specific APP0 marker segments are identified
 | 
					 | 
				
			||||||
	//  by a zero terminated string which identifies the application
 | 
					 | 
				
			||||||
	//  (not 'JFIF' or 'JFXX')."
 | 
					 | 
				
			||||||
	if (marker >= APP0 && marker <= APP15) {
 | 
					 | 
				
			||||||
		const uint8_t *nul = memchr(payload, 0, p - payload);
 | 
					 | 
				
			||||||
		int unprintable = !nul;
 | 
					 | 
				
			||||||
		if (nul) {
 | 
					 | 
				
			||||||
			for (const uint8_t *q = payload; q < nul; q++)
 | 
					 | 
				
			||||||
				unprintable += *q < 32 || *q >= 127;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		*o = add_to_subarray(*o, "apps",
 | 
					 | 
				
			||||||
			unprintable ? jv_null() : jv_string((const char *) payload));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// CIPA DC-007 (Multi-Picture Format) 5.2
 | 
					 | 
				
			||||||
	// http://fileformats.archiveteam.org/wiki/Multi-Picture_Format
 | 
					 | 
				
			||||||
	if (marker == APP2 && p - payload >= 8 && !memcmp(payload, "MPF\0", 4)) {
 | 
					 | 
				
			||||||
		payload += 4;
 | 
					 | 
				
			||||||
		*o = parse_mpf(*o, &data->mpf_offsets, payload, p - payload);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// CIPA DC-006 (Stereo Still Image Format for Digital Cameras)
 | 
					 | 
				
			||||||
	// TODO(p): Handle by properly skipping trailing data (use Stim offsets).
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// https://www.w3.org/Graphics/JPEG/jfif3.pdf
 | 
					 | 
				
			||||||
	if (marker == APP0 && p - payload >= 14 && !memcmp(payload, "JFIF\0", 5)) {
 | 
					 | 
				
			||||||
		payload += 5;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jv units = jv_number(payload[2]);
 | 
					 | 
				
			||||||
		switch (payload[2]) {
 | 
					 | 
				
			||||||
		break; case 0: units = jv_null();
 | 
					 | 
				
			||||||
		break; case 1: units = jv_string("DPI");
 | 
					 | 
				
			||||||
		break; case 2: units = jv_string("dots per cm");
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// The rest is picture data.
 | 
					 | 
				
			||||||
		*o = add_to_subarray(*o, "JFIF", JV_OBJECT(
 | 
					 | 
				
			||||||
			jv_string("version"), jv_number(payload[0] * 100 + payload[1]),
 | 
					 | 
				
			||||||
			jv_string("units"), units,
 | 
					 | 
				
			||||||
			jv_string("density-x"), jv_number(payload[3] << 8 | payload[4]),
 | 
					 | 
				
			||||||
			jv_string("density-y"), jv_number(payload[5] << 8 | payload[6]),
 | 
					 | 
				
			||||||
			jv_string("thumbnail-w"), jv_number(payload[7]),
 | 
					 | 
				
			||||||
			jv_string("thumbnail-h"), jv_number(payload[8])
 | 
					 | 
				
			||||||
		));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (marker == APP0 && p - payload >= 6 && !memcmp(payload, "JFXX\0", 5)) {
 | 
					 | 
				
			||||||
		payload += 5;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jv extension = jv_number(payload[0]);
 | 
					 | 
				
			||||||
		switch (payload[0]) {
 | 
					 | 
				
			||||||
		break; case 0x10: extension = jv_string("JPEG thumbnail");
 | 
					 | 
				
			||||||
		break; case 0x11: extension = jv_string("Paletted thumbnail");
 | 
					 | 
				
			||||||
		break; case 0x13: extension = jv_string("RGB thumbnail");
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// The rest is picture data.
 | 
					 | 
				
			||||||
		*o = add_to_subarray(*o, "JFXX",
 | 
					 | 
				
			||||||
			JV_OBJECT(jv_string("extension"), extension));
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// https://www.cipa.jp/std/documents/e/DC-008-2012_E.pdf 4.7.2
 | 
					 | 
				
			||||||
	// Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3
 | 
					 | 
				
			||||||
	if (marker == APP1 && p - payload >= 6 && !memcmp(payload, "Exif\0", 5)) {
 | 
					 | 
				
			||||||
		payload += 6;
 | 
					 | 
				
			||||||
		if (payload[-1] != 0)
 | 
					 | 
				
			||||||
			*o = add_warning(*o, "weirdly padded Exif header");
 | 
					 | 
				
			||||||
		if (data->exif)
 | 
					 | 
				
			||||||
			*o = add_warning(*o, "multiple Exif segments");
 | 
					 | 
				
			||||||
		parse_append(&data->exif, &data->exif_len, payload, p - payload);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// https://www.color.org/specification/ICC1v43_2010-12.pdf B.4
 | 
					 | 
				
			||||||
	if (marker == APP2 && p - payload >= 14 &&
 | 
					 | 
				
			||||||
		!memcmp(payload, "ICC_PROFILE\0", 12) && !data->icc_done &&
 | 
					 | 
				
			||||||
		payload[12] == ++data->icc_sequence && payload[13] >= payload[12]) {
 | 
					 | 
				
			||||||
		payload += 14;
 | 
					 | 
				
			||||||
		parse_append(&data->icc, &data->icc_len, payload, p - payload);
 | 
					 | 
				
			||||||
		data->icc_done = payload[-1] == data->icc_sequence;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	// Adobe XMP Specification Part 3: Storage in Files, 2020/1, 1.1.3 + 3.1.3
 | 
					 | 
				
			||||||
	// https://www.adobe.com/devnet-apps/photoshop/fileformatashtml/
 | 
					 | 
				
			||||||
	if (marker == APP13 && p - payload >= 14 &&
 | 
					 | 
				
			||||||
		!memcmp(payload, "Photoshop 3.0\0", 14)) {
 | 
					 | 
				
			||||||
		payload += 14;
 | 
					 | 
				
			||||||
		parse_append(&data->psir, &data->psir_len, payload, p - payload);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return p;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
parse_jpeg(jv o, const uint8_t *p, size_t len)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	struct data data = {};
 | 
					 | 
				
			||||||
	const uint8_t *end = p + len;
 | 
					 | 
				
			||||||
	jv markers = jv_array();
 | 
					 | 
				
			||||||
	while (p) {
 | 
					 | 
				
			||||||
		// This is an expectable condition, use a simple warning.
 | 
					 | 
				
			||||||
		if (p + 2 > end) {
 | 
					 | 
				
			||||||
			if (!data.ended)
 | 
					 | 
				
			||||||
				o = add_warning(o, "unexpected EOF");
 | 
					 | 
				
			||||||
			break;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
		if (*p++ != 0xFF || *p == 0) {
 | 
					 | 
				
			||||||
			if (!data.ended)
 | 
					 | 
				
			||||||
				o = add_error(o, "no marker found where one was expected");
 | 
					 | 
				
			||||||
			break;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		// Markers may be preceded by fill bytes.
 | 
					 | 
				
			||||||
		if (*p == 0xFF) {
 | 
					 | 
				
			||||||
			o = jv_object_set(o, jv_string("fillers"), jv_bool(true));
 | 
					 | 
				
			||||||
			continue;
 | 
					 | 
				
			||||||
		}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		uint8_t marker = *p++;
 | 
					 | 
				
			||||||
		markers = jv_array_append(markers,
 | 
					 | 
				
			||||||
			jv_string(marker_ids[marker] ? marker_ids[marker] : "RES"));
 | 
					 | 
				
			||||||
		p = parse_marker(marker, p, end, &data, &o);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	if (data.exif) {
 | 
					 | 
				
			||||||
		o = parse_exif(o, data.exif, data.exif_len);
 | 
					 | 
				
			||||||
		free(data.exif);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (data.icc) {
 | 
					 | 
				
			||||||
		if (data.icc_done)
 | 
					 | 
				
			||||||
			o = parse_icc(o, data.icc, data.icc_len);
 | 
					 | 
				
			||||||
		else
 | 
					 | 
				
			||||||
			o = add_warning(o, "bad ICC profile sequence");
 | 
					 | 
				
			||||||
		free(data.icc);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (data.psir) {
 | 
					 | 
				
			||||||
		o = parse_psir(o, data.psir, data.psir_len);
 | 
					 | 
				
			||||||
		free(data.psir);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	free(data.mpf_offsets);
 | 
					 | 
				
			||||||
	return jv_set(o, jv_string("markers"), markers);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// --- I/O ---------------------------------------------------------------------
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
do_file(const char *filename, jv o)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	const char *err = NULL;
 | 
					 | 
				
			||||||
	FILE *fp = fopen(filename, "rb");
 | 
					 | 
				
			||||||
	if (!fp) {
 | 
					 | 
				
			||||||
		err = strerror(errno);
 | 
					 | 
				
			||||||
		goto error;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	uint8_t *data = NULL, buf[256 << 10];
 | 
					 | 
				
			||||||
	size_t n, len = 0;
 | 
					 | 
				
			||||||
	while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
 | 
					 | 
				
			||||||
		data = realloc(data, len + n);
 | 
					 | 
				
			||||||
		memcpy(data + len, buf, n);
 | 
					 | 
				
			||||||
		len += n;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (ferror(fp)) {
 | 
					 | 
				
			||||||
		err = strerror(errno);
 | 
					 | 
				
			||||||
		goto error_read;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#if 0
 | 
					 | 
				
			||||||
	// Not sure if I want to ensure their existence...
 | 
					 | 
				
			||||||
	o = jv_object_set(o, jv_string("info"), jv_array());
 | 
					 | 
				
			||||||
	o = jv_object_set(o, jv_string("warnings"), jv_array());
 | 
					 | 
				
			||||||
#endif
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	o = parse_jpeg(o, data, len);
 | 
					 | 
				
			||||||
error_read:
 | 
					 | 
				
			||||||
	fclose(fp);
 | 
					 | 
				
			||||||
	free(data);
 | 
					 | 
				
			||||||
error:
 | 
					 | 
				
			||||||
	if (err)
 | 
					 | 
				
			||||||
		o = add_error(o, err);
 | 
					 | 
				
			||||||
	return o;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int
 | 
					 | 
				
			||||||
main(int argc, char *argv[])
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
 | 
					 | 
				
			||||||
	// Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
 | 
					 | 
				
			||||||
	for (int i = 1; i < argc; i++) {
 | 
					 | 
				
			||||||
		const char *filename = argv[i];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jv o = jv_object();
 | 
					 | 
				
			||||||
		o = jv_object_set(o, jv_string("filename"), jv_string(filename));
 | 
					 | 
				
			||||||
		o = do_file(filename, o);
 | 
					 | 
				
			||||||
		jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
 | 
					 | 
				
			||||||
		fputc('\n', stdout);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -348,6 +348,8 @@ error:
 | 
				
			|||||||
int
 | 
					int
 | 
				
			||||||
main(int argc, char *argv[])
 | 
					main(int argc, char *argv[])
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
 | 
						(void) parse_icc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
 | 
						// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
 | 
				
			||||||
	// Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
 | 
						// Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
 | 
				
			||||||
	for (int i = 1; i < argc; i++) {
 | 
						for (int i = 1; i < argc; i++) {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										175
									
								
								tools/rawinfo.c
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,175 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					// rawinfo.c: acquire information about raw image files in JSON format
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Copyright (c) 2023, Přemysl Eric Janouch <p@janouch.name>
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// Permission to use, copy, modify, and/or distribute this software for any
 | 
				
			||||||
 | 
					// purpose with or without fee is hereby granted.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
				
			||||||
 | 
					// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
				
			||||||
 | 
					// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
				
			||||||
 | 
					// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
				
			||||||
 | 
					// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
				
			||||||
 | 
					// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
				
			||||||
 | 
					// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include "info.h"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <jv.h>
 | 
				
			||||||
 | 
					#include <libraw.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#if LIBRAW_VERSION < LIBRAW_MAKE_VERSION(0, 21, 0)
 | 
				
			||||||
 | 
					#error LibRaw 0.21.0 or newer is required.
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <errno.h>
 | 
				
			||||||
 | 
					#include <stdio.h>
 | 
				
			||||||
 | 
					#include <stdlib.h>
 | 
				
			||||||
 | 
					#include <string.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- Raw image files ---------------------------------------------------------
 | 
				
			||||||
 | 
					// This is in principle similar to LibRaw's `raw-identify -v`,
 | 
				
			||||||
 | 
					// but the output is machine-processable.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					parse_raw(jv o, const uint8_t *p, size_t len)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						libraw_data_t *iprc = libraw_init(LIBRAW_OPIONS_NO_DATAERR_CALLBACK);
 | 
				
			||||||
 | 
						if (!iprc)
 | 
				
			||||||
 | 
							return add_error(o, "failed to obtain a LibRaw handle");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						int err = 0;
 | 
				
			||||||
 | 
						if ((err = libraw_open_buffer(iprc, p, len))) {
 | 
				
			||||||
 | 
							libraw_close(iprc);
 | 
				
			||||||
 | 
							return add_error(o, libraw_strerror(err));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						// -> iprc->rawparams.shot_select
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("count"), jv_number(iprc->idata.raw_count));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("width"), jv_number(iprc->sizes.width));
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("height"), jv_number(iprc->sizes.height));
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("flip"), jv_number(iprc->sizes.flip));
 | 
				
			||||||
 | 
						o = jv_set(o, jv_string("pixel_aspect_ratio"),
 | 
				
			||||||
 | 
							jv_number(iprc->sizes.pixel_aspect));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						if ((err = libraw_adjust_sizes_info_only(iprc))) {
 | 
				
			||||||
 | 
							o = add_warning(o, libraw_strerror(err));
 | 
				
			||||||
 | 
						} else {
 | 
				
			||||||
 | 
							o = jv_set(
 | 
				
			||||||
 | 
								o, jv_string("output_width"), jv_number(iprc->sizes.iwidth));
 | 
				
			||||||
 | 
							o = jv_set(
 | 
				
			||||||
 | 
								o, jv_string("output_height"), jv_number(iprc->sizes.iheight));
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						jv thumbnails = jv_array();
 | 
				
			||||||
 | 
						for (int i = 0; i < iprc->thumbs_list.thumbcount; i++) {
 | 
				
			||||||
 | 
							libraw_thumbnail_item_t *item = iprc->thumbs_list.thumblist + i;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							const char *format = "?";
 | 
				
			||||||
 | 
							switch (item->tformat) {
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_UNKNOWN:
 | 
				
			||||||
 | 
								format = "unknown";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_KODAK_THUMB:
 | 
				
			||||||
 | 
								format = "Kodak thumbnail";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_KODAK_YCBCR:
 | 
				
			||||||
 | 
								format = "Kodak YCbCr";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_KODAK_RGB:
 | 
				
			||||||
 | 
								format = "Kodak RGB";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_JPEG:
 | 
				
			||||||
 | 
								format = "JPEG";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_LAYER:
 | 
				
			||||||
 | 
								format = "layer";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_ROLLEI:
 | 
				
			||||||
 | 
								format = "Rollei";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_PPM:
 | 
				
			||||||
 | 
								format = "PPM";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_PPM16:
 | 
				
			||||||
 | 
								format = "PPM16";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							case LIBRAW_INTERNAL_THUMBNAIL_X3F:
 | 
				
			||||||
 | 
								format = "X3F";
 | 
				
			||||||
 | 
								break;
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							jv to = JV_OBJECT(
 | 
				
			||||||
 | 
								jv_string("width"), jv_number(item->twidth),
 | 
				
			||||||
 | 
								jv_string("height"), jv_number(item->theight),
 | 
				
			||||||
 | 
								jv_string("flip"), jv_number(item->tflip),
 | 
				
			||||||
 | 
								jv_string("format"), jv_string(format));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							if (item->tformat == LIBRAW_INTERNAL_THUMBNAIL_JPEG &&
 | 
				
			||||||
 | 
								item->toffset > 0 &&
 | 
				
			||||||
 | 
								(size_t) item->toffset + item->tlength <= len) {
 | 
				
			||||||
 | 
								to = jv_set(to, jv_string("JPEG"),
 | 
				
			||||||
 | 
									parse_jpeg(jv_object(), p + item->toffset, item->tlength));
 | 
				
			||||||
 | 
							}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							thumbnails = jv_array_append(thumbnails, to);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						libraw_close(iprc);
 | 
				
			||||||
 | 
						return jv_set(o, jv_string("thumbnails"), thumbnails);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// --- I/O ---------------------------------------------------------------------
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					static jv
 | 
				
			||||||
 | 
					do_file(const char *filename, jv o)
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						const char *err = NULL;
 | 
				
			||||||
 | 
						FILE *fp = fopen(filename, "rb");
 | 
				
			||||||
 | 
						if (!fp) {
 | 
				
			||||||
 | 
							err = strerror(errno);
 | 
				
			||||||
 | 
							goto error;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						uint8_t *data = NULL, buf[256 << 10];
 | 
				
			||||||
 | 
						size_t n, len = 0;
 | 
				
			||||||
 | 
						while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
 | 
				
			||||||
 | 
							data = realloc(data, len + n);
 | 
				
			||||||
 | 
							memcpy(data + len, buf, n);
 | 
				
			||||||
 | 
							len += n;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						if (ferror(fp)) {
 | 
				
			||||||
 | 
							err = strerror(errno);
 | 
				
			||||||
 | 
							goto error_read;
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						o = parse_raw(o, data, len);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					error_read:
 | 
				
			||||||
 | 
						fclose(fp);
 | 
				
			||||||
 | 
						free(data);
 | 
				
			||||||
 | 
					error:
 | 
				
			||||||
 | 
						if (err)
 | 
				
			||||||
 | 
							o = add_error(o, err);
 | 
				
			||||||
 | 
						return o;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					int
 | 
				
			||||||
 | 
					main(int argc, char *argv[])
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
						// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
 | 
				
			||||||
 | 
						// Usage: find . -print0 | xargs -0 ./rawinfo
 | 
				
			||||||
 | 
						for (int i = 1; i < argc; i++) {
 | 
				
			||||||
 | 
							const char *filename = argv[i];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
							jv o = jv_object();
 | 
				
			||||||
 | 
							o = jv_object_set(o, jv_string("filename"), jv_string(filename));
 | 
				
			||||||
 | 
							o = do_file(filename, o);
 | 
				
			||||||
 | 
							jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
 | 
				
			||||||
 | 
							fputc('\n', stdout);
 | 
				
			||||||
 | 
						}
 | 
				
			||||||
 | 
						return 0;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,79 +0,0 @@
 | 
				
			|||||||
//
 | 
					 | 
				
			||||||
// tiffinfo.c: acquire information about TIFF files in JSON format
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Copyright (c) 2021, Přemysl Eric Janouch <p@janouch.name>
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// Permission to use, copy, modify, and/or distribute this software for any
 | 
					 | 
				
			||||||
// purpose with or without fee is hereby granted.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 | 
					 | 
				
			||||||
// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 | 
					 | 
				
			||||||
// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
 | 
					 | 
				
			||||||
// SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 | 
					 | 
				
			||||||
// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION
 | 
					 | 
				
			||||||
// OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
 | 
					 | 
				
			||||||
// CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 | 
					 | 
				
			||||||
//
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include "info.h"
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <jv.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
#include <errno.h>
 | 
					 | 
				
			||||||
#include <stdlib.h>
 | 
					 | 
				
			||||||
#include <string.h>
 | 
					 | 
				
			||||||
#include <stdbool.h>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// This is essentially the same as jpeginfo.c, but we only have an Exif segment.
 | 
					 | 
				
			||||||
// TODO(p): Photoshop data and ICC profiles also have their tag,
 | 
					 | 
				
			||||||
// they're not currently processed.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
static jv
 | 
					 | 
				
			||||||
do_file(const char *filename, jv o)
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	const char *err = NULL;
 | 
					 | 
				
			||||||
	FILE *fp = fopen(filename, "rb");
 | 
					 | 
				
			||||||
	if (!fp) {
 | 
					 | 
				
			||||||
		err = strerror(errno);
 | 
					 | 
				
			||||||
		goto error;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	uint8_t *data = NULL, buf[256 << 10];
 | 
					 | 
				
			||||||
	size_t n, len = 0;
 | 
					 | 
				
			||||||
	while ((n = fread(buf, sizeof *buf, sizeof buf / sizeof *buf, fp))) {
 | 
					 | 
				
			||||||
		data = realloc(data, len + n);
 | 
					 | 
				
			||||||
		memcpy(data + len, buf, n);
 | 
					 | 
				
			||||||
		len += n;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	if (ferror(fp)) {
 | 
					 | 
				
			||||||
		err = strerror(errno);
 | 
					 | 
				
			||||||
		goto error_read;
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
	o = parse_exif(o, data, len);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
error_read:
 | 
					 | 
				
			||||||
	fclose(fp);
 | 
					 | 
				
			||||||
	free(data);
 | 
					 | 
				
			||||||
error:
 | 
					 | 
				
			||||||
	if (err)
 | 
					 | 
				
			||||||
		o = add_error(o, err);
 | 
					 | 
				
			||||||
	return o;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
int
 | 
					 | 
				
			||||||
main(int argc, char *argv[])
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
	// XXX: Can't use `xargs -P0`, there's a risk of non-atomic writes.
 | 
					 | 
				
			||||||
	// Usage: find . -iname *.png -print0 | xargs -0 ./pnginfo
 | 
					 | 
				
			||||||
	for (int i = 1; i < argc; i++) {
 | 
					 | 
				
			||||||
		const char *filename = argv[i];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
		jv o = jv_object();
 | 
					 | 
				
			||||||
		o = jv_object_set(o, jv_string("filename"), jv_string(filename));
 | 
					 | 
				
			||||||
		o = do_file(filename, o);
 | 
					 | 
				
			||||||
		jv_dumpf(o, stdout, 0 /* Might consider JV_PRINT_SORTED. */);
 | 
					 | 
				
			||||||
		fputc('\n', stdout);
 | 
					 | 
				
			||||||
	}
 | 
					 | 
				
			||||||
	return 0;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
							
								
								
									
										31
									
								
								xdg.c
									
									
									
									
									
								
							
							
						
						@@ -17,11 +17,14 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
#include <glib.h>
 | 
					#include <glib.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#include <stdlib.h>
 | 
				
			||||||
 | 
					#include <string.h>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// Add `element` to the `output` set. `relation` is a map of sets of strings
 | 
					/// Add `element` to the `output` set. `relation` is a map of sets of strings
 | 
				
			||||||
/// defining is-a relations, and is traversed recursively.
 | 
					/// defining is-a relations, and is traversed recursively.
 | 
				
			||||||
static void
 | 
					static void
 | 
				
			||||||
add_applying_transitive_closure(
 | 
					add_applying_transitive_closure(
 | 
				
			||||||
	const gchar *element, GHashTable *relation, GHashTable *output)
 | 
						const char *element, GHashTable *relation, GHashTable *output)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	// Stop condition.
 | 
						// Stop condition.
 | 
				
			||||||
	if (!g_hash_table_add(output, g_strdup(element)))
 | 
						if (!g_hash_table_add(output, g_strdup(element)))
 | 
				
			||||||
@@ -45,34 +48,46 @@ char *
 | 
				
			|||||||
get_xdg_home_dir(const char *var, const char *default_)
 | 
					get_xdg_home_dir(const char *var, const char *default_)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	const char *env = getenv(var);
 | 
						const char *env = getenv(var);
 | 
				
			||||||
	if (env && *env == '/')
 | 
						if (env && g_path_is_absolute(env))
 | 
				
			||||||
		return g_strdup(env);
 | 
							return g_strdup(env);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef G_OS_WIN32
 | 
				
			||||||
 | 
						return g_build_filename(g_get_home_dir(), default_, NULL);
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
	// The specification doesn't handle a missing HOME variable explicitly.
 | 
						// The specification doesn't handle a missing HOME variable explicitly.
 | 
				
			||||||
	// Implicitly, assuming Bourne shell semantics, it simply resolves empty.
 | 
						// Implicitly, assuming Bourne shell semantics, it simply resolves empty.
 | 
				
			||||||
	const char *home = getenv("HOME");
 | 
						const char *home = getenv("HOME");
 | 
				
			||||||
	return g_build_filename(home ? home : "", default_, NULL);
 | 
						return g_build_filename(home ? home : "", default_, NULL);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// Reïmplemented partly due to https://gitlab.gnome.org/GNOME/glib/-/issues/2501
 | 
				
			||||||
static gchar **
 | 
					static gchar **
 | 
				
			||||||
get_xdg_data_dirs(void)
 | 
					get_xdg_data_dirs(void)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	// GStrvBuilder is too new, it would help a little bit.
 | 
						// GStrvBuilder is too new, it would help a little bit.
 | 
				
			||||||
	GPtrArray *output = g_ptr_array_new_with_free_func(g_free);
 | 
						GPtrArray *output = g_ptr_array_new_with_free_func(g_free);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#ifdef G_OS_WIN32
 | 
				
			||||||
 | 
						g_ptr_array_add(output, g_strdup(g_get_user_data_dir()));
 | 
				
			||||||
 | 
						for (const gchar *const *p = g_get_system_data_dirs(); *p; p++)
 | 
				
			||||||
 | 
							g_ptr_array_add(output, g_strdup(*p));
 | 
				
			||||||
 | 
					#else
 | 
				
			||||||
	g_ptr_array_add(output, get_xdg_home_dir("XDG_DATA_HOME", ".local/share"));
 | 
						g_ptr_array_add(output, get_xdg_home_dir("XDG_DATA_HOME", ".local/share"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	const gchar *xdg_data_dirs;
 | 
						const char *xdg_data_dirs = "";
 | 
				
			||||||
	if (!(xdg_data_dirs = getenv("XDG_DATA_DIRS")) || !*xdg_data_dirs)
 | 
						if (!(xdg_data_dirs = getenv("XDG_DATA_DIRS")) || !*xdg_data_dirs)
 | 
				
			||||||
		xdg_data_dirs = "/usr/local/share/:/usr/share/";
 | 
							xdg_data_dirs = "/usr/local/share/:/usr/share/";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gchar **candidates = g_strsplit(xdg_data_dirs, ":", 0);
 | 
						gchar **candidates = g_strsplit(xdg_data_dirs, G_SEARCHPATH_SEPARATOR_S, 0);
 | 
				
			||||||
	for (gchar **p = candidates; *p; p++) {
 | 
						for (gchar **p = candidates; *p; p++) {
 | 
				
			||||||
		if (**p == '/')
 | 
							if (g_path_is_absolute(*p))
 | 
				
			||||||
			g_ptr_array_add(output, *p);
 | 
								g_ptr_array_add(output, *p);
 | 
				
			||||||
		else
 | 
							else
 | 
				
			||||||
			g_free(*p);
 | 
								g_free(*p);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
	g_free(candidates);
 | 
						g_free(candidates);
 | 
				
			||||||
 | 
					#endif
 | 
				
			||||||
	g_ptr_array_add(output, NULL);
 | 
						g_ptr_array_add(output, NULL);
 | 
				
			||||||
	return (gchar **) g_ptr_array_free(output, FALSE);
 | 
						return (gchar **) g_ptr_array_free(output, FALSE);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -82,7 +97,7 @@ get_xdg_data_dirs(void)
 | 
				
			|||||||
// Derived from shared-mime-info-spec 0.21.
 | 
					// Derived from shared-mime-info-spec 0.21.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static void
 | 
					static void
 | 
				
			||||||
read_mime_subclasses(const gchar *path, GHashTable *subclass_sets)
 | 
					read_mime_subclasses(const char *path, GHashTable *subclass_sets)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	gchar *data = NULL;
 | 
						gchar *data = NULL;
 | 
				
			||||||
	if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */))
 | 
						if (!g_file_get_contents(path, &data, NULL /* length */, NULL /* error */))
 | 
				
			||||||
@@ -112,7 +127,7 @@ read_mime_subclasses(const gchar *path, GHashTable *subclass_sets)
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
static gboolean
 | 
					static gboolean
 | 
				
			||||||
filter_mime_globs(const gchar *path, guint is_globs2, GHashTable *supported_set,
 | 
					filter_mime_globs(const char *path, guint is_globs2, GHashTable *supported_set,
 | 
				
			||||||
	GHashTable *output_set)
 | 
						GHashTable *output_set)
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
	gchar *data = NULL;
 | 
						gchar *data = NULL;
 | 
				
			||||||
@@ -120,7 +135,7 @@ filter_mime_globs(const gchar *path, guint is_globs2, GHashTable *supported_set,
 | 
				
			|||||||
		return FALSE;
 | 
							return FALSE;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	gchar *datasave = NULL;
 | 
						gchar *datasave = NULL;
 | 
				
			||||||
	for (const gchar *line = strtok_r(data, "\r\n", &datasave); line;
 | 
						for (const char *line = strtok_r(data, "\r\n", &datasave); line;
 | 
				
			||||||
			line = strtok_r(NULL, "\r\n", &datasave)) {
 | 
								line = strtok_r(NULL, "\r\n", &datasave)) {
 | 
				
			||||||
		if (*line == '#')
 | 
							if (*line == '#')
 | 
				
			||||||
			continue;
 | 
								continue;
 | 
				
			||||||
 
 | 
				
			|||||||